diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..5480842b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "kiroAgent.configureMCP": "Disabled" +} \ No newline at end of file diff --git a/quicklendx-contracts/src/lib.rs b/quicklendx-contracts/src/lib.rs index bc8966bb..fd9b5b25 100644 --- a/quicklendx-contracts/src/lib.rs +++ b/quicklendx-contracts/src/lib.rs @@ -1,211 +1,3 @@ -<<<<<<< feature/dispute-resolution-finality -#![cfg_attr(target_family = "wasm", no_std)] -#[cfg(target_family = "wasm")] -extern crate alloc; - -#[cfg(test)] -mod scratch_events; -#[cfg(test)] -mod test_default; -#[cfg(test)] -mod test_fees; -#[cfg(test)] -mod test_fees_extended; -use soroban_sdk::{contract, contractimpl, symbol_short, Address, BytesN, Env, Map, String, Vec}; - -mod admin; -mod analytics; -mod audit; -mod backup; -mod bid; -mod currency; -mod defaults; -mod dispute; -mod emergency; -mod errors; -mod escrow; -mod events; -mod fees; -mod init; -mod investment; -mod investment_queries; -mod invoice; -mod notifications; -mod pause; -mod payments; -mod profits; -mod protocol_limits; -mod reentrancy; -mod settlement; -mod storage; -#[cfg(test)] -#[cfg(test)] -mod test_admin; -#[cfg(test)] -mod test_admin_simple; -#[cfg(test)] -mod test_admin_standalone; -#[cfg(test)] -mod test_dispute; -#[cfg(test)] -mod test_init; -#[cfg(test)] -mod test_investment_consistency; -#[cfg(test)] -mod test_investment_queries; -#[cfg(test)] -mod test_max_invoices_per_business; -#[cfg(test)] -mod test_overflow; -#[cfg(test)] -mod test_pause; -#[cfg(test)] -mod test_profit_fee; -#[cfg(test)] -mod test_refund; -#[cfg(test)] -mod test_storage; -#[cfg(test)] -mod test_string_limits; -#[cfg(test)] -mod test_types; -#[cfg(test)] -mod test_vesting; -pub mod types; -pub use invoice::{InvoiceCategory, InvoiceStatus}; -mod verification; -mod vesting; -use admin::AdminStorage; -use bid::{Bid, BidStorage}; -use defaults::{ - handle_default as do_handle_default, mark_invoice_defaulted as do_mark_invoice_defaulted, - OverdueScanResult, -}; -use errors::QuickLendXError; -use escrow::{ - accept_bid_and_fund as do_accept_bid_and_fund, refund_escrow_funds as do_refund_escrow_funds, -}; -use events::{ - emit_bid_accepted, emit_bid_placed, emit_bid_withdrawn, emit_escrow_created, - emit_escrow_released, emit_insurance_added, emit_insurance_premium_collected, - emit_investor_verified, emit_invoice_cancelled, emit_invoice_metadata_cleared, - emit_invoice_metadata_updated, emit_invoice_uploaded, emit_invoice_verified, -}; -use investment::{InsuranceCoverage, Investment, InvestmentStatus, InvestmentStorage}; -use invoice::{Invoice, InvoiceMetadata, InvoiceStorage}; -use payments::{create_escrow, release_escrow, EscrowStorage}; -use profits::{calculate_profit as do_calculate_profit, PlatformFee, PlatformFeeConfig}; -use settlement::{ - process_partial_payment as do_process_partial_payment, settle_invoice as do_settle_invoice, -}; -use verification::{ - calculate_investment_limit, calculate_investor_risk_score, determine_investor_tier, - get_investor_verification as do_get_investor_verification, normalize_tag, reject_business, - reject_investor as do_reject_investor, require_business_not_pending, - require_investor_not_pending, submit_investor_kyc as do_submit_investor_kyc, - submit_kyc_application, validate_bid, validate_investor_investment, validate_invoice_metadata, - verify_business, verify_investor as do_verify_investor, verify_invoice_data, - BusinessVerificationStatus, BusinessVerificationStorage, InvestorRiskLevel, InvestorTier, - InvestorVerification, InvestorVerificationStorage, -}; - -pub use crate::types::*; - -#[contract] -pub struct QuickLendXContract; - -/// Maximum number of records returned by paginated query endpoints. -pub(crate) const MAX_QUERY_LIMIT: u32 = 100; - -/// @notice Validates and caps query limit to prevent resource abuse -/// @param limit The requested limit value -/// @return The capped limit value, never exceeding MAX_QUERY_LIMIT -/// @dev Returns 0 if limit is 0, enforcing empty result behavior -#[inline] -fn cap_query_limit(limit: u32) -> u32 { - investment_queries::InvestmentQueries::cap_query_limit(limit) -} - -/// @notice Validates query parameters for security and resource protection -/// @param offset The pagination offset -/// @param limit The requested result limit -/// @return Result indicating validation success or failure -/// @dev Prevents potential overflow and ensures reasonable query bounds -fn validate_query_params(offset: u32, limit: u32) -> Result<(), QuickLendXError> { - // Check for potential overflow in offset + limit calculation - if offset > u32::MAX - MAX_QUERY_LIMIT { - return Err(QuickLendXError::InvalidAmount); - } - - // Limit is automatically capped by cap_query_limit, but we validate the input - // Note: limit=0 is allowed and results in empty response - Ok(()) -} - -/// Map the contract-exported `types::BidStatus` filter to the bid-storage enum. -fn map_public_bid_status(s: BidStatus) -> bid::BidStatus { - match s { - BidStatus::Placed => bid::BidStatus::Placed, - BidStatus::Withdrawn => bid::BidStatus::Withdrawn, - BidStatus::Accepted => bid::BidStatus::Accepted, - BidStatus::Expired => bid::BidStatus::Expired, - } -} - -#[contractimpl] -impl QuickLendXContract { - // ============================================================================ - // Admin Management Functions - // ============================================================================ - - /// Initialize the protocol with all required configuration (one-time setup) - pub fn initialize(env: Env, params: init::InitializationParams) -> Result<(), QuickLendXError> { - init::ProtocolInitializer::initialize(&env, ¶ms) - } - - /// Check if the protocol has been initialized - pub fn is_initialized(env: Env) -> bool { - init::ProtocolInitializer::is_initialized(&env) - } - - /// Get the protocol/contract version - /// - /// Returns the version written during initialization, or the current - /// PROTOCOL_VERSION constant if the contract has not been initialized yet. - /// - /// # Returns - /// * `u32` - The protocol version number - /// - /// # Version Format - /// Version is a simple integer increment (e.g., 1, 2, 3...) - /// Major versions indicate breaking changes that require migration. - pub fn get_version(_env: Env) -> u32 { - 1u32 - } - - /// Get current protocol limits - pub fn get_protocol_limits(env: Env) -> protocol_limits::ProtocolLimits { - protocol_limits::ProtocolLimitsContract::get_protocol_limits(env) - } - - /// Initialize the admin address (deprecated: use initialize) - pub fn initialize_admin(env: Env, admin: Address) -> Result<(), QuickLendXError> { - AdminStorage::initialize(&env, &admin) - } - - /// Transfer admin role to a new address - /// - /// # Arguments - /// * `env` - The contract environment - /// * `new_admin` - The new admin address - /// - /// # Returns - /// * `Ok(())` if transfer succeeds - /// * `Err(QuickLendXError::NotAdmin)` if caller is not current admin - /// - /// # Security - /// - Requires authorization from current admin - pub fn transfer_admin(env: Env, new_admin: Address) -> Result<(), QuickLendXError> { let current_admin = AdminStorage::get_admin(&env).ok_or(QuickLendXError::NotAdmin)?; AdminStorage::transfer_admin(&env, ¤t_admin, &new_admin) } @@ -277,7 +69,7 @@ impl QuickLendXContract { /// Admin-only: configure default bid TTL (days). Bounds: 1..=30. pub fn set_bid_ttl_days(env: Env, days: u64) -> Result { - let admin = AdminStorage::get_admin(&env).ok_or(QuickLendXError::NotAdmin)?; + let admin = AdminStorage::require_current_admin(&env)?; bid::BidStorage::set_bid_ttl_days(&env, &admin, days) } @@ -360,6 +152,7 @@ impl QuickLendXContract { admin: Address, currency: Address, ) -> Result<(), QuickLendXError> { + AdminStorage::require_current_admin(&env)?; currency::CurrencyWhitelist::add_currency(&env, &admin, ¤cy) } @@ -369,6 +162,7 @@ impl QuickLendXContract { admin: Address, currency: Address, ) -> Result<(), QuickLendXError> { + AdminStorage::require_current_admin(&env)?; currency::CurrencyWhitelist::remove_currency(&env, &admin, ¤cy) } @@ -382,7 +176,8 @@ impl QuickLendXContract { currency::CurrencyWhitelist::get_whitelisted_currencies(&env) } - /// Replace the entire currency whitelist atomically (admin only). + /// @notice Replace the entire currency whitelist atomically. + /// @dev Requires authenticated admin approval; no caller-address fallback is allowed. pub fn set_currencies( env: Env, admin: Address, @@ -1388,7 +1183,8 @@ impl QuickLendXContract { BusinessVerificationStorage::get_admin(&env) } - /// Initialize protocol limits (admin only). Sets min amount, max due date days, grace period. + /// @notice Initialize protocol limits. + /// @dev Requires authenticated canonical admin approval before initializing or updating limits. pub fn initialize_protocol_limits( env: Env, admin: Address, @@ -1396,6 +1192,10 @@ impl QuickLendXContract { max_due_date_days: u64, grace_period_seconds: u64, ) -> Result<(), QuickLendXError> { + let current_admin = AdminStorage::require_current_admin(&env)?; + if admin != current_admin { + return Err(QuickLendXError::NotAdmin); + } let _ = protocol_limits::ProtocolLimitsContract::initialize(env.clone(), admin.clone()); protocol_limits::ProtocolLimitsContract::set_protocol_limits( env, @@ -1839,8 +1639,13 @@ impl QuickLendXContract { // Fee and Revenue Management Functions // ======================================== - /// Initialize fee management system + /// @notice Initialize fee management storage. + /// @dev Requires authenticated canonical admin approval and rejects mismatched caller addresses. pub fn initialize_fee_system(env: Env, admin: Address) -> Result<(), QuickLendXError> { + let current_admin = AdminStorage::require_current_admin(&env)?; + if admin != current_admin { + return Err(QuickLendXError::NotAdmin); + } fees::FeeManager::initialize(&env, &admin) } @@ -1858,10 +1663,10 @@ impl QuickLendXContract { Ok(()) } - /// Update platform fee basis points (admin only) + /// @notice Update the platform fee basis points. + /// @dev Requires the stored admin to authenticate for the current invocation. pub fn update_platform_fee_bps(env: Env, new_fee_bps: u32) -> Result<(), QuickLendXError> { - let admin = - BusinessVerificationStorage::get_admin(&env).ok_or(QuickLendXError::NotAdmin)?; + let admin = AdminStorage::require_current_admin(&env)?; let old_config = fees::FeeManager::get_platform_fee_config(&env)?; let old_fee_bps = old_config.fee_bps; @@ -2934,8 +2739,7 @@ impl QuickLendXContract { }); (platform, performance) } -} -======= + /// QuickLendX Smart Contract Library /// /// This crate contains the core arithmetic modules for the QuickLendX @@ -2971,5 +2775,4 @@ mod test_fuzz; mod test_business_kyc; #[cfg(test)] -mod test_investor_kyc; ->>>>>>> main +mod test_investor_kyc; \ No newline at end of file diff --git a/quicklendx-contracts/src/test/test_analytics.rs b/quicklendx-contracts/src/test/test_analytics.rs index d134eb2e..5292706e 100644 --- a/quicklendx-contracts/src/test/test_analytics.rs +++ b/quicklendx-contracts/src/test/test_analytics.rs @@ -18,19 +18,26 @@ use soroban_sdk::{ fn setup_contract(env: &Env) -> (QuickLendXContractClient<'_>, Address, Address) { let contract_id = env.register(QuickLendXContract, ()); - let client = QuickLendXContractClient::new(env, &contract_id); - let admin = Address::generate(env); - let business = Address::generate(env); - env.mock_all_auths(); + let client = QuickLendXContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let business = Address::generate(&env); + let currency = Address::generate(&env); + client.set_admin(&admin); - (client, admin, business) + client.submit_kyc_application(&business, &String::from_str(&env, "Business KYC")); + client.verify_business(&admin, &business); + + (env, client, admin, business, currency) } -fn create_invoice( +fn upload_invoice( env: &Env, client: &QuickLendXContractClient, business: &Address, + currency: &Address, amount: i128, + category: InvoiceCategory, description: &str, ) -> BytesN<32> { let currency = Address::generate(env); @@ -38,10 +45,10 @@ fn create_invoice( client.store_invoice( business, &amount, - ¤cy, - &due_date, + currency, + &(env.ledger().timestamp() + 86_400), &String::from_str(env, description), - &InvoiceCategory::Services, + &category, &Vec::new(env), ) } @@ -67,13 +74,9 @@ fn test_platform_metrics_empty_data() { } #[test] -fn test_platform_metrics_with_invoices() { +fn test_platform_metrics_empty_summary_defaults() { let env = Env::default(); - let (client, _admin, business) = setup_contract(&env); - - create_invoice(&env, &client, &business, 1000, "Invoice A"); - create_invoice(&env, &client, &business, 2000, "Invoice B"); - create_invoice(&env, &client, &business, 3000, "Invoice C"); + let (platform, performance) = crate::get_analytics_summary(env); let metrics = client.get_platform_metrics().unwrap(); assert_eq!(metrics.total_invoices, 3); @@ -82,19 +85,27 @@ fn test_platform_metrics_with_invoices() { } #[test] -fn test_platform_metrics_after_status_changes() { - let env = Env::default(); - let (client, _admin, business) = setup_contract(&env); - - let inv1 = create_invoice(&env, &client, &business, 1000, "Status inv 1"); - let inv2 = create_invoice(&env, &client, &business, 2000, "Status inv 2"); +fn test_platform_metrics_with_multiple_invoices() { + let (env, client, _admin, business, currency) = setup(); - // Verify and fund inv1 - client.update_invoice_status(&inv1, &InvoiceStatus::Verified); - client.update_invoice_status(&inv1, &InvoiceStatus::Funded); - - // Mark inv2 as paid - client.update_invoice_status(&inv2, &InvoiceStatus::Paid); + upload_invoice( + &env, + &client, + &business, + ¤cy, + 1_000, + InvoiceCategory::Services, + "Invoice A", + ); + upload_invoice( + &env, + &client, + &business, + ¤cy, + 2_000, + InvoiceCategory::Technology, + "Invoice B", + ); let metrics = client.get_platform_metrics().unwrap(); assert_eq!(metrics.total_invoices, 2); @@ -216,112 +227,140 @@ fn test_user_behavior_new_user() { } #[test] -fn test_user_behavior_with_invoices() { - let env = Env::default(); - env.ledger().set_timestamp(1_000_000); - let (client, _admin, business) = setup_contract(&env); +fn test_user_behavior_metrics_tracks_uploaded_invoices() { + let (env, client, _admin, business, currency) = setup(); - create_invoice(&env, &client, &business, 1000, "Behavior inv 1"); - create_invoice(&env, &client, &business, 2000, "Behavior inv 2"); + upload_invoice( + &env, + &client, + &business, + ¤cy, + 1_000, + InvoiceCategory::Services, + "Behavior invoice 1", + ); + upload_invoice( + &env, + &client, + &business, + ¤cy, + 2_500, + InvoiceCategory::Consulting, + "Behavior invoice 2", + ); - let behavior = client.get_user_behavior_metrics(&business); - assert_eq!(behavior.total_invoices_uploaded, 2); - assert!(behavior.last_activity > 0); + let metrics = crate::get_user_behavior_metrics(env.clone(), business.clone()); + assert_eq!(metrics.user_address, business); + assert_eq!(metrics.total_invoices_uploaded, 2); + assert_eq!(metrics.total_investments_made, 0); + assert_eq!(metrics.risk_score, 25); + assert!(metrics.last_activity > 0); } -// ============================================================================ -// FINANCIAL METRICS TESTS -// ============================================================================ - #[test] -fn test_financial_metrics_empty_data() { - let env = Env::default(); - let (client, _admin, _business) = setup_contract(&env); +fn test_financial_metrics_respects_period_filter_and_categories() { + let (env, client, _admin, business, currency) = setup(); - let metrics = client.get_financial_metrics(&TimePeriod::AllTime); - assert_eq!(metrics.total_volume, 0); - assert_eq!(metrics.total_fees, 0); - assert_eq!(metrics.total_profits, 0); - assert_eq!(metrics.average_return_rate, 0); -} + let old_invoice = upload_invoice( + &env, + &client, + &business, + ¤cy, + 1_000, + InvoiceCategory::Services, + "Old invoice", + ); + let mut old = InvoiceStorage::get_invoice(&env, &old_invoice).unwrap(); + old.created_at = env.ledger().timestamp() - (31 * 24 * 60 * 60); + InvoiceStorage::store_invoice(&env, &old); -#[test] -fn test_financial_metrics_with_invoices_all_time() { - let env = Env::default(); - env.ledger().set_timestamp(1_000_000); - let (client, _admin, business) = setup_contract(&env); + upload_invoice( + &env, + &client, + &business, + ¤cy, + 2_500, + InvoiceCategory::Technology, + "Recent invoice", + ); - create_invoice(&env, &client, &business, 5000, "Financial inv 1"); - create_invoice(&env, &client, &business, 3000, "Financial inv 2"); + let monthly = crate::get_financial_metrics(env.clone(), TimePeriod::Monthly); + assert_eq!(monthly.total_volume, 2_500); - let metrics = client.get_financial_metrics(&TimePeriod::AllTime); - assert_eq!(metrics.total_volume, 8000); - // Volume by category should have Services category with 8000 - let mut services_volume = 0i128; - for (cat, vol) in metrics.volume_by_category.iter() { - if cat == InvoiceCategory::Services { - services_volume = vol; + let mut technology_volume = 0i128; + for (category, volume) in monthly.volume_by_category.iter() { + if category == InvoiceCategory::Technology { + technology_volume = volume; } } - assert_eq!(services_volume, 8000); + assert_eq!(technology_volume, 2_500); + + let all_time = crate::get_financial_metrics(env, TimePeriod::AllTime); + assert_eq!(all_time.total_volume, 3_500); } #[test] -fn test_financial_metrics_period_boundary() { - let env = Env::default(); - // Set timestamp to 2 days in - env.ledger().set_timestamp(2 * 86400); - let (client, _admin, business) = setup_contract(&env); +fn test_performance_metrics_reflect_paid_and_defaulted_invoices() { + let (env, client, _admin, business, currency) = setup(); - // Create invoice — its created_at will be the current timestamp (2 days) - create_invoice(&env, &client, &business, 1000, "Period boundary"); + let paid_invoice = upload_invoice( + &env, + &client, + &business, + ¤cy, + 1_000, + InvoiceCategory::Services, + "Paid invoice", + ); + let defaulted_invoice = upload_invoice( + &env, + &client, + &business, + ¤cy, + 2_000, + InvoiceCategory::Services, + "Defaulted invoice", + ); - // Daily period looks at last 24h → should include (since created_at == now, AllTime includes now) - let daily = client.get_financial_metrics(&TimePeriod::Daily); - // The invoice is at timestamp 2*86400, daily start = 2*86400 - 86400 = 86400 - // Invoice created_at (2*86400) >= start (86400) && <= end (2*86400) → included - assert_eq!(daily.total_volume, 1000); + client.update_invoice_status(&paid_invoice, &InvoiceStatus::Paid); + client.update_invoice_status(&defaulted_invoice, &InvoiceStatus::Defaulted); - // AllTime always includes everything - let all_time = client.get_financial_metrics(&TimePeriod::AllTime); - assert_eq!(all_time.total_volume, 1000); + let metrics = AnalyticsCalculator::calculate_performance_metrics(&env).unwrap(); + assert_eq!(metrics.transaction_success_rate, 5_000); + assert_eq!(metrics.error_rate, 5_000); } -// ============================================================================ -// BUSINESS REPORT TESTS -// ============================================================================ - #[test] -fn test_business_report_empty() { - let env = Env::default(); - env.ledger().set_timestamp(1_000_000); - let (client, _admin, business) = setup_contract(&env); - - let report = client.generate_business_report(&business, &TimePeriod::AllTime); - assert_eq!(report.business_address, business); - assert_eq!(report.invoices_uploaded, 0); - assert_eq!(report.invoices_funded, 0); - assert_eq!(report.total_volume, 0); - assert_eq!(report.success_rate, 0); - assert_eq!(report.default_rate, 0); - assert!(report.rating_average.is_none()); - assert_eq!(report.period, TimePeriod::AllTime); -} +fn test_business_report_generation_matches_invoice_state() { + let (env, client, _admin, business, currency) = setup(); -#[test] -fn test_business_report_with_invoices() { - let env = Env::default(); - env.ledger().set_timestamp(1_000_000); - let (client, _admin, business) = setup_contract(&env); + let funded = upload_invoice( + &env, + &client, + &business, + ¤cy, + 1_000, + InvoiceCategory::Services, + "Funded invoice", + ); + client.update_invoice_status(&funded, &InvoiceStatus::Funded); - let inv1 = create_invoice(&env, &client, &business, 1000, "Biz report inv 1"); - let _inv2 = create_invoice(&env, &client, &business, 2000, "Biz report inv 2"); + let paid = upload_invoice( + &env, + &client, + &business, + ¤cy, + 2_000, + InvoiceCategory::Technology, + "Paid invoice", + ); + client.update_invoice_status(&paid, &InvoiceStatus::Paid); - // Fund one invoice - client.update_invoice_status(&inv1, &InvoiceStatus::Verified); - client.update_invoice_status(&inv1, &InvoiceStatus::Funded); + let report = + crate::generate_business_report(env.clone(), business.clone(), TimePeriod::AllTime) + .unwrap(); - let report = client.generate_business_report(&business, &TimePeriod::AllTime); + assert_eq!(report.business_address, business); assert_eq!(report.invoices_uploaded, 2); assert_eq!(report.invoices_funded, 1); assert_eq!(report.total_volume, 3000); diff --git a/quicklendx-contracts/src/test_admin.rs b/quicklendx-contracts/src/test_admin.rs index 0258a85a..7abb78dc 100644 --- a/quicklendx-contracts/src/test_admin.rs +++ b/quicklendx-contracts/src/test_admin.rs @@ -1,574 +1,574 @@ -//! Comprehensive test suite for hardened admin role management. -//! -//! Test Coverage: -//! 1. Initialization — admin setup, double-init prevention, authorization -//! 2. Transfer — success path, authorization, validation, atomicity -//! 3. Query Functions — get_admin, is_admin, is_initialized -//! 4. Authorization — require_admin, require_current_admin -//! 5. Security — transfer locks, concurrent operations, edge cases -//! 6. Events — initialization, transfer audit trail -//! 7. Utilities — with_admin_auth, with_current_admin -//! 8. Legacy Compatibility — set_admin routing -//! -//! Target: 95%+ test coverage for admin.rs - -#[cfg(test)] -mod test_admin { - use crate::admin::AdminStorage; - use crate::errors::QuickLendXError; - use crate::{QuickLendXContract, QuickLendXContractClient}; - use soroban_sdk::{ - testutils::{Address as _, Events}, - Address, Env, - }; - - fn setup() -> (Env, QuickLendXContractClient<'static>) { - let env = Env::default(); - let contract_id = env.register(QuickLendXContract, ()); - let client = QuickLendXContractClient::new(&env, &contract_id); - (env, client) - } - - fn setup_with_admin() -> (Env, QuickLendXContractClient<'static>, Address) { - let (env, client) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - client.initialize_admin(&admin); - (env, client, admin) - } - - // ============================================================================ - // 1. Initialization Tests - // ============================================================================ - - #[test] - fn test_initialize_admin_succeeds() { - let (env, client) = setup(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let result = client.try_initialize_admin(&admin); - - assert!(result.is_ok(), "First initialization must succeed"); - assert_eq!( - client.get_current_admin(), - Some(admin.clone()), - "Stored admin must match initialized address" - ); - assert!( - AdminStorage::is_initialized(&env), - "Admin system must be marked as initialized" - ); - } - - #[test] - fn test_initialize_admin_requires_authorization() { - let env = Env::default(); - let contract_id = env.register(QuickLendXContract, ()); - let client = QuickLendXContractClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - - // Should panic without authorization - let result = std::panic::catch_unwind(|| { - client.initialize_admin(&admin); - }); - assert!(result.is_err(), "Initialization without auth must fail"); - } - - #[test] - fn test_initialize_admin_double_init_fails() { - let (env, client) = setup(); - env.mock_all_auths(); - - let admin1 = Address::generate(&env); - let admin2 = Address::generate(&env); - - // First initialization succeeds - client.initialize_admin(&admin1); - - // Second initialization fails - let result = client.try_initialize_admin(&admin2); - assert!(result.is_err(), "Double initialization must be rejected"); - - // Original admin remains - assert_eq!( - client.get_current_admin(), - Some(admin1), - "Original admin must remain after failed re-init" - ); - } - - #[test] - fn test_initialize_admin_same_address_twice_fails() { - let (env, client) = setup(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - client.initialize_admin(&admin); - - let result = client.try_initialize_admin(&admin); - assert!( - result.is_err(), - "Re-initializing with same address must fail" - ); - } - - #[test] - fn test_initialize_admin_emits_event() { - let (env, client) = setup(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - client.initialize_admin(&admin); - - let events = env.events().all(); - assert!(!events.is_empty(), "Initialization must emit event"); - - let event = &events[0]; - assert_eq!(event.0, (soroban_sdk::symbol_short!("adm_init"),)); - } - - // ============================================================================ - // 2. Admin Transfer Tests - // ============================================================================ - - #[test] - fn test_transfer_admin_succeeds() { - let (env, client, admin1) = setup_with_admin(); - let admin2 = Address::generate(&env); - - let result = client.try_transfer_admin(&admin1, &admin2); - assert!(result.is_ok(), "Admin transfer must succeed"); - - assert_eq!( - client.get_current_admin(), - Some(admin2), - "New admin must be stored" - ); - assert!( - !AdminStorage::is_admin(&env, &admin1), - "Old admin must no longer be admin" - ); - assert!( - AdminStorage::is_admin(&env, &admin2), - "New admin must be recognized" - ); - } - - #[test] - fn test_transfer_admin_requires_current_admin_auth() { - let (env, client, admin1) = setup_with_admin(); - let admin2 = Address::generate(&env); - let non_admin = Address::generate(&env); - - // Non-admin cannot transfer - let result = client.try_transfer_admin(&non_admin, &admin2); - assert!(result.is_err(), "Non-admin transfer must fail"); - - // Admin remains unchanged - assert_eq!( - client.get_current_admin(), - Some(admin1), - "Admin must remain unchanged after failed transfer" - ); - } - - #[test] - fn test_transfer_admin_to_self_fails() { - let (env, client, admin) = setup_with_admin(); - - let result = client.try_transfer_admin(&admin, &admin); - assert!(result.is_err(), "Transfer to self must fail"); - - assert_eq!( - client.get_current_admin(), - Some(admin), - "Admin must remain unchanged" - ); - } - - #[test] - fn test_transfer_admin_without_initialization_fails() { - let (env, client) = setup(); - env.mock_all_auths(); - - let admin1 = Address::generate(&env); - let admin2 = Address::generate(&env); - - let result = client.try_transfer_admin(&admin1, &admin2); - assert!(result.is_err(), "Transfer without initialization must fail"); - } - - #[test] - fn test_transfer_admin_emits_event() { - let (env, client, admin1) = setup_with_admin(); - let admin2 = Address::generate(&env); - - client.transfer_admin(&admin1, &admin2); - - let events = env.events().all(); - let transfer_events: Vec<_> = events - .iter() - .filter(|e| e.0 == (soroban_sdk::symbol_short!("adm_trf"),)) - .collect(); - - assert!(!transfer_events.is_empty(), "Transfer must emit event"); - } - - #[test] - fn test_transfer_admin_chain() { - let (env, client, admin1) = setup_with_admin(); - let admin2 = Address::generate(&env); - let admin3 = Address::generate(&env); - - // Transfer from admin1 to admin2 - client.transfer_admin(&admin1, &admin2); - assert_eq!(client.get_current_admin(), Some(admin2.clone())); - - // Transfer from admin2 to admin3 - client.transfer_admin(&admin2, &admin3); - assert_eq!(client.get_current_admin(), Some(admin3)); - - // admin1 can no longer transfer - let result = client.try_transfer_admin(&admin1, &admin2); - assert!(result.is_err(), "Old admin cannot transfer"); - } - - // ============================================================================ - // 3. Query Function Tests - // ============================================================================ - - #[test] - fn test_get_admin_before_initialization() { - let (env, client) = setup(); - - assert_eq!( - client.get_current_admin(), - None, - "Admin must be None before initialization" - ); - assert!( - !AdminStorage::is_initialized(&env), - "System must not be initialized" - ); - } - - #[test] - fn test_get_admin_after_initialization() { - let (env, client, admin) = setup_with_admin(); - - assert_eq!( - client.get_current_admin(), - Some(admin.clone()), - "Admin must be returned after initialization" - ); - assert!( - AdminStorage::is_initialized(&env), - "System must be initialized" - ); - } - - #[test] - fn test_is_admin_checks() { - let (env, client, admin) = setup_with_admin(); - let non_admin = Address::generate(&env); - - assert!( - AdminStorage::is_admin(&env, &admin), - "Admin address must return true" - ); - assert!( - !AdminStorage::is_admin(&env, &non_admin), - "Non-admin address must return false" - ); - } - - #[test] - fn test_is_admin_before_initialization() { - let (env, _client) = setup(); - let address = Address::generate(&env); - - assert!( - !AdminStorage::is_admin(&env, &address), - "No address should be admin before initialization" - ); - } - - // ============================================================================ - // 4. Authorization Tests - // ============================================================================ - - #[test] - fn test_require_admin_succeeds_for_admin() { - let (env, _client, admin) = setup_with_admin(); - - let result = AdminStorage::require_admin(&env, &admin); - assert!(result.is_ok(), "require_admin must succeed for admin"); - } - - #[test] - fn test_require_admin_fails_for_non_admin() { - let (env, _client, _admin) = setup_with_admin(); - let non_admin = Address::generate(&env); - - let result = AdminStorage::require_admin(&env, &non_admin); - assert_eq!( - result, - Err(QuickLendXError::NotAdmin), - "require_admin must fail for non-admin" - ); - } - - #[test] - fn test_require_admin_fails_before_initialization() { - let (env, _client) = setup(); - let address = Address::generate(&env); - - let result = AdminStorage::require_admin(&env, &address); - assert_eq!( - result, - Err(QuickLendXError::OperationNotAllowed), - "require_admin must fail before initialization" - ); - } - - #[test] - fn test_require_current_admin_succeeds() { - let (env, _client, admin) = setup_with_admin(); - - let result = AdminStorage::require_current_admin(&env); - assert!(result.is_ok(), "require_current_admin must succeed"); - assert_eq!(result.unwrap(), admin, "Must return correct admin address"); - } - - #[test] - fn test_require_current_admin_fails_before_initialization() { - let (env, _client) = setup(); - - let result = AdminStorage::require_current_admin(&env); - assert_eq!( - result, - Err(QuickLendXError::OperationNotAllowed), - "require_current_admin must fail before initialization" - ); - } - - // ============================================================================ - // 5. Security Tests - // ============================================================================ - - #[test] - fn test_admin_operations_atomic() { - let (env, client, admin1) = setup_with_admin(); - let admin2 = Address::generate(&env); - - // Verify atomicity by checking state before and after - assert!(AdminStorage::is_admin(&env, &admin1)); - assert!(!AdminStorage::is_admin(&env, &admin2)); - - client.transfer_admin(&admin1, &admin2); - - // State should be completely switched - assert!(!AdminStorage::is_admin(&env, &admin1)); - assert!(AdminStorage::is_admin(&env, &admin2)); - } - - #[test] - fn test_initialization_state_consistency() { - let (env, client) = setup(); - env.mock_all_auths(); - - // Before initialization - assert!(!AdminStorage::is_initialized(&env)); - assert_eq!(AdminStorage::get_admin(&env), None); - - let admin = Address::generate(&env); - client.initialize_admin(&admin); - - // After initialization - assert!(AdminStorage::is_initialized(&env)); - assert_eq!(AdminStorage::get_admin(&env), Some(admin)); - } - - // ============================================================================ - // 6. Utility Function Tests - // ============================================================================ - - #[test] - fn test_with_admin_auth_succeeds() { - let (env, _client, admin) = setup_with_admin(); - - let result = AdminStorage::with_admin_auth(&env, &admin, || Ok("success".to_string())); - - assert!(result.is_ok()); - assert_eq!(result.unwrap(), "success"); - } - - #[test] - fn test_with_admin_auth_fails_for_non_admin() { - let (env, _client, _admin) = setup_with_admin(); - let non_admin = Address::generate(&env); - - let result = AdminStorage::with_admin_auth(&env, &non_admin, || Ok("should not execute")); - - assert_eq!(result, Err(QuickLendXError::NotAdmin)); - } - - #[test] - fn test_with_current_admin_succeeds() { - let (env, _client, admin) = setup_with_admin(); - - let result = AdminStorage::with_current_admin(&env, |current_admin| { - assert_eq!(current_admin, &admin); - Ok("success") - }); - - assert!(result.is_ok()); - assert_eq!(result.unwrap(), "success"); - } - - #[test] - fn test_with_current_admin_fails_before_initialization() { - let (env, _client) = setup(); - - let result = AdminStorage::with_current_admin(&env, |_| Ok("should not execute")); - - assert_eq!(result, Err(QuickLendXError::OperationNotAllowed)); - } - - // ============================================================================ - // 7. Legacy Compatibility Tests - // ============================================================================ - - #[test] - fn test_set_admin_routes_to_initialize() { - let (env, client) = setup(); - env.mock_all_auths(); - - let admin = Address::generate(&env); - let result = client.try_set_admin(&admin); - - assert!(result.is_ok(), "set_admin must route to initialize"); - assert_eq!(client.get_current_admin(), Some(admin)); - assert!(AdminStorage::is_initialized(&env)); - } - - #[test] - fn test_set_admin_routes_to_transfer() { - let (env, client, admin1) = setup_with_admin(); - let admin2 = Address::generate(&env); - - let result = client.try_set_admin(&admin2); - - assert!(result.is_ok(), "set_admin must route to transfer"); - assert_eq!(client.get_current_admin(), Some(admin2)); - } - - // ============================================================================ - // 8. Edge Cases and Error Conditions - // ============================================================================ - - #[test] - fn test_multiple_rapid_transfers() { - let (env, client, mut current_admin) = setup_with_admin(); - - // Perform multiple transfers in sequence - for i in 0..5 { - let new_admin = Address::generate(&env); - client.transfer_admin(¤t_admin, &new_admin); - - assert_eq!( - client.get_current_admin(), - Some(new_admin.clone()), - "Transfer {} must succeed", - i - ); - current_admin = new_admin; - } - } - - #[test] - fn test_admin_state_after_failed_operations() { - let (env, client, admin) = setup_with_admin(); - let non_admin = Address::generate(&env); - - // Failed transfer should not change state - let _result = client.try_transfer_admin(&non_admin, &admin); - assert_eq!( - client.get_current_admin(), - Some(admin), - "Failed transfer must not change admin" - ); - - // Failed initialization should not change state - let _result = client.try_initialize_admin(&non_admin); - assert_eq!( - client.get_current_admin(), - Some(admin), - "Failed re-initialization must not change admin" - ); - } - - #[test] - fn test_event_emission_consistency() { - let (env, client) = setup(); - env.mock_all_auths(); - - let admin1 = Address::generate(&env); - let admin2 = Address::generate(&env); - - // Initialize and transfer - client.initialize_admin(&admin1); - client.transfer_admin(&admin1, &admin2); - - let events = env.events().all(); - - // Should have initialization and transfer events - let init_events: Vec<_> = events - .iter() - .filter(|e| e.0 == (soroban_sdk::symbol_short!("adm_init"),)) - .collect(); - let transfer_events: Vec<_> = events - .iter() - .filter(|e| e.0 == (soroban_sdk::symbol_short!("adm_trf"),)) - .collect(); - - assert_eq!(init_events.len(), 1, "Must have one init event"); - assert_eq!(transfer_events.len(), 1, "Must have one transfer event"); - } - - // ============================================================================ - // 9. Integration Tests - // ============================================================================ - - #[test] - fn test_full_admin_lifecycle() { - let (env, client) = setup(); - env.mock_all_auths(); - - // 1. Initial state - assert!(!AdminStorage::is_initialized(&env)); - assert_eq!(AdminStorage::get_admin(&env), None); - - // 2. Initialize admin - let admin1 = Address::generate(&env); - client.initialize_admin(&admin1); - assert!(AdminStorage::is_initialized(&env)); - assert_eq!(AdminStorage::get_admin(&env), Some(admin1.clone())); - - // 3. Transfer admin - let admin2 = Address::generate(&env); - client.transfer_admin(&admin1, &admin2); - assert_eq!(AdminStorage::get_admin(&env), Some(admin2.clone())); - - // 4. Verify old admin cannot operate - let admin3 = Address::generate(&env); - let result = client.try_transfer_admin(&admin1, &admin3); - assert!(result.is_err()); - - // 5. Verify new admin can operate - client.transfer_admin(&admin2, &admin3); - assert_eq!(AdminStorage::get_admin(&env), Some(admin3)); - } -} +//! Comprehensive test suite for hardened admin role management. +//! +//! Test Coverage: +//! 1. Initialization — admin setup, double-init prevention, authorization +//! 2. Transfer — success path, authorization, validation, atomicity +//! 3. Query Functions — get_admin, is_admin, is_initialized +//! 4. Authorization — require_admin, require_current_admin +//! 5. Security — transfer locks, concurrent operations, edge cases +//! 6. Events — initialization, transfer audit trail +//! 7. Utilities — with_admin_auth, with_current_admin +//! 8. Legacy Compatibility — set_admin routing +//! +//! Target: 95%+ test coverage for admin.rs + +#[cfg(test)] +mod test_admin { + use crate::admin::AdminStorage; + use crate::errors::QuickLendXError; + use crate::{QuickLendXContract, QuickLendXContractClient}; + use soroban_sdk::{ + testutils::{Address as _, Events}, + Address, Env, + }; + + fn setup() -> (Env, QuickLendXContractClient<'static>) { + let env = Env::default(); + let contract_id = env.register(QuickLendXContract, ()); + let client = QuickLendXContractClient::new(&env, &contract_id); + (env, client) + } + + fn setup_with_admin() -> (Env, QuickLendXContractClient<'static>, Address) { + let (env, client) = setup(); + env.mock_all_auths(); + let admin = Address::generate(&env); + client.initialize_admin(&admin); + (env, client, admin) + } + + // ============================================================================ + // 1. Initialization Tests + // ============================================================================ + + #[test] + fn test_initialize_admin_succeeds() { + let (env, client) = setup(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let result = client.try_initialize_admin(&admin); + + assert!(result.is_ok(), "First initialization must succeed"); + assert_eq!( + client.get_current_admin(), + Some(admin.clone()), + "Stored admin must match initialized address" + ); + assert!( + AdminStorage::is_initialized(&env), + "Admin system must be marked as initialized" + ); + } + + #[test] + fn test_initialize_admin_requires_authorization() { + let env = Env::default(); + let contract_id = env.register(QuickLendXContract, ()); + let client = QuickLendXContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + + // Should panic without authorization + let result = std::panic::catch_unwind(|| { + client.initialize_admin(&admin); + }); + assert!(result.is_err(), "Initialization without auth must fail"); + } + + #[test] + fn test_initialize_admin_double_init_fails() { + let (env, client) = setup(); + env.mock_all_auths(); + + let admin1 = Address::generate(&env); + let admin2 = Address::generate(&env); + + // First initialization succeeds + client.initialize_admin(&admin1); + + // Second initialization fails + let result = client.try_initialize_admin(&admin2); + assert!(result.is_err(), "Double initialization must be rejected"); + + // Original admin remains + assert_eq!( + client.get_current_admin(), + Some(admin1), + "Original admin must remain after failed re-init" + ); + } + + #[test] + fn test_initialize_admin_same_address_twice_fails() { + let (env, client) = setup(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + client.initialize_admin(&admin); + + let result = client.try_initialize_admin(&admin); + assert!( + result.is_err(), + "Re-initializing with same address must fail" + ); + } + + #[test] + fn test_initialize_admin_emits_event() { + let (env, client) = setup(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + client.initialize_admin(&admin); + + let events = env.events().all(); + assert!(!events.is_empty(), "Initialization must emit event"); + + let event = &events[0]; + assert_eq!(event.0, (soroban_sdk::symbol_short!("adm_init"),)); + } + + // ============================================================================ + // 2. Admin Transfer Tests + // ============================================================================ + + #[test] + fn test_transfer_admin_succeeds() { + let (env, client, admin1) = setup_with_admin(); + let admin2 = Address::generate(&env); + + let result = client.try_transfer_admin(&admin1, &admin2); + assert!(result.is_ok(), "Admin transfer must succeed"); + + assert_eq!( + client.get_current_admin(), + Some(admin2), + "New admin must be stored" + ); + assert!( + !AdminStorage::is_admin(&env, &admin1), + "Old admin must no longer be admin" + ); + assert!( + AdminStorage::is_admin(&env, &admin2), + "New admin must be recognized" + ); + } + + #[test] + fn test_transfer_admin_requires_current_admin_auth() { + let (env, client, admin1) = setup_with_admin(); + let admin2 = Address::generate(&env); + let non_admin = Address::generate(&env); + + // Non-admin cannot transfer + let result = client.try_transfer_admin(&non_admin, &admin2); + assert!(result.is_err(), "Non-admin transfer must fail"); + + // Admin remains unchanged + assert_eq!( + client.get_current_admin(), + Some(admin1), + "Admin must remain unchanged after failed transfer" + ); + } + + #[test] + fn test_transfer_admin_to_self_fails() { + let (env, client, admin) = setup_with_admin(); + + let result = client.try_transfer_admin(&admin, &admin); + assert!(result.is_err(), "Transfer to self must fail"); + + assert_eq!( + client.get_current_admin(), + Some(admin), + "Admin must remain unchanged" + ); + } + + #[test] + fn test_transfer_admin_without_initialization_fails() { + let (env, client) = setup(); + env.mock_all_auths(); + + let admin1 = Address::generate(&env); + let admin2 = Address::generate(&env); + + let result = client.try_transfer_admin(&admin1, &admin2); + assert!(result.is_err(), "Transfer without initialization must fail"); + } + + #[test] + fn test_transfer_admin_emits_event() { + let (env, client, admin1) = setup_with_admin(); + let admin2 = Address::generate(&env); + + client.transfer_admin(&admin1, &admin2); + + let events = env.events().all(); + let transfer_events: Vec<_> = events + .iter() + .filter(|e| e.0 == (soroban_sdk::symbol_short!("adm_trf"),)) + .collect(); + + assert!(!transfer_events.is_empty(), "Transfer must emit event"); + } + + #[test] + fn test_transfer_admin_chain() { + let (env, client, admin1) = setup_with_admin(); + let admin2 = Address::generate(&env); + let admin3 = Address::generate(&env); + + // Transfer from admin1 to admin2 + client.transfer_admin(&admin1, &admin2); + assert_eq!(client.get_current_admin(), Some(admin2.clone())); + + // Transfer from admin2 to admin3 + client.transfer_admin(&admin2, &admin3); + assert_eq!(client.get_current_admin(), Some(admin3)); + + // admin1 can no longer transfer + let result = client.try_transfer_admin(&admin1, &admin2); + assert!(result.is_err(), "Old admin cannot transfer"); + } + + // ============================================================================ + // 3. Query Function Tests + // ============================================================================ + + #[test] + fn test_get_admin_before_initialization() { + let (env, client) = setup(); + + assert_eq!( + client.get_current_admin(), + None, + "Admin must be None before initialization" + ); + assert!( + !AdminStorage::is_initialized(&env), + "System must not be initialized" + ); + } + + #[test] + fn test_get_admin_after_initialization() { + let (env, client, admin) = setup_with_admin(); + + assert_eq!( + client.get_current_admin(), + Some(admin.clone()), + "Admin must be returned after initialization" + ); + assert!( + AdminStorage::is_initialized(&env), + "System must be initialized" + ); + } + + #[test] + fn test_is_admin_checks() { + let (env, client, admin) = setup_with_admin(); + let non_admin = Address::generate(&env); + + assert!( + AdminStorage::is_admin(&env, &admin), + "Admin address must return true" + ); + assert!( + !AdminStorage::is_admin(&env, &non_admin), + "Non-admin address must return false" + ); + } + + #[test] + fn test_is_admin_before_initialization() { + let (env, _client) = setup(); + let address = Address::generate(&env); + + assert!( + !AdminStorage::is_admin(&env, &address), + "No address should be admin before initialization" + ); + } + + // ============================================================================ + // 4. Authorization Tests + // ============================================================================ + + #[test] + fn test_require_admin_succeeds_for_admin() { + let (env, _client, admin) = setup_with_admin(); + + let result = AdminStorage::require_admin(&env, &admin); + assert!(result.is_ok(), "require_admin must succeed for admin"); + } + + #[test] + fn test_require_admin_fails_for_non_admin() { + let (env, _client, _admin) = setup_with_admin(); + let non_admin = Address::generate(&env); + + let result = AdminStorage::require_admin(&env, &non_admin); + assert_eq!( + result, + Err(QuickLendXError::NotAdmin), + "require_admin must fail for non-admin" + ); + } + + #[test] + fn test_require_admin_fails_before_initialization() { + let (env, _client) = setup(); + let address = Address::generate(&env); + + let result = AdminStorage::require_admin(&env, &address); + assert_eq!( + result, + Err(QuickLendXError::OperationNotAllowed), + "require_admin must fail before initialization" + ); + } + + #[test] + fn test_require_current_admin_succeeds() { + let (env, _client, admin) = setup_with_admin(); + + let result = AdminStorage::require_current_admin(&env); + assert!(result.is_ok(), "require_current_admin must succeed"); + assert_eq!(result.unwrap(), admin, "Must return correct admin address"); + } + + #[test] + fn test_require_current_admin_fails_before_initialization() { + let (env, _client) = setup(); + + let result = AdminStorage::require_current_admin(&env); + assert_eq!( + result, + Err(QuickLendXError::OperationNotAllowed), + "require_current_admin must fail before initialization" + ); + } + + // ============================================================================ + // 5. Security Tests + // ============================================================================ + + #[test] + fn test_admin_operations_atomic() { + let (env, client, admin1) = setup_with_admin(); + let admin2 = Address::generate(&env); + + // Verify atomicity by checking state before and after + assert!(AdminStorage::is_admin(&env, &admin1)); + assert!(!AdminStorage::is_admin(&env, &admin2)); + + client.transfer_admin(&admin1, &admin2); + + // State should be completely switched + assert!(!AdminStorage::is_admin(&env, &admin1)); + assert!(AdminStorage::is_admin(&env, &admin2)); + } + + #[test] + fn test_initialization_state_consistency() { + let (env, client) = setup(); + env.mock_all_auths(); + + // Before initialization + assert!(!AdminStorage::is_initialized(&env)); + assert_eq!(AdminStorage::get_admin(&env), None); + + let admin = Address::generate(&env); + client.initialize_admin(&admin); + + // After initialization + assert!(AdminStorage::is_initialized(&env)); + assert_eq!(AdminStorage::get_admin(&env), Some(admin)); + } + + // ============================================================================ + // 6. Utility Function Tests + // ============================================================================ + + #[test] + fn test_with_admin_auth_succeeds() { + let (env, _client, admin) = setup_with_admin(); + + let result = AdminStorage::with_admin_auth(&env, &admin, || Ok("success".to_string())); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "success"); + } + + #[test] + fn test_with_admin_auth_fails_for_non_admin() { + let (env, _client, _admin) = setup_with_admin(); + let non_admin = Address::generate(&env); + + let result = AdminStorage::with_admin_auth(&env, &non_admin, || Ok("should not execute")); + + assert_eq!(result, Err(QuickLendXError::NotAdmin)); + } + + #[test] + fn test_with_current_admin_succeeds() { + let (env, _client, admin) = setup_with_admin(); + + let result = AdminStorage::with_current_admin(&env, |current_admin| { + assert_eq!(current_admin, &admin); + Ok("success") + }); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "success"); + } + + #[test] + fn test_with_current_admin_fails_before_initialization() { + let (env, _client) = setup(); + + let result = AdminStorage::with_current_admin(&env, |_| Ok("should not execute")); + + assert_eq!(result, Err(QuickLendXError::OperationNotAllowed)); + } + + // ============================================================================ + // 7. Legacy Compatibility Tests + // ============================================================================ + + #[test] + fn test_set_admin_routes_to_initialize() { + let (env, client) = setup(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let result = client.try_set_admin(&admin); + + assert!(result.is_ok(), "set_admin must route to initialize"); + assert_eq!(client.get_current_admin(), Some(admin)); + assert!(AdminStorage::is_initialized(&env)); + } + + #[test] + fn test_set_admin_routes_to_transfer() { + let (env, client, admin1) = setup_with_admin(); + let admin2 = Address::generate(&env); + + let result = client.try_set_admin(&admin2); + + assert!(result.is_ok(), "set_admin must route to transfer"); + assert_eq!(client.get_current_admin(), Some(admin2)); + } + + // ============================================================================ + // 8. Edge Cases and Error Conditions + // ============================================================================ + + #[test] + fn test_multiple_rapid_transfers() { + let (env, client, mut current_admin) = setup_with_admin(); + + // Perform multiple transfers in sequence + for i in 0..5 { + let new_admin = Address::generate(&env); + client.transfer_admin(¤t_admin, &new_admin); + + assert_eq!( + client.get_current_admin(), + Some(new_admin.clone()), + "Transfer {} must succeed", + i + ); + current_admin = new_admin; + } + } + + #[test] + fn test_admin_state_after_failed_operations() { + let (env, client, admin) = setup_with_admin(); + let non_admin = Address::generate(&env); + + // Failed transfer should not change state + let _result = client.try_transfer_admin(&non_admin, &admin); + assert_eq!( + client.get_current_admin(), + Some(admin), + "Failed transfer must not change admin" + ); + + // Failed initialization should not change state + let _result = client.try_initialize_admin(&non_admin); + assert_eq!( + client.get_current_admin(), + Some(admin), + "Failed re-initialization must not change admin" + ); + } + + #[test] + fn test_event_emission_consistency() { + let (env, client) = setup(); + env.mock_all_auths(); + + let admin1 = Address::generate(&env); + let admin2 = Address::generate(&env); + + // Initialize and transfer + client.initialize_admin(&admin1); + client.transfer_admin(&admin1, &admin2); + + let events = env.events().all(); + + // Should have initialization and transfer events + let init_events: Vec<_> = events + .iter() + .filter(|e| e.0 == (soroban_sdk::symbol_short!("adm_init"),)) + .collect(); + let transfer_events: Vec<_> = events + .iter() + .filter(|e| e.0 == (soroban_sdk::symbol_short!("adm_trf"),)) + .collect(); + + assert_eq!(init_events.len(), 1, "Must have one init event"); + assert_eq!(transfer_events.len(), 1, "Must have one transfer event"); + } + + // ============================================================================ + // 9. Integration Tests + // ============================================================================ + + #[test] + fn test_full_admin_lifecycle() { + let (env, client) = setup(); + env.mock_all_auths(); + + // 1. Initial state + assert!(!AdminStorage::is_initialized(&env)); + assert_eq!(AdminStorage::get_admin(&env), None); + + // 2. Initialize admin + let admin1 = Address::generate(&env); + client.initialize_admin(&admin1); + assert!(AdminStorage::is_initialized(&env)); + assert_eq!(AdminStorage::get_admin(&env), Some(admin1.clone())); + + // 3. Transfer admin + let admin2 = Address::generate(&env); + client.transfer_admin(&admin1, &admin2); + assert_eq!(AdminStorage::get_admin(&env), Some(admin2.clone())); + + // 4. Verify old admin cannot operate + let admin3 = Address::generate(&env); + let result = client.try_transfer_admin(&admin1, &admin3); + assert!(result.is_err()); + + // 5. Verify new admin can operate + client.transfer_admin(&admin2, &admin3); + assert_eq!(AdminStorage::get_admin(&env), Some(admin3)); + } +} diff --git a/quicklendx-contracts/src/test_bid.rs b/quicklendx-contracts/src/test_bid.rs index 9c6ff38a..e69de29b 100644 --- a/quicklendx-contracts/src/test_bid.rs +++ b/quicklendx-contracts/src/test_bid.rs @@ -1,2060 +0,0 @@ -/// Minimized test suite for bid functionality -/// Coverage: placement/withdrawal, invoice status gating, indexing/query correctness -/// -/// Test Categories (Core Only): -/// 1. Status Gating - verify bids only work on verified invoices -/// 2. Withdrawal - authorize only bid owner can withdraw -/// 3. Indexing - multiple bids properly indexed and queryable -/// 4. Ranking - profit-based bid comparison works correctly -use super::*; -use crate::bid::{BidStatus, BidStorage}; -use crate::errors::QuickLendXError; -use crate::invoice::InvoiceCategory; -use crate::payments::EscrowStatus; -use crate::protocol_limits::compute_min_bid_amount; -use soroban_sdk::{ - testutils::{Address as _, Ledger}, - Address, BytesN, Env, String, Vec, -}; - -fn setup() -> (Env, QuickLendXContractClient<'static>, Address) { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(QuickLendXContract, ()); - let client = QuickLendXContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); - client.set_admin(&admin); - (env, client, admin) -} - -// Helper: Create verified investor - using same pattern as test.rs -fn add_verified_investor(env: &Env, client: &QuickLendXContractClient, limit: i128) -> Address { - let investor = Address::generate(env); - client.submit_investor_kyc(&investor, &String::from_str(env, "KYC")); - client.verify_investor(&investor, &limit); - investor -} - -// Helper: Create verified invoice -fn create_verified_invoice( - env: &Env, - client: &QuickLendXContractClient, - admin: &Address, - business: &Address, - amount: i128, -) -> BytesN<32> { - let currency = Address::generate(env); - let due_date = env.ledger().timestamp() + 86400; - - let invoice_id = client.store_invoice( - admin, - business, - &amount, - ¤cy, - &due_date, - &String::from_str(env, "Invoice"), - &InvoiceCategory::Services, - &Vec::new(env), - ); - - let _ = client.try_verify_invoice(&invoice_id); - invoice_id -} - -fn assert_contract_error( - result: Result>, - expected: QuickLendXError, -) { - let err = result.expect_err("expected contract call to fail"); - let contract_err = err.expect("expected contract error"); - assert_eq!(contract_err, expected); -} - -// ============================================================================ -// Category 1: Status Gating - Invoice Verification Required -// ============================================================================ - -/// Core Test: Bid on pending (non-verified) invoice fails -#[test] -fn test_bid_placement_non_verified_invoice_fails() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - let currency = Address::generate(&env); - - // Create pending invoice (not verified) - let invoice_id = client.store_invoice( - &admin, - &business, - &10_000, - ¤cy, - &(env.ledger().timestamp() + 86400), - &String::from_str(&env, "Pending"), - &InvoiceCategory::Services, - &Vec::new(&env), - ); - - // Attempt bid on pending invoice should fail - let result = client.try_place_bid(&investor, &invoice_id, &5_000, &6_000); - assert!(result.is_err(), "Bid on pending invoice must fail"); -} - -/// Core Test: Bid on verified invoice succeeds -#[test] -fn test_bid_placement_verified_invoice_succeeds() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 10_000); - - // Bid on verified invoice should succeed - let result = client.try_place_bid(&investor, &invoice_id, &5_000, &6_000); - assert!(result.is_ok(), "Bid on verified invoice must succeed"); - - let bid_id = result.unwrap().unwrap(); - let bid = client.get_bid(&bid_id); - assert!(bid.is_some()); - assert_eq!(bid.unwrap().status, BidStatus::Placed); -} - -/// Core Test: Minimum bid amount enforced (absolute floor + percentage of invoice) -#[test] -fn test_bid_minimum_amount_enforced() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 1_000_000); - let business = Address::generate(&env); - - let invoice_amount = 200_000; - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, invoice_amount); - - let min_bid = compute_min_bid_amount( - invoice_amount, - &crate::protocol_limits::ProtocolLimits { - min_invoice_amount: 1_000_000, - min_bid_amount: 100, - min_bid_bps: 100, - max_due_date_days: 365, - grace_period_seconds: 86400, - max_invoices_per_business: 100, - }, - ); - let below_min = min_bid.saturating_sub(1); - - let result = client.try_place_bid(&investor, &invoice_id, &below_min, &(min_bid + 100)); - assert!(result.is_err(), "Bid below minimum must fail"); - - let result = client.try_place_bid(&investor, &invoice_id, &min_bid, &(min_bid + 100)); - assert!(result.is_ok(), "Bid at minimum must succeed"); -} - -/// Core Test: Investment limit enforced -#[test] -fn test_bid_placement_respects_investment_limit() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 1_000); // Low limit - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 10_000); - - // Bid exceeding limit should fail - let result = client.try_place_bid(&investor, &invoice_id, &2_000, &3_000); - assert!(result.is_err(), "Bid exceeding investment limit must fail"); -} - -// ============================================================================ -// Category 2: Withdrawal - Authorization and State Constraints -// ============================================================================ - -/// Core Test: Bid owner can withdraw own bid -#[test] -fn test_bid_withdrawal_by_owner_succeeds() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 10_000); - - // Place bid - let bid_id = client.place_bid(&investor, &invoice_id, &5_000, &6_000); - - // Withdraw should succeed - let result = client.try_withdraw_bid(&bid_id); - assert!(result.is_ok(), "Owner bid withdrawal must succeed"); - - // Verify withdrawn - let bid = client.get_bid(&bid_id); - assert!(bid.is_some()); - assert_eq!(bid.unwrap().status, BidStatus::Withdrawn); -} - -/// Core Test: Only Placed bids can be withdrawn -#[test] -fn test_bid_withdrawal_only_placed_bids() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 10_000); - - let bid_id = client.place_bid(&investor, &invoice_id, &5_000, &6_000); - - // Withdraw once - let _ = client.try_withdraw_bid(&bid_id); - - // Second withdraw attempt should fail - let result = client.try_withdraw_bid(&bid_id); - assert_contract_error(result, QuickLendXError::OperationNotAllowed); -} - -/// Core Test: Accepted bids cannot be withdrawn -#[test] -fn test_bid_withdrawal_rejects_accepted_bid() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 10_000); - let bid_id = client.place_bid(&investor, &invoice_id, &5_000, &6_000); - - client.accept_bid(&invoice_id, &bid_id); - - let result = client.try_withdraw_bid(&bid_id); - assert_contract_error(result, QuickLendXError::OperationNotAllowed); - - let bid = client.get_bid(&bid_id).unwrap(); - assert_eq!(bid.status, BidStatus::Accepted); -} - -/// Core Test: Expired bids are refreshed then rejected during withdrawal -#[test] -fn test_bid_withdrawal_rejects_expired_bid_without_manual_cleanup() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 10_000); - let bid_id = client.place_bid(&investor, &invoice_id, &5_000, &6_000); - - let expiration = client.get_bid(&bid_id).unwrap().expiration_timestamp; - env.ledger().set_timestamp(expiration + 1); - - let result = client.try_withdraw_bid(&bid_id); - assert_contract_error(result, QuickLendXError::OperationNotAllowed); - - let bid = client.get_bid(&bid_id).unwrap(); - assert_eq!(bid.status, BidStatus::Expired); - - let expired_bids = client.get_bids_by_status(&invoice_id, &BidStatus::Expired); - assert_eq!(expired_bids.len(), 1, "Expired bid should be queryable by status"); -} - -// ============================================================================ -// Category 3: Indexing & Query Correctness - Multiple Bids -// ============================================================================ - -/// Core Test: Multiple bids indexed and queryable by status -#[test] -fn test_multiple_bids_indexing_and_query() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let investor3 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place 3 bids - let bid_id_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - let bid_id_2 = client.place_bid(&investor2, &invoice_id, &15_000, &18_000); - let bid_id_3 = client.place_bid(&investor3, &invoice_id, &20_000, &24_000); - - // Query placed bids - let placed_bids = client.get_bids_by_status(&invoice_id, &BidStatus::Placed); - assert_eq!(placed_bids.len(), 3, "Should have 3 placed bids"); - - // Verify all bid IDs present - let found_1 = placed_bids.iter().any(|b| b.bid_id == bid_id_1); - let found_2 = placed_bids.iter().any(|b| b.bid_id == bid_id_2); - let found_3 = placed_bids.iter().any(|b| b.bid_id == bid_id_3); - assert!(found_1 && found_2 && found_3, "All bid IDs must be indexed"); - - // Withdraw one and verify status filtering - let _ = client.try_withdraw_bid(&bid_id_1); - let placed_after = client.get_bids_by_status(&invoice_id, &BidStatus::Placed); - assert_eq!( - placed_after.len(), - 2, - "Should have 2 placed bids after withdrawal" - ); - - // ============================================================================ - // Bid TTL configuration tests - // ============================================================================ - - #[test] - fn test_default_bid_ttl_used_in_place_bid() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 10_000); - - let current_ts = env.ledger().timestamp(); - let bid_id = client.place_bid(&investor, &invoice_id, &5_000, &6_000); - let bid = client.get_bid(&bid_id).unwrap(); - - let expected = current_ts + (7u64 * 86400u64); - assert_eq!(bid.expiration_timestamp, expected); - } - - #[test] - fn test_admin_can_update_ttl_and_bid_uses_new_value() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - - // Update TTL to 14 days - let _ = client.set_bid_ttl_days(&14u64); - - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 10_000); - - let current_ts = env.ledger().timestamp(); - let bid_id = client.place_bid(&investor, &invoice_id, &5_000, &6_000); - let bid = client.get_bid(&bid_id).unwrap(); - - let expected = current_ts + (14u64 * 86400u64); - assert_eq!(bid.expiration_timestamp, expected); - } - - #[test] - fn test_set_bid_ttl_bounds_enforced() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - - // Too small - let result = client.try_set_bid_ttl_days(&0u64); - assert!(result.is_err()); - - // Too large - let result = client.try_set_bid_ttl_days(&31u64); - assert!(result.is_err()); - } - - let withdrawn_bids = client.get_bids_by_status(&invoice_id, &BidStatus::Withdrawn); - assert_eq!(withdrawn_bids.len(), 1, "Should have 1 withdrawn bid"); -} - -/// Core Test: Query by investor works correctly -#[test] -fn test_query_bids_by_investor() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id_1 = create_verified_invoice(&env, &client, &admin, &business, 100_000); - let invoice_id_2 = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Investor1 places 2 bids on different invoices - let _bid_1a = client.place_bid(&investor1, &invoice_id_1, &10_000, &12_000); - let _bid_1b = client.place_bid(&investor1, &invoice_id_2, &15_000, &18_000); - - // Investor2 places 1 bid - let _bid_2 = client.place_bid(&investor2, &invoice_id_1, &20_000, &24_000); - - // Query investor1 bids on invoice 1 - let inv1_bids = client.get_bids_by_investor(&invoice_id_1, &investor1); - assert_eq!( - inv1_bids.len(), - 1, - "Investor1 should have 1 bid on invoice 1" - ); - - // Query investor2 bids on invoice 1 - let inv2_bids = client.get_bids_by_investor(&invoice_id_1, &investor2); - assert_eq!( - inv2_bids.len(), - 1, - "Investor2 should have 1 bid on invoice 1" - ); -} - -// ============================================================================ -// Category 4: Bid Ranking - Profit-Based Comparison Logic -// ============================================================================ - -/// Core Test: Best bid selection based on profit margin -#[test] -fn test_bid_ranking_by_profit() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let investor3 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place bids with different profit margins - // investor1: profit = 12k - 10k = 2k - let _bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - - // investor2: profit = 18k - 15k = 3k (highest) - let _bid_2 = client.place_bid(&investor2, &invoice_id, &15_000, &18_000); - - // investor3: profit = 13k - 12k = 1k (lowest) - let _bid_3 = client.place_bid(&investor3, &invoice_id, &12_000, &13_000); - - // Best bid should be investor2 (highest profit) - let best_bid = client.get_best_bid(&invoice_id); - assert!(best_bid.is_some()); - assert_eq!( - best_bid.unwrap().investor, - investor2, - "Best bid must have highest profit" - ); - - // Ranked bids should order by profit descending - let ranked = client.get_ranked_bids(&invoice_id); - assert_eq!(ranked.len(), 3, "Should have 3 ranked bids"); - assert_eq!( - ranked.get(0).unwrap().investor, - investor2, - "Rank 1: investor2 (profit 3k)" - ); - assert_eq!( - ranked.get(1).unwrap().investor, - investor1, - "Rank 2: investor1 (profit 2k)" - ); - assert_eq!( - ranked.get(2).unwrap().investor, - investor3, - "Rank 3: investor3 (profit 1k)" - ); -} - -/// Core Test: Best bid ignores withdrawn bids -#[test] -fn test_best_bid_excludes_withdrawn() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // investor1: profit = 2k - let _bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - - // investor2: profit = 10k (best initially) - let bid_2 = client.place_bid(&investor2, &invoice_id, &15_000, &25_000); - - // Withdraw best bid - let _ = client.try_withdraw_bid(&bid_2); - - // Best bid should now be investor1 - let best = client.get_best_bid(&invoice_id); - assert!(best.is_some()); - assert_eq!( - best.unwrap().investor, - investor1, - "Best bid must skip withdrawn bids" - ); -} - -/// Core Test: Bid expiration cleanup -#[test] -fn test_bid_expiration_and_cleanup() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 10_000); - - // Place bid - let bid_id = client.place_bid(&investor, &invoice_id, &5_000, &6_000); - - let placed = client.get_bids_by_status(&invoice_id, &BidStatus::Placed); - assert_eq!(placed.len(), 1, "Should have 1 placed bid"); - - // Advance time past expiration (7 days = 604800 seconds) - env.ledger() - .set_timestamp(env.ledger().timestamp() + 604800 + 1); - - // Query to trigger cleanup - let placed_after = client.get_bids_by_status(&invoice_id, &BidStatus::Placed); - assert_eq!( - placed_after.len(), - 0, - "Placed bids should be empty after expiration" - ); - - // Bid should be marked expired - let bid = client.get_bid(&bid_id); - assert!(bid.is_some()); - assert_eq!( - bid.unwrap().status, - BidStatus::Expired, - "Bid must be marked expired" - ); -} - -// ============================================================================ -// Category 6: Bid Expiration - Default TTL and Cleanup -// ============================================================================ - -/// Test: Bid uses default TTL (7 days) when placed -#[test] -fn test_bid_default_ttl_seven_days() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 10_000); - - let initial_timestamp = env.ledger().timestamp(); - let bid_id = client.place_bid(&investor, &invoice_id, &5_000, &6_000); - - let bid = client.get_bid(&bid_id).unwrap(); - let expected_expiration = initial_timestamp + (7 * 24 * 60 * 60); // 7 days in seconds - - assert_eq!( - bid.expiration_timestamp, expected_expiration, - "Bid expiration should be 7 days from placement" - ); -} - -/// Test: cleanup_expired_bids returns count of removed bids -#[test] -fn test_cleanup_expired_bids_returns_count() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let investor3 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place 3 bids - let bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - let bid_2 = client.place_bid(&investor2, &invoice_id, &15_000, &18_000); - let bid_3 = client.place_bid(&investor3, &invoice_id, &20_000, &24_000); - - // Advance time past expiration - env.ledger() - .set_timestamp(env.ledger().timestamp() + 604800 + 1); - - // Cleanup should return count of 3 - let removed_count = client.cleanup_expired_bids(&invoice_id); - assert_eq!(removed_count, 3, "Should remove all 3 expired bids"); - - // Verify all bids are marked expired (check individual bid records) - let bid_1_status = client.get_bid(&bid_1).unwrap(); - assert_eq!( - bid_1_status.status, - BidStatus::Expired, - "Bid 1 should be expired" - ); - - let bid_2_status = client.get_bid(&bid_2).unwrap(); - assert_eq!( - bid_2_status.status, - BidStatus::Expired, - "Bid 2 should be expired" - ); - - let bid_3_status = client.get_bid(&bid_3).unwrap(); - assert_eq!( - bid_3_status.status, - BidStatus::Expired, - "Bid 3 should be expired" - ); - - // Verify no bids are in Placed status - let placed_bids = client.get_bids_by_status(&invoice_id, &BidStatus::Placed); - assert_eq!(placed_bids.len(), 0, "No bids should be in Placed status"); -} - -/// Test: cleanup_expired_bids is idempotent when called multiple times -#[test] -fn test_cleanup_expired_bids_idempotent() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place 2 bids that will both expire - let bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - let bid_2 = client.place_bid(&investor2, &invoice_id, &15_000, &18_000); - - // Advance time past expiration - env.ledger() - .set_timestamp(env.ledger().timestamp() + 604800 + 1); - - // First cleanup should expire both bids and return count 2 - let removed_first = client.cleanup_expired_bids(&invoice_id); - assert_eq!( - removed_first, 2, - "First cleanup should remove 2 expired bids" - ); - - // Verify both bids are marked Expired and removed from invoice list - assert_eq!( - client.get_bid(&bid_1).unwrap().status, - BidStatus::Expired, - "Bid 1 should be expired after first cleanup" - ); - assert_eq!( - client.get_bid(&bid_2).unwrap().status, - BidStatus::Expired, - "Bid 2 should be expired after first cleanup" - ); - let bids_after_first = client.get_bids_for_invoice(&invoice_id); - assert_eq!( - bids_after_first.len(), - 0, - "Invoice bid list should be empty after first cleanup" - ); - - // Second cleanup should be a no-op and return 0, with state unchanged - let removed_second = client.cleanup_expired_bids(&invoice_id); - assert_eq!( - removed_second, 0, - "Second cleanup should be idempotent and remove 0 bids" - ); - assert_eq!( - client.get_bid(&bid_1).unwrap().status, - BidStatus::Expired, - "Bid 1 should remain expired after second cleanup" - ); - assert_eq!( - client.get_bid(&bid_2).unwrap().status, - BidStatus::Expired, - "Bid 2 should remain expired after second cleanup" - ); - let bids_after_second = client.get_bids_for_invoice(&invoice_id); - assert_eq!( - bids_after_second.len(), - 0, - "Invoice bid list should remain empty after second cleanup" - ); -} - -/// Test: get_ranked_bids excludes expired bids -#[test] -fn test_get_ranked_bids_excludes_expired() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let investor3 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place 3 bids with different profits - // investor1: profit = 2k - let _bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - // investor2: profit = 3k (best) - let _bid_2 = client.place_bid(&investor2, &invoice_id, &15_000, &18_000); - // investor3: profit = 1k - let _bid_3 = client.place_bid(&investor3, &invoice_id, &12_000, &13_000); - - // Verify all 3 bids are ranked - let ranked_before = client.get_ranked_bids(&invoice_id); - assert_eq!( - ranked_before.len(), - 3, - "Should have 3 ranked bids initially" - ); - - // Advance time past expiration - env.ledger() - .set_timestamp(env.ledger().timestamp() + 604800 + 1); - - // get_ranked_bids should trigger cleanup and exclude expired bids - let ranked_after = client.get_ranked_bids(&invoice_id); - assert_eq!( - ranked_after.len(), - 0, - "Ranked bids should be empty after expiration" - ); -} - -/// Test: get_best_bid excludes expired bids -#[test] -fn test_get_best_bid_excludes_expired() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // investor1: profit = 2k - let _bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - // investor2: profit = 10k (best) - let _bid_2 = client.place_bid(&investor2, &invoice_id, &15_000, &25_000); - - // Verify best bid is investor2 - let best_before = client.get_best_bid(&invoice_id); - assert!(best_before.is_some()); - assert_eq!( - best_before.unwrap().investor, - investor2, - "Best bid should be investor2" - ); - - // Advance time past expiration - env.ledger() - .set_timestamp(env.ledger().timestamp() + 604800 + 1); - - // get_best_bid should return None after all bids expire - let best_after = client.get_best_bid(&invoice_id); - assert!( - best_after.is_none(), - "Best bid should be None after all bids expire" - ); -} - -/// Test: place_bid cleans up expired bids before placing new bid -#[test] -fn test_place_bid_cleans_up_expired_before_placing() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place initial bid - let bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - - // Verify bid is placed - let placed_before = client.get_bids_by_status(&invoice_id, &BidStatus::Placed); - assert_eq!(placed_before.len(), 1, "Should have 1 placed bid"); - - // Advance time past expiration - env.ledger() - .set_timestamp(env.ledger().timestamp() + 604800 + 1); - - // Place new bid - should trigger cleanup of expired bid - let _bid_2 = client.place_bid(&investor2, &invoice_id, &15_000, &18_000); - - // Verify old bid is expired and new bid is placed - let placed_after = client.get_bids_by_status(&invoice_id, &BidStatus::Placed); - assert_eq!( - placed_after.len(), - 1, - "Should have only 1 placed bid (new one)" - ); - - // Verify the expired bid is marked as expired (check individual record) - let bid_1_status = client.get_bid(&bid_1).unwrap(); - assert_eq!( - bid_1_status.status, - BidStatus::Expired, - "First bid should be expired" - ); -} - -/// Test: Partial expiration - only expired bids are cleaned up -#[test] -fn test_partial_expiration_cleanup() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let investor3 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place first bid - let bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - - // Advance time by 3 days (not expired yet) - env.ledger() - .set_timestamp(env.ledger().timestamp() + (3 * 24 * 60 * 60)); - - // Place second bid - let bid_2 = client.place_bid(&investor2, &invoice_id, &15_000, &18_000); - - // Advance time by 5 more days (total 8 days - first bid expired, second not) - env.ledger() - .set_timestamp(env.ledger().timestamp() + (5 * 24 * 60 * 60)); - - // Place third bid - should clean up only first expired bid - let _bid_3 = client.place_bid(&investor3, &invoice_id, &20_000, &24_000); - - // Verify first bid is expired (check individual record) - let bid_1_status = client.get_bid(&bid_1).unwrap(); - assert_eq!( - bid_1_status.status, - BidStatus::Expired, - "First bid should be expired" - ); - - // Verify second and third bids are still placed - let bid_2_status = client.get_bid(&bid_2).unwrap(); - assert_eq!( - bid_2_status.status, - BidStatus::Placed, - "Second bid should still be placed" - ); - - let placed_bids = client.get_bids_by_status(&invoice_id, &BidStatus::Placed); - assert_eq!( - placed_bids.len(), - 2, - "Should have 2 placed bids (second and third)" - ); -} - -/// Test: Cleanup is triggered when querying bids after expiration -#[test] -fn test_cleanup_triggered_on_query_after_expiration() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place two bids at different times - let bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - - // Advance time by 1 day - env.ledger() - .set_timestamp(env.ledger().timestamp() + (1 * 24 * 60 * 60)); - - let _bid_2 = client.place_bid(&investor2, &invoice_id, &15_000, &18_000); - - // Advance time by 7 more days (first bid expired, second still valid) - env.ledger() - .set_timestamp(env.ledger().timestamp() + (7 * 24 * 60 * 60)); - - // Query bids - should trigger cleanup - let placed_bids = client.get_bids_by_status(&invoice_id, &BidStatus::Placed); - assert_eq!( - placed_bids.len(), - 1, - "Should have only 1 placed bid after cleanup" - ); - - // Verify first bid is expired (check individual record) - let bid_1_status = client.get_bid(&bid_1).unwrap(); - assert_eq!( - bid_1_status.status, - BidStatus::Expired, - "First bid should be expired" - ); -} - -/// Test: Cannot accept expired bid -#[test] -fn test_cannot_accept_expired_bid() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place bid - let bid_id = client.place_bid(&investor, &invoice_id, &10_000, &12_000); - - // Advance time past expiration - env.ledger() - .set_timestamp(env.ledger().timestamp() + 604800 + 1); - - // Try to accept expired bid - should fail (cleanup happens during accept_bid) - let result = client.try_accept_bid(&invoice_id, &bid_id); - assert!(result.is_err(), "Should not be able to accept expired bid"); -} - -/// Test: Bid at exact expiration boundary (not expired) -#[test] -fn test_bid_at_exact_expiration_not_expired() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place bid - let bid_id = client.place_bid(&investor, &invoice_id, &10_000, &12_000); - let bid = client.get_bid(&bid_id).unwrap(); - - // Set time to exactly expiration timestamp (not past it) - env.ledger().set_timestamp(bid.expiration_timestamp); - - // Bid should still be valid (not expired) - let placed_bids = client.get_bids_by_status(&invoice_id, &BidStatus::Placed); - assert_eq!( - placed_bids.len(), - 1, - "Bid at exact expiration should still be placed" - ); - - // Verify bid status is still Placed - let bid_status = client.get_bid(&bid_id).unwrap(); - assert_eq!( - bid_status.status, - BidStatus::Placed, - "Bid should still be placed at exact expiration" - ); -} - -/// Test: Bid one second past expiration (expired) -#[test] -fn test_bid_one_second_past_expiration_expired() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place bid - let bid_id = client.place_bid(&investor, &invoice_id, &10_000, &12_000); - let bid = client.get_bid(&bid_id).unwrap(); - - // Set time to one second past expiration - env.ledger().set_timestamp(bid.expiration_timestamp + 1); - - // Trigger cleanup - let removed = client.cleanup_expired_bids(&invoice_id); - assert_eq!(removed, 1, "Should remove 1 expired bid"); - - // Verify bid is expired - let bid_status = client.get_bid(&bid_id).unwrap(); - assert_eq!( - bid_status.status, - BidStatus::Expired, - "Bid should be expired one second past expiration" - ); -} - -/// Test: Cleanup with no expired bids returns zero -#[test] -fn test_cleanup_with_no_expired_bids_returns_zero() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place bid - let _bid_id = client.place_bid(&investor, &invoice_id, &10_000, &12_000); - - // Cleanup immediately (no expired bids) - let removed = client.cleanup_expired_bids(&invoice_id); - assert_eq!(removed, 0, "Should remove 0 bids when none are expired"); - - // Verify bid is still placed - let placed_bids = client.get_bids_by_status(&invoice_id, &BidStatus::Placed); - assert_eq!(placed_bids.len(), 1, "Bid should still be placed"); -} - -/// Test: Cleanup on invoice with no bids returns zero -#[test] -fn test_cleanup_on_invoice_with_no_bids() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Cleanup on invoice with no bids - let removed = client.cleanup_expired_bids(&invoice_id); - assert_eq!(removed, 0, "Should remove 0 bids when invoice has no bids"); -} - -/// Test: Withdrawn bids are not affected by expiration cleanup -#[test] -fn test_withdrawn_bids_not_affected_by_expiration() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place two bids - let bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - let bid_2 = client.place_bid(&investor2, &invoice_id, &15_000, &18_000); - - // Withdraw first bid - let _ = client.try_withdraw_bid(&bid_1); - - // Advance time past expiration - env.ledger() - .set_timestamp(env.ledger().timestamp() + 604800 + 1); - - // Cleanup should only affect placed bids - let removed = client.cleanup_expired_bids(&invoice_id); - assert_eq!(removed, 1, "Should remove only 1 placed bid"); - - // Verify first bid is still withdrawn (not expired) - check individual record - let bid_1_status = client.get_bid(&bid_1).unwrap(); - assert_eq!( - bid_1_status.status, - BidStatus::Withdrawn, - "Withdrawn bid should remain withdrawn" - ); - - // Verify second bid is expired - check individual record - let bid_2_status = client.get_bid(&bid_2).unwrap(); - assert_eq!( - bid_2_status.status, - BidStatus::Expired, - "Placed bid should be expired" - ); -} - -/// Test: Cancelled bids are not affected by expiration cleanup -#[test] -fn test_cancelled_bids_not_affected_by_expiration() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place two bids - let bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - let bid_2 = client.place_bid(&investor2, &invoice_id, &15_000, &18_000); - - // Cancel first bid - let _ = client.cancel_bid(&bid_1); - - // Advance time past expiration - env.ledger() - .set_timestamp(env.ledger().timestamp() + 604800 + 1); - - // Cleanup should only affect placed bids - let removed = client.cleanup_expired_bids(&invoice_id); - assert_eq!(removed, 1, "Should remove only 1 placed bid"); - - // Verify first bid is still cancelled (not expired) - check individual record - let bid_1_status = client.get_bid(&bid_1).unwrap(); - assert_eq!( - bid_1_status.status, - BidStatus::Cancelled, - "Cancelled bid should remain cancelled" - ); - - // Verify second bid is expired - check individual record - let bid_2_status = client.get_bid(&bid_2).unwrap(); - assert_eq!( - bid_2_status.status, - BidStatus::Expired, - "Placed bid should be expired" - ); -} - -/// Test: Mixed status bids - only Placed bids expire -#[test] -fn test_mixed_status_bids_only_placed_expire() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let investor3 = add_verified_investor(&env, &client, 100_000); - let investor4 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place four bids - let bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - let bid_2 = client.place_bid(&investor2, &invoice_id, &15_000, &18_000); - let bid_3 = client.place_bid(&investor3, &invoice_id, &20_000, &24_000); - let bid_4 = client.place_bid(&investor4, &invoice_id, &25_000, &30_000); - - // Withdraw bid 1 - let _ = client.try_withdraw_bid(&bid_1); - - // Cancel bid 2 - let _ = client.cancel_bid(&bid_2); - - // Leave bid 3 and 4 as Placed - - // Advance time past expiration - env.ledger() - .set_timestamp(env.ledger().timestamp() + 604800 + 1); - - // Cleanup should only affect placed bids (3 and 4) - let removed = client.cleanup_expired_bids(&invoice_id); - assert_eq!(removed, 2, "Should remove 2 placed bids"); - - // Verify statuses - assert_eq!(client.get_bid(&bid_1).unwrap().status, BidStatus::Withdrawn); - assert_eq!(client.get_bid(&bid_2).unwrap().status, BidStatus::Cancelled); - assert_eq!(client.get_bid(&bid_3).unwrap().status, BidStatus::Expired); - assert_eq!(client.get_bid(&bid_4).unwrap().status, BidStatus::Expired); -} - -/// Test: Expiration cleanup is isolated per invoice -#[test] -fn test_expiration_cleanup_isolated_per_invoice() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - // Create two invoices - let invoice_id_1 = create_verified_invoice(&env, &client, &admin, &business, 50_000); - let invoice_id_2 = create_verified_invoice(&env, &client, &admin, &business, 50_000); - - // Place bids on both invoices - let bid_1 = client.place_bid(&investor, &invoice_id_1, &10_000, &12_000); - let bid_2 = client.place_bid(&investor, &invoice_id_2, &15_000, &18_000); - - // Advance time past expiration - env.ledger() - .set_timestamp(env.ledger().timestamp() + 604800 + 1); - - // Cleanup only invoice 1 - let removed_1 = client.cleanup_expired_bids(&invoice_id_1); - assert_eq!(removed_1, 1, "Should remove 1 bid from invoice 1"); - - // Verify invoice 1 bid is expired - let bid_1_status = client.get_bid(&bid_1).unwrap(); - assert_eq!( - bid_1_status.status, - BidStatus::Expired, - "Invoice 1 bid should be expired" - ); - - // Verify invoice 2 bid is still placed (cleanup not triggered) - let bid_2_status = client.get_bid(&bid_2).unwrap(); - assert_eq!( - bid_2_status.status, - BidStatus::Placed, - "Invoice 2 bid should still be placed" - ); - - // Now cleanup invoice 2 - let removed_2 = client.cleanup_expired_bids(&invoice_id_2); - assert_eq!(removed_2, 1, "Should remove 1 bid from invoice 2"); - - // Verify invoice 2 bid is now expired - let bid_2_status_after = client.get_bid(&bid_2).unwrap(); - assert_eq!( - bid_2_status_after.status, - BidStatus::Expired, - "Invoice 2 bid should now be expired" - ); -} - -/// Test: Expired bids removed from invoice bid list -#[test] -fn test_expired_bids_removed_from_invoice_list() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place two bids - let _bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - let _bid_2 = client.place_bid(&investor2, &invoice_id, &15_000, &18_000); - - // Get bids for invoice before expiration - let bids_before = client.get_bids_for_invoice(&invoice_id); - assert_eq!(bids_before.len(), 2, "Should have 2 bids in invoice list"); - - // Advance time past expiration - env.ledger() - .set_timestamp(env.ledger().timestamp() + 604800 + 1); - - // Cleanup - let _ = client.cleanup_expired_bids(&invoice_id); - - // Get bids for invoice after expiration - should be empty - let bids_after = client.get_bids_for_invoice(&invoice_id); - assert_eq!( - bids_after.len(), - 0, - "Expired bids should be removed from invoice list" - ); -} - -/// Test: Ranking after expiration returns empty list -#[test] -fn test_ranking_after_all_bids_expire() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let investor3 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place three bids with different profits - let _bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - let _bid_2 = client.place_bid(&investor2, &invoice_id, &15_000, &20_000); - let _bid_3 = client.place_bid(&investor3, &invoice_id, &20_000, &24_000); - - // Verify ranking works before expiration - let ranked_before = client.get_ranked_bids(&invoice_id); - assert_eq!(ranked_before.len(), 3, "Should have 3 ranked bids"); - assert_eq!( - ranked_before.get(0).unwrap().investor, - investor2, - "Best bid should be investor2" - ); - - // Advance time past expiration - env.ledger() - .set_timestamp(env.ledger().timestamp() + 604800 + 1); - - // Ranking should return empty after all bids expire - let ranked_after = client.get_ranked_bids(&invoice_id); - assert_eq!( - ranked_after.len(), - 0, - "Ranking should be empty after all bids expire" - ); - - // Best bid should be None - let best_after = client.get_best_bid(&invoice_id); - assert!( - best_after.is_none(), - "Best bid should be None after all bids expire" - ); -} -// ============================================================================ -// Category 5: Investment Limit Management -// ============================================================================ - -/// Test: Admin can set investment limit for verified investor -#[test] -fn test_set_investment_limit_succeeds() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - - // Create investor with initial limit - let investor = add_verified_investor(&env, &client, 50_000); - - // Verify initial limit (will be adjusted by tier/risk multipliers) - let verification = client.get_investor_verification(&investor).unwrap(); - let initial_limit = verification.investment_limit; - - // Admin updates limit - client.set_investment_limit(&investor, &100_000); - - // Verify limit was updated (should be higher than initial) - let updated_verification = client.get_investor_verification(&investor).unwrap(); - assert!( - updated_verification.investment_limit > initial_limit, - "Investment limit should be increased" - ); -} - -/// Test: Non-admin cannot set investment limit -#[test] -fn test_set_investment_limit_non_admin_fails() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - - // Create an unverified investor (no admin setup) - let investor = Address::generate(&env); - client.submit_investor_kyc(&investor, &String::from_str(&env, "KYC")); - - // Try to set limit without admin setup - should fail with NotAdmin error - let result = client.try_set_investment_limit(&investor, &100_000); - assert!(result.is_err(), "Should fail when no admin is configured"); -} - -/// Test: Cannot set limit for unverified investor -#[test] -fn test_set_investment_limit_unverified_fails() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - - let unverified_investor = Address::generate(&env); - - // Try to set limit for unverified investor - let result = client.try_set_investment_limit(&unverified_investor, &100_000); - assert!( - result.is_err(), - "Should not be able to set limit for unverified investor" - ); -} - -/// Test: Cannot set invalid investment limit -#[test] -fn test_set_investment_limit_invalid_amount_fails() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - - let investor = add_verified_investor(&env, &client, 50_000); - - // Try to set zero or negative limit - let result = client.try_set_investment_limit(&investor, &0); - assert!( - result.is_err(), - "Should not be able to set zero investment limit" - ); - - let result = client.try_set_investment_limit(&investor, &-1000); - assert!( - result.is_err(), - "Should not be able to set negative investment limit" - ); -} - -/// Test: Updated limit is enforced in bid placement -#[test] -fn test_updated_limit_enforced_in_bidding() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - - // Create investor with low initial limit - let investor = add_verified_investor(&env, &client, 10_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 50_000); - - // Bid above initial limit should fail - let result = client.try_place_bid(&investor, &invoice_id, &15_000, &16_000); - assert!(result.is_err(), "Bid above initial limit should fail"); - - // Admin increases limit - let _ = client.set_investment_limit(&investor, &50_000); - - // Now the same bid should succeed - let result = client.try_place_bid(&investor, &invoice_id, &15_000, &16_000); - assert!(result.is_ok(), "Bid should succeed after limit increase"); -} - -/// Test: cancel_bid transitions Placed → Cancelled -#[test] -fn test_cancel_bid_succeeds() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 10_000); - let bid_id = client.place_bid(&investor, &invoice_id, &5_000, &6_000); - - let result = client.cancel_bid(&bid_id); - assert!(result, "cancel_bid should return true for a Placed bid"); - - let bid = client.get_bid(&bid_id).unwrap(); - assert_eq!(bid.status, BidStatus::Cancelled, "Bid must be Cancelled"); -} - -/// Test: cancel_bid on already Withdrawn bid returns false -#[test] -fn test_cancel_bid_on_withdrawn_returns_false() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 10_000); - let bid_id = client.place_bid(&investor, &invoice_id, &5_000, &6_000); - - client.withdraw_bid(&bid_id); - let result = client.cancel_bid(&bid_id); - assert!(!result, "cancel_bid must return false for non-Placed bid"); -} - -/// Test: cancel_bid on already Cancelled bid returns false -#[test] -fn test_cancel_bid_on_cancelled_returns_false() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 10_000); - let bid_id = client.place_bid(&investor, &invoice_id, &5_000, &6_000); - - client.cancel_bid(&bid_id); - let result = client.cancel_bid(&bid_id); - assert!(!result, "Double cancel must return false"); -} - -/// Test: cancel_bid on non-existent bid_id returns false -#[test] -fn test_cancel_bid_nonexistent_returns_false() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let fake_bid_id = BytesN::from_array(&env, &[0u8; 32]); - let result = client.cancel_bid(&fake_bid_id); - assert!(!result, "cancel_bid on unknown ID must return false"); -} - -/// Test: cancelled bid excluded from ranking -#[test] -fn test_cancelled_bid_excluded_from_ranking() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // investor1 profit = 5k (best) - let bid_1 = client.place_bid(&investor1, &invoice_id, &10_000, &15_000); - // investor2 profit = 2k - let _bid_2 = client.place_bid(&investor2, &invoice_id, &10_000, &12_000); - - client.cancel_bid(&bid_1); - - let best = client.get_best_bid(&invoice_id).unwrap(); - assert_eq!( - best.investor, investor2, - "Cancelled bid must be excluded from ranking" - ); -} - -/// Test: get_all_bids_by_investor returns bids across multiple invoices -#[test] -fn test_get_all_bids_by_investor_cross_invoice() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id_1 = create_verified_invoice(&env, &client, &admin, &business, 50_000); - let invoice_id_2 = create_verified_invoice(&env, &client, &admin, &business, 50_000); - - client.place_bid(&investor, &invoice_id_1, &10_000, &12_000); - client.place_bid(&investor, &invoice_id_2, &15_000, &18_000); - - let all_bids = client.get_all_bids_by_investor(&investor); - assert_eq!(all_bids.len(), 2, "Must return bids across all invoices"); -} - -/// Test: get_all_bids_by_investor returns empty for investor with no bids -#[test] -fn test_get_all_bids_by_investor_empty() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let investor = Address::generate(&env); - let all_bids = client.get_all_bids_by_investor(&investor); - assert_eq!(all_bids.len(), 0, "Must return empty for unknown investor"); -} - -// ============================================================================ -// Multiple Investors - Same Invoice Tests (Issue #343) -// ============================================================================ - -/// Test: Multiple investors place bids on same invoice - all bids are tracked -#[test] -fn test_multiple_investors_place_bids_on_same_invoice() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - - // Create 5 verified investors - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let investor3 = add_verified_investor(&env, &client, 100_000); - let investor4 = add_verified_investor(&env, &client, 100_000); - let investor5 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // All 5 investors place bids with different amounts and profits - let bid_id1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); // profit: 2k - let bid_id2 = client.place_bid(&investor2, &invoice_id, &15_000, &20_000); // profit: 5k (best) - let bid_id3 = client.place_bid(&investor3, &invoice_id, &20_000, &24_000); // profit: 4k - let bid_id4 = client.place_bid(&investor4, &invoice_id, &12_000, &15_000); // profit: 3k - let bid_id5 = client.place_bid(&investor5, &invoice_id, &18_000, &21_000); // profit: 3k - - // Verify all bids are in Placed status - let placed_bids = client.get_bids_by_status(&invoice_id, &BidStatus::Placed); - assert_eq!( - placed_bids.len(), - 5, - "All 5 bids should be in Placed status" - ); - - // Verify get_bids_for_invoice returns all bid IDs - let all_bid_ids = client.get_bids_for_invoice(&invoice_id); - assert_eq!( - all_bid_ids.len(), - 5, - "get_bids_for_invoice should return all 5 bid IDs" - ); - - // Verify all specific bid IDs are present - assert!( - all_bid_ids.iter().any(|bid| bid.bid_id == bid_id1), - "bid_id1 should be in list" - ); - assert!( - all_bid_ids.iter().any(|bid| bid.bid_id == bid_id2), - "bid_id2 should be in list" - ); - assert!( - all_bid_ids.iter().any(|bid| bid.bid_id == bid_id3), - "bid_id3 should be in list" - ); - assert!( - all_bid_ids.iter().any(|bid| bid.bid_id == bid_id4), - "bid_id4 should be in list" - ); - assert!( - all_bid_ids.iter().any(|bid| bid.bid_id == bid_id5), - "bid_id5 should be in list" - ); -} - -/// Test: Multiple investors bids are correctly ranked by profit -#[test] -fn test_multiple_investors_bids_ranking_order() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let investor3 = add_verified_investor(&env, &client, 100_000); - let investor4 = add_verified_investor(&env, &client, 100_000); - let investor5 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place bids with different profit margins - let _bid1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); // profit: 2k - let _bid2 = client.place_bid(&investor2, &invoice_id, &15_000, &20_000); // profit: 5k (best) - let _bid3 = client.place_bid(&investor3, &invoice_id, &20_000, &24_000); // profit: 4k - let _bid4 = client.place_bid(&investor4, &invoice_id, &12_000, &15_000); // profit: 3k - let _bid5 = client.place_bid(&investor5, &invoice_id, &18_000, &21_000); // profit: 3k - - // Get ranked bids - let ranked = client.get_ranked_bids(&invoice_id); - assert_eq!(ranked.len(), 5, "Should have 5 ranked bids"); - - // Verify ranking order by profit (descending) - assert_eq!( - ranked.get(0).unwrap().investor, - investor2, - "Rank 1: investor2 (profit 5k)" - ); - assert_eq!( - ranked.get(1).unwrap().investor, - investor3, - "Rank 2: investor3 (profit 4k)" - ); - // investor4 and investor5 both have 3k profit - either order is valid - let rank3_investor = ranked.get(2).unwrap().investor; - let rank4_investor = ranked.get(3).unwrap().investor; - assert!( - (rank3_investor == investor4 && rank4_investor == investor5) - || (rank3_investor == investor5 && rank4_investor == investor4), - "Ranks 3-4: investor4 and investor5 (both profit 3k)" - ); - assert_eq!( - ranked.get(4).unwrap().investor, - investor1, - "Rank 5: investor1 (profit 2k)" - ); - - // Verify best bid is investor2 - let best = client.get_best_bid(&invoice_id).unwrap(); - assert_eq!( - best.investor, investor2, - "Best bid should be investor2 with highest profit" - ); -} - -/// Test: Business accepts one bid, others remain Placed -#[test] -fn test_business_accepts_one_bid_others_remain_placed() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let investor3 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Three investors place bids - let bid_id1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - let bid_id2 = client.place_bid(&investor2, &invoice_id, &15_000, &20_000); - let bid_id3 = client.place_bid(&investor3, &invoice_id, &20_000, &24_000); - - // Business accepts bid2 - let result = client.try_accept_bid(&invoice_id, &bid_id2); - assert!(result.is_ok(), "Business should be able to accept bid2"); - - // Verify bid2 is Accepted - let bid2 = client.get_bid(&bid_id2).unwrap(); - assert_eq!( - bid2.status, - BidStatus::Accepted, - "Accepted bid should have Accepted status" - ); - - // Verify bid1 and bid3 remain Placed - let bid1 = client.get_bid(&bid_id1).unwrap(); - assert_eq!( - bid1.status, - BidStatus::Placed, - "Non-accepted bid1 should remain Placed" - ); - - let bid3 = client.get_bid(&bid_id3).unwrap(); - assert_eq!( - bid3.status, - BidStatus::Placed, - "Non-accepted bid3 should remain Placed" - ); - - // Verify invoice is now Funded - let invoice = client.get_invoice(&invoice_id); - assert_eq!( - invoice.status, - InvoiceStatus::Funded, - "Invoice should be Funded after accepting bid" - ); -} - -/// Test: Only one escrow is created when business accepts a bid -#[test] -fn test_only_one_escrow_created_for_accepted_bid() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let investor3 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Three investors place bids - let _bid_id1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - let bid_id2 = client.place_bid(&investor2, &invoice_id, &15_000, &20_000); - let _bid_id3 = client.place_bid(&investor3, &invoice_id, &20_000, &24_000); - - // Business accepts bid2 - client.accept_bid(&invoice_id, &bid_id2); - - // Verify exactly one escrow exists for this invoice - let escrow = client.get_escrow_details(&invoice_id); - assert_eq!( - escrow.status, - EscrowStatus::Held, - "Escrow should be in Held status" - ); - assert_eq!( - escrow.investor, investor2, - "Escrow should reference investor2" - ); - assert_eq!( - escrow.amount, 15_000, - "Escrow should hold the accepted bid amount" - ); - assert_eq!( - escrow.invoice_id, invoice_id, - "Escrow should reference correct invoice" - ); - - // Verify invoice funded amount matches escrow amount - let invoice = client.get_invoice(&invoice_id); - assert_eq!( - invoice.funded_amount, 15_000, - "Invoice funded amount should match escrow" - ); - assert_eq!( - invoice.investor, - Some(investor2), - "Invoice should reference investor2" - ); -} - -/// Test: Non-accepted investors can withdraw their bids after one is accepted -#[test] -fn test_non_accepted_investors_can_withdraw_after_acceptance() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let investor3 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Three investors place bids - let bid_id1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - let bid_id2 = client.place_bid(&investor2, &invoice_id, &15_000, &20_000); - let bid_id3 = client.place_bid(&investor3, &invoice_id, &20_000, &24_000); - - // Business accepts bid2 - client.accept_bid(&invoice_id, &bid_id2); - - // investor1 withdraws their bid - let result1 = client.try_withdraw_bid(&bid_id1); - assert!( - result1.is_ok(), - "investor1 should be able to withdraw their bid" - ); - - let bid1 = client.get_bid(&bid_id1).unwrap(); - assert_eq!( - bid1.status, - BidStatus::Withdrawn, - "bid1 should be Withdrawn" - ); - - // investor3 withdraws their bid - let result3 = client.try_withdraw_bid(&bid_id3); - assert!( - result3.is_ok(), - "investor3 should be able to withdraw their bid" - ); - - let bid3 = client.get_bid(&bid_id3).unwrap(); - assert_eq!( - bid3.status, - BidStatus::Withdrawn, - "bid3 should be Withdrawn" - ); - - // Verify bid2 remains Accepted - let bid2 = client.get_bid(&bid_id2).unwrap(); - assert_eq!( - bid2.status, - BidStatus::Accepted, - "bid2 should remain Accepted" - ); - - // Verify only Accepted bid remains in Placed status query - let placed_bids = client.get_bids_by_status(&invoice_id, &BidStatus::Placed); - assert_eq!( - placed_bids.len(), - 0, - "No bids should be in Placed status after withdrawals" - ); - - let withdrawn_bids = client.get_bids_by_status(&invoice_id, &BidStatus::Withdrawn); - assert_eq!(withdrawn_bids.len(), 2, "Two bids should be Withdrawn"); -} - -/// Test: get_bids_for_invoice returns all bids regardless of status -#[test] -fn test_get_bids_for_invoice_returns_all_bids() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let investor3 = add_verified_investor(&env, &client, 100_000); - let investor4 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Four investors place bids - let bid_id1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - let bid_id2 = client.place_bid(&investor2, &invoice_id, &15_000, &20_000); - let bid_id3 = client.place_bid(&investor3, &invoice_id, &20_000, &24_000); - let bid_id4 = client.place_bid(&investor4, &invoice_id, &12_000, &15_000); - - // Initial state: all bids should be returned - let all_bids = client.get_bids_for_invoice(&invoice_id); - assert_eq!(all_bids.len(), 4, "Should return all 4 bids initially"); - - // Business accepts bid2 - client.accept_bid(&invoice_id, &bid_id2); - - // investor1 withdraws - client.withdraw_bid(&bid_id1); - - // investor4 cancels - client.cancel_bid(&bid_id4); - - // get_bids_for_invoice should still return all bid IDs - // Note: This returns bid IDs, not full records - let all_bids_after = client.get_bids_for_invoice(&invoice_id); - assert_eq!(all_bids_after.len(), 4, "Should still return all 4 bid IDs"); - - // Verify we can retrieve each bid with different statuses - assert_eq!( - client.get_bid(&bid_id1).unwrap().status, - BidStatus::Withdrawn - ); - assert_eq!( - client.get_bid(&bid_id2).unwrap().status, - BidStatus::Accepted - ); - assert_eq!(client.get_bid(&bid_id3).unwrap().status, BidStatus::Placed); - assert_eq!( - client.get_bid(&bid_id4).unwrap().status, - BidStatus::Cancelled - ); -} - -/// Test: Cannot accept second bid after one is already accepted -#[test] -fn test_cannot_accept_second_bid_after_first_accepted() { - let (env, client, admin) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Two investors place bids - let bid_id1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - let bid_id2 = client.place_bid(&investor2, &invoice_id, &15_000, &20_000); - - // Business accepts bid1 - let result = client.try_accept_bid(&invoice_id, &bid_id1); - assert!(result.is_ok(), "First accept should succeed"); - - // Attempt to accept bid2 should fail (invoice already funded) - let result = client.try_accept_bid(&invoice_id, &bid_id2); - assert!( - result.is_err(), - "Second accept should fail - invoice already funded" - ); - - // Verify only bid1 is Accepted - assert_eq!( - client.get_bid(&bid_id1).unwrap().status, - BidStatus::Accepted - ); - assert_eq!(client.get_bid(&bid_id2).unwrap().status, BidStatus::Placed); - - // Verify invoice is Funded with bid1's amount - let invoice = client.get_invoice(&invoice_id).unwrap(); - assert_eq!(invoice.status, InvoiceStatus::Funded); - assert_eq!(invoice.funded_amount, 10_000); - assert_eq!(invoice.investor, Some(investor1)); -} - -/// Test: cleanup_expired_bids correctly handles and counts already-expired bids -#[test] -fn test_cleanup_expired_bids_with_pre_set_expired() { - let (env, client) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Place a bid - let bid_id = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - - // Advance time past expiration - env.ledger().set_timestamp(env.ledger().timestamp() + 604800 + 1); - - // Simulate another process marking the bid as expired but NOT removing it from invoice index - let _ = BidStorage::count_active_placed_bids_for_investor(&env, &investor1); - - // Verify bid status is now Expired - let bid = client.get_bid(&bid_id).unwrap(); - assert_eq!(bid.status, BidStatus::Expired); - - // Verify it's still in the invoice bid index - let bids_in_index = client.get_bids_for_invoice(&invoice_id); - assert!(bids_in_index.iter().any(|b| b.bid_id == bid_id)); - - // Now call cleanup_expired_bids - it should find the already-expired bid, remove it, and return 1 - let cleaned = client.cleanup_expired_bids(&invoice_id); - assert_eq!(cleaned, 1, "Should count already-expired bid as cleaned up"); - - // Verify it's gone from the index - let bids_after = client.get_bids_for_invoice(&invoice_id); - assert_eq!(bids_after.len(), 0, "Index should be empty after cleanup"); - - // Second call should return 0 - let cleaned_again = client.cleanup_expired_bids(&invoice_id); - assert_eq!(cleaned_again, 0, "Second cleanup should be idempotent and return 0"); -} - -/// Test: cleanup_expired_bids with partial expirations over time -#[test] -fn test_cleanup_expired_bids_partial_idempotency() { - let (env, client) = setup(); - env.mock_all_auths(); - let admin = Address::generate(&env); - let _ = client.set_admin(&admin); - let investor1 = add_verified_investor(&env, &client, 100_000); - let investor2 = add_verified_investor(&env, &client, 100_000); - let business = Address::generate(&env); - let invoice_id = create_verified_invoice(&env, &client, &admin, &business, 100_000); - - // Bid 1 - let _bid_id1 = client.place_bid(&investor1, &invoice_id, &10_000, &12_000); - - // Advance time partly - env.ledger().set_timestamp(env.ledger().timestamp() + 302400); // 3.5 days - - // Bid 2 - let _bid_id2 = client.place_bid(&investor2, &invoice_id, &10_000, &12_000); - - // Advance time so Bid 1 expires but Bid 2 doesn't - env.ledger().set_timestamp(env.ledger().timestamp() + 432000); // +5 days - - // First cleanup - let cleaned = client.cleanup_expired_bids(&invoice_id); - assert_eq!(cleaned, 1, "Only bid 1 should be cleaned up"); - - // Second cleanup immediately - let cleaned2 = client.cleanup_expired_bids(&invoice_id); - assert_eq!(cleaned2, 0, "Second cleanup should return 0 - no new expirations"); - - // Advance time so Bid 2 also expires - env.ledger().set_timestamp(env.ledger().timestamp() + 432000); // +5 days - - // Third cleanup - let cleaned3 = client.cleanup_expired_bids(&invoice_id); - assert_eq!(cleaned3, 1, "Bid 2 should now be cleaned up"); - - let cleaned4 = client.cleanup_expired_bids(&invoice_id); - assert_eq!(cleaned4, 0, "No more bids left!"); -} diff --git a/quicklendx-contracts/src/test_fees.rs b/quicklendx-contracts/src/test_fees.rs index 0805e0d8..55ed8ce7 100644 --- a/quicklendx-contracts/src/test_fees.rs +++ b/quicklendx-contracts/src/test_fees.rs @@ -176,7 +176,7 @@ fn test_only_admin_can_update_platform_fee() { invoke: &MockAuthInvoke { contract: &contract_id, fn_name: "set_platform_fee", - args: (300i128,).into_val(&env), + args: (300u32,).into_val(&env), sub_invokes: &[], }, }; @@ -201,7 +201,7 @@ fn test_only_admin_can_update_platform_fee() { invoke: &MockAuthInvoke { contract: &contract_id, fn_name: "set_platform_fee", - args: (300i128,).into_val(&env), + args: (300u32,).into_val(&env), sub_invokes: &[], }, }; diff --git a/quicklendx-contracts/src/test_ledger_timestamp_consistency.rs b/quicklendx-contracts/src/test_ledger_timestamp_consistency.rs index cbd35c89..be15562d 100644 --- a/quicklendx-contracts/src/test_ledger_timestamp_consistency.rs +++ b/quicklendx-contracts/src/test_ledger_timestamp_consistency.rs @@ -29,6 +29,7 @@ #![cfg(test)] extern crate std; +use std::vec::Vec as StdVec; use crate::{invoice::InvoiceCategory, QuickLendXContract, QuickLendXContractClient}; use soroban_sdk::{ diff --git a/quicklendx-contracts/src/test_lifecycle.rs b/quicklendx-contracts/src/test_lifecycle.rs index 44213657..148eec88 100644 --- a/quicklendx-contracts/src/test_lifecycle.rs +++ b/quicklendx-contracts/src/test_lifecycle.rs @@ -40,7 +40,7 @@ use super::*; use crate::bid::BidStatus; use crate::errors::QuickLendXError; use crate::investment::InvestmentStatus; -use crate::invoice::{InvoiceCategory, InvoiceStatus}; +use crate::invoice::{InvoiceCategory, InvoiceStatus, InvoiceStorage}; use crate::verification::BusinessVerificationStatus; use soroban_sdk::{ symbol_short, @@ -374,12 +374,18 @@ fn test_full_invoice_lifecycle() { // ── step 10: investor rates the invoice ──────────────────────────────────── let rating: u32 = 5; - client.add_invoice_rating( - &invoice_id, - &rating, - &String::from_str(&env, "Excellent! Payment on time."), - &investor, - ); + env.as_contract(&contract_id, || { + let mut invoice = InvoiceStorage::get_invoice(&env, &invoice_id).unwrap(); + invoice + .add_rating( + rating, + String::from_str(&env, "Excellent! Payment on time."), + investor.clone(), + env.ledger().timestamp(), + ) + .unwrap(); + InvoiceStorage::update_invoice(&env, &invoice); + }); let (avg, count, high, low) = client.get_invoice_rating_stats(&invoice_id).unwrap(); assert_eq!(count, 1); @@ -472,12 +478,18 @@ fn test_lifecycle_escrow_token_flow() { // ── step 10: investor rates the invoice ──────────────────────────────────── let rating: u32 = 4; - client.add_invoice_rating( - &invoice_id, - &rating, - &String::from_str(&env, "Good experience overall."), - &investor, - ); + env.as_contract(&contract_id, || { + let mut invoice = InvoiceStorage::get_invoice(&env, &invoice_id).unwrap(); + invoice + .add_rating( + rating, + String::from_str(&env, "Good experience overall."), + investor.clone(), + env.ledger().timestamp(), + ) + .unwrap(); + InvoiceStorage::update_invoice(&env, &invoice); + }); let (avg, count, high, low) = client.get_invoice_rating_stats(&invoice_id).unwrap(); assert_eq!(count, 1); @@ -656,21 +668,23 @@ fn test_full_lifecycle_step_by_step() { // ── Step 10: Investor rates the invoice ──────────────────────────────────── let rating: u32 = 5; - client.add_invoice_rating( - &invoice_id, - &rating, - &String::from_str(&env, "Excellent! Payment on time."), - &investor, - ); - let (avg, count, high, low) = client.get_invoice_rating_stats(&invoice_id); - assert_eq!(count, 1); - assert_eq!(avg, Some(rating)); - assert_eq!(high, Some(rating)); - assert_eq!(low, Some(rating)); - assert!( - has_event_with_topic(&env, symbol_short!("rated")), - "rated event expected after rating" - ); + env.as_contract(&contract_id, || { + let mut invoice = InvoiceStorage::get_invoice(&env, &invoice_id).unwrap(); + invoice + .add_rating( + rating, + String::from_str(&env, "Excellent! Payment on time."), + investor.clone(), + env.ledger().timestamp(), + ) + .unwrap(); + InvoiceStorage::update_invoice(&env, &invoice); + }); + let invoice = client.get_invoice(&invoice_id); + assert_eq!(invoice.total_ratings, 1); + assert_eq!(invoice.average_rating, Some(rating)); + assert_eq!(invoice.get_highest_rating(), Some(rating)); + assert_eq!(invoice.get_lowest_rating(), Some(rating)); assert_lifecycle_events_emitted(&env); } diff --git a/quicklendx-contracts/src/test_max_invoices_per_business.rs b/quicklendx-contracts/src/test_max_invoices_per_business.rs index e26e5071..6108a92d 100644 --- a/quicklendx-contracts/src/test_max_invoices_per_business.rs +++ b/quicklendx-contracts/src/test_max_invoices_per_business.rs @@ -58,6 +58,7 @@ fn upload_basic_invoice( #[test] fn test_upload_invoice_enforces_max_invoices_per_business() { let (env, client, admin, business, currency) = setup(); + client.update_limits_max_invoices(&admin, &10, &365, &86_400, &5); client.update_limits_max_invoices(&admin, &10i128, &365u64, &0u64, &3u32); @@ -120,6 +121,7 @@ fn test_cancelled_invoices_free_up_slots() { #[test] fn test_paid_invoices_free_up_slots() { let (env, client, admin, business, currency) = setup(); + client.update_limits_max_invoices(&admin, &10, &365, &86_400, &2); client.update_limits_max_invoices(&admin, &10i128, &365u64, &0u64, &2u32); @@ -142,6 +144,7 @@ fn test_paid_invoices_free_up_slots() { #[test] fn test_limit_zero_disables_max_invoices_check() { let (env, client, admin, business, currency) = setup(); + client.update_limits_max_invoices(&admin, &10, &365, &86_400, &0); // limit=0 means unlimited client.update_limits_max_invoices(&admin, &10i128, &365u64, &0u64, &0u32);