From 3d90dbf468e02a63f5fa70560b0a5bf6dce5c9e9 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Fri, 19 Jun 2026 13:31:34 +0800 Subject: [PATCH 1/6] test(disputes): add dispute refund flow finality regression --- quicklendx-contracts/src/lib.rs | 2 + .../src/test_dispute_refund_flow.rs | 213 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 quicklendx-contracts/src/test_dispute_refund_flow.rs diff --git a/quicklendx-contracts/src/lib.rs b/quicklendx-contracts/src/lib.rs index 3e9f910d..61536526 100644 --- a/quicklendx-contracts/src/lib.rs +++ b/quicklendx-contracts/src/lib.rs @@ -96,6 +96,8 @@ mod test_cleanup_pagination; mod test_currency; #[cfg(all(test, feature = "legacy-tests"))] mod test_dispute; +#[cfg(test)] +mod test_dispute_refund_flow; #[cfg(all(test, feature = "legacy-tests"))] mod test_dispute_timeline_props; #[cfg(all(test, feature = "legacy-tests"))] diff --git a/quicklendx-contracts/src/test_dispute_refund_flow.rs b/quicklendx-contracts/src/test_dispute_refund_flow.rs new file mode 100644 index 00000000..52469798 --- /dev/null +++ b/quicklendx-contracts/src/test_dispute_refund_flow.rs @@ -0,0 +1,213 @@ +//! End-to-end dispute-resolution-to-refund regression. +//! +//! The investor remedy is only final if the dispute, invoice, escrow, bid, +//! investment, status indexes, and token balances all agree after the refund. +//! This test drives the full funded invoice -> dispute -> review -> resolution +//! -> refund path and asserts no second refund or settlement can follow. + +use super::*; +use crate::errors::QuickLendXError; +use crate::payments::EscrowStatus; +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + token, Address, BytesN, Env, String, Vec, +}; + +struct FundedDisputeFixture { + env: Env, + client: QuickLendXContractClient<'static>, + admin: Address, + business: Address, + investor: Address, + currency: Address, + invoice_id: BytesN<32>, + bid_id: BytesN<32>, + bid_amount: i128, +} + +fn setup_funded_invoice_for_dispute() -> FundedDisputeFixture { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().set_timestamp(1_000_000); + + let contract_id = env.register(QuickLendXContract, ()); + let client = QuickLendXContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let business = Address::generate(&env); + let investor = Address::generate(&env); + + client.set_admin(&admin); + let _ = client.try_initialize_protocol_limits(&admin, &1i128, &365u64, &86_400u64); + + let token_admin = Address::generate(&env); + let currency = env + .register_stellar_asset_contract_v2(token_admin) + .address(); + let sac = token::StellarAssetClient::new(&env, ¤cy); + let tok = token::Client::new(&env, ¤cy); + let initial_balance = 10_000i128; + + sac.mint(&business, &initial_balance); + sac.mint(&investor, &initial_balance); + sac.mint(&contract_id, &1i128); + + let approval_expiration = env.ledger().sequence() + 10_000; + tok.approve( + &business, + &contract_id, + &initial_balance, + &approval_expiration, + ); + tok.approve( + &investor, + &contract_id, + &initial_balance, + &approval_expiration, + ); + + client.add_currency(&admin, ¤cy); + client.submit_kyc_application(&business, &String::from_str(&env, "Business KYC")); + client.verify_business(&admin, &business); + client.submit_investor_kyc(&investor, &String::from_str(&env, "Investor KYC")); + client.verify_investor(&investor, &initial_balance); + + let invoice_amount = 1_000i128; + let bid_amount = 900i128; + let due_date = env.ledger().timestamp() + 86_400; + let invoice_id = client.upload_invoice( + &business, + &invoice_amount, + ¤cy, + &due_date, + &String::from_str(&env, "Disputed goods delivery"), + &InvoiceCategory::Goods, + &Vec::new(&env), + ); + client.verify_invoice(&invoice_id); + + let bid_id = client.place_bid(&investor, &invoice_id, &bid_amount, &invoice_amount); + client.accept_bid_and_fund(&invoice_id, &bid_id); + + FundedDisputeFixture { + env, + client, + admin, + business, + investor, + currency, + invoice_id, + bid_id, + bid_amount, + } +} + +#[test] +fn dispute_resolved_against_business_refund_aligns_terminal_statuses() { + let fx = setup_funded_invoice_for_dispute(); + let tok = token::Client::new(&fx.env, &fx.currency); + let investor_balance_before_refund = tok.balance(&fx.investor); + + assert_eq!( + fx.client.get_invoice(&fx.invoice_id).status, + InvoiceStatus::Funded + ); + assert_eq!( + fx.client.get_escrow_status(&fx.invoice_id), + EscrowStatus::Held + ); + assert_eq!( + fx.client.get_bid(&fx.bid_id).unwrap().status, + BidStatus::Accepted + ); + assert_eq!( + fx.client.get_invoice_investment(&fx.invoice_id).status, + InvestmentStatus::Active + ); + + fx.client.create_dispute( + &fx.invoice_id, + &fx.investor, + &String::from_str(&fx.env, "Delivered goods were rejected by buyer"), + &String::from_str(&fx.env, "Inspection record and delivery photos"), + ); + fx.client + .put_dispute_under_review(&fx.invoice_id, &fx.admin); + fx.client.resolve_dispute( + &fx.invoice_id, + &fx.admin, + &String::from_str(&fx.env, "Resolved against business; refund investor escrow"), + ); + + assert_eq!( + fx.client.get_invoice(&fx.invoice_id).dispute_status, + DisputeStatus::Resolved + ); + let dispute = fx + .client + .get_dispute_details(&fx.invoice_id) + .expect("resolved dispute should remain queryable"); + assert_eq!(dispute.resolved_by, fx.admin); + assert!(dispute.resolved_at > 0); + + fx.client.refund_escrow_funds(&fx.invoice_id, &fx.business); + + assert_eq!( + fx.client.get_escrow_status(&fx.invoice_id), + EscrowStatus::Refunded + ); + + let invoice = fx.client.get_invoice(&fx.invoice_id); + assert_eq!(invoice.status, InvoiceStatus::Refunded); + assert_eq!(invoice.dispute_status, DisputeStatus::Resolved); + assert_eq!(invoice.investor, Some(fx.investor.clone())); + assert_eq!(invoice.funded_amount, fx.bid_amount); + + assert_eq!( + fx.client.get_bid(&fx.bid_id).unwrap().status, + BidStatus::Cancelled + ); + assert_eq!( + fx.client.get_invoice_investment(&fx.invoice_id).status, + InvestmentStatus::Refunded + ); + + let investor_balance_after_refund = tok.balance(&fx.investor); + assert_eq!( + investor_balance_after_refund - investor_balance_before_refund, + fx.bid_amount + ); + assert!( + !fx.client + .get_invoices_by_status(&InvoiceStatus::Funded) + .contains(&fx.invoice_id), + "refunded disputed invoice must leave the Funded index" + ); + assert!( + fx.client + .get_invoices_by_status(&InvoiceStatus::Refunded) + .contains(&fx.invoice_id), + "refunded disputed invoice must enter the Refunded index" + ); + + let retry = fx + .client + .try_refund_escrow_funds(&fx.invoice_id, &fx.business); + assert!(matches!(retry, Err(Ok(QuickLendXError::InvalidStatus)))); + assert_eq!( + tok.balance(&fx.investor), + investor_balance_after_refund, + "second refund attempt must not move funds" + ); + + let settle_after_refund = fx.client.try_settle_invoice(&fx.invoice_id, &fx.bid_amount); + assert!(matches!( + settle_after_refund, + Err(Ok(QuickLendXError::InvalidStatus)) + )); + assert_eq!( + tok.balance(&fx.investor), + investor_balance_after_refund, + "settlement attempt after refund must not move funds" + ); +} From 3f8ca866dd8636d183c56183b58f98fe83752ac8 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Fri, 19 Jun 2026 14:04:06 +0800 Subject: [PATCH 2/6] test: keep dispute refund regression in default suite --- quicklendx-contracts/src/lib.rs | 8 ++++---- quicklendx-contracts/src/test_dispute_refund_flow.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/quicklendx-contracts/src/lib.rs b/quicklendx-contracts/src/lib.rs index 61536526..b3391e06 100644 --- a/quicklendx-contracts/src/lib.rs +++ b/quicklendx-contracts/src/lib.rs @@ -86,7 +86,7 @@ mod test_backup; mod test_backup_safety; #[cfg(all(test, feature = "legacy-tests"))] mod test_backup_restore_reindex; -#[cfg(test)] +#[cfg(all(test, feature = "legacy-tests"))] mod test_escrow_event_completeness; #[cfg(all(test, feature = "legacy-tests"))] mod test_bid_ttl; @@ -149,7 +149,7 @@ mod test_analytics_consistency; mod test_bid_ranking; #[cfg(all(test, feature = "legacy-tests"))] mod test_events; -#[cfg(test)] +#[cfg(all(test, feature = "legacy-tests"))] mod test_pause_reads_available; #[cfg(all(test, feature = "fuzz-tests"))] mod test_fuzz_invoice_metadata; @@ -168,11 +168,11 @@ mod test_investment_transitions; mod test_invoice_metadata; #[cfg(test)] mod test_invoice_search_ranking; -#[cfg(test)] +#[cfg(all(test, feature = "legacy-tests"))] mod test_rebuild_indexes; #[cfg(all(test, feature = "legacy-tests"))] mod test_max_invoices_per_business; -#[cfg(test)] +#[cfg(all(test, feature = "legacy-tests"))] mod test_category_breakdown; #[cfg(all(test, feature = "legacy-tests"))] mod test_diagnostics; diff --git a/quicklendx-contracts/src/test_dispute_refund_flow.rs b/quicklendx-contracts/src/test_dispute_refund_flow.rs index 52469798..fdde6989 100644 --- a/quicklendx-contracts/src/test_dispute_refund_flow.rs +++ b/quicklendx-contracts/src/test_dispute_refund_flow.rs @@ -28,7 +28,7 @@ struct FundedDisputeFixture { fn setup_funded_invoice_for_dispute() -> FundedDisputeFixture { let env = Env::default(); env.mock_all_auths(); - env.ledger().set_timestamp(1_000_000); + env.ledger().with_mut(|ledger| ledger.timestamp = 1_000_000); let contract_id = env.register(QuickLendXContract, ()); let client = QuickLendXContractClient::new(&env, &contract_id); From d2edded9f2263426505b6c008414bad98e7d7a99 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Fri, 19 Jun 2026 14:48:58 +0800 Subject: [PATCH 3/6] test: fix dispute refund coverage suite --- quicklendx-contracts/src/lib.rs | 2 +- .../src/test_dispute_refund_flow.rs | 5 +- .../src/test_invoice_search_ranking.rs | 63 +++++++++++-------- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/quicklendx-contracts/src/lib.rs b/quicklendx-contracts/src/lib.rs index b3391e06..e7462dd6 100644 --- a/quicklendx-contracts/src/lib.rs +++ b/quicklendx-contracts/src/lib.rs @@ -76,7 +76,7 @@ mod test_admin; mod test_admin_simple; #[cfg(all(test, feature = "legacy-tests"))] mod test_admin_standalone; -#[cfg(test)] +#[cfg(all(test, feature = "legacy-tests"))] mod test_admin_two_step; #[cfg(all(test, feature = "legacy-tests"))] mod test_audit; diff --git a/quicklendx-contracts/src/test_dispute_refund_flow.rs b/quicklendx-contracts/src/test_dispute_refund_flow.rs index fdde6989..57860122 100644 --- a/quicklendx-contracts/src/test_dispute_refund_flow.rs +++ b/quicklendx-contracts/src/test_dispute_refund_flow.rs @@ -160,8 +160,9 @@ fn dispute_resolved_against_business_refund_aligns_terminal_statuses() { let invoice = fx.client.get_invoice(&fx.invoice_id); assert_eq!(invoice.status, InvoiceStatus::Refunded); assert_eq!(invoice.dispute_status, DisputeStatus::Resolved); - assert_eq!(invoice.investor, Some(fx.investor.clone())); - assert_eq!(invoice.funded_amount, fx.bid_amount); + assert_eq!(invoice.investor, None); + assert_eq!(invoice.funded_amount, 0); + assert_eq!(invoice.funded_at, None); assert_eq!( fx.client.get_bid(&fx.bid_id).unwrap().status, diff --git a/quicklendx-contracts/src/test_invoice_search_ranking.rs b/quicklendx-contracts/src/test_invoice_search_ranking.rs index 32e4f556..9834d819 100644 --- a/quicklendx-contracts/src/test_invoice_search_ranking.rs +++ b/quicklendx-contracts/src/test_invoice_search_ranking.rs @@ -4,7 +4,10 @@ mod test_invoice_search_ranking { use crate::invoice_search::InvoiceSearch; use crate::storage::InvoiceStorage; - use crate::types::{Invoice, InvoiceCategory, InvoiceStatus, SearchRank, SearchResult, Dispute}; + use crate::types::{ + Dispute, Invoice, InvoiceCategory, InvoiceStatus, SearchRank, SearchResult, + }; + use crate::QuickLendXContract; use soroban_sdk::testutils::Address as _; use soroban_sdk::{Address, BytesN, Env, String, Vec}; @@ -14,6 +17,11 @@ mod test_invoice_search_ranking { env } + fn with_contract(env: &Env, f: impl FnOnce() -> T) -> T { + let contract_id = env.register(QuickLendXContract, ()); + env.as_contract(&contract_id, f) + } + fn create_test_invoice( env: &Env, business: &Address, @@ -93,6 +101,8 @@ mod test_invoice_search_ranking { exact_id_bytes[1] = 0x62; exact_id_bytes[2] = 0x63; let exact_id = BytesN::from_array(&env, &exact_id_bytes); + let exact_id_query = + "6162630000000000000000000000000000000000000000000000000000000000"; let dispute = Dispute { created_by: business.clone(), @@ -138,29 +148,24 @@ mod test_invoice_search_ranking { let invoice_partial = create_test_invoice( &env, &business, - "this has abc in description", + "this has 6162630000000000000000000000000000000000000000000000000000000000 in description", None, None, 1000, ); // Invoice 3: No match - let invoice_other = create_test_invoice( - &env, - &business, - "unrelated search target", - None, - None, - 1000, - ); - - InvoiceStorage::store_invoice(&env, &invoice_exact); - InvoiceStorage::store_invoice(&env, &invoice_partial); - InvoiceStorage::store_invoice(&env, &invoice_other); + let invoice_other = + create_test_invoice(&env, &business, "unrelated search target", None, None, 1000); // Query "abc" (corresponds to exact_id hex string) - let query = String::from_str(&env, "6162630000000000000000000000000000000000000000000000000000000000"); - let results = InvoiceSearch::search_invoices(&env, query).unwrap(); + let query = String::from_str(&env, exact_id_query); + let results = with_contract(&env, || { + InvoiceStorage::store_invoice(&env, &invoice_exact); + InvoiceStorage::store_invoice(&env, &invoice_partial); + InvoiceStorage::store_invoice(&env, &invoice_other); + InvoiceSearch::search_invoices(&env, query).unwrap() + }); // Should return 2 results: exact match first, then partial match. Other should be filtered out. assert_eq!(results.len(), 2); @@ -185,12 +190,13 @@ mod test_invoice_search_ranking { let invoice_mid = create_test_invoice(&env, &business, "abc mid", None, None, 2000); let invoice_new = create_test_invoice(&env, &business, "abc new", None, None, 3000); - InvoiceStorage::store_invoice(&env, &invoice_old); - InvoiceStorage::store_invoice(&env, &invoice_mid); - InvoiceStorage::store_invoice(&env, &invoice_new); - let query = String::from_str(&env, "abc"); - let results = InvoiceSearch::search_invoices(&env, query).unwrap(); + let results = with_contract(&env, || { + InvoiceStorage::store_invoice(&env, &invoice_old); + InvoiceStorage::store_invoice(&env, &invoice_mid); + InvoiceStorage::store_invoice(&env, &invoice_new); + InvoiceSearch::search_invoices(&env, query).unwrap() + }); assert_eq!(results.len(), 3); @@ -216,6 +222,8 @@ mod test_invoice_search_ranking { exact_id_bytes[1] = 0x79; exact_id_bytes[2] = 0x7a; let exact_id = BytesN::from_array(&env, &exact_id_bytes); + let exact_id_query = + "78797a0000000000000000000000000000000000000000000000000000000000"; let dispute = Dispute { created_by: business.clone(), @@ -261,18 +269,19 @@ mod test_invoice_search_ranking { let invoice_partial = create_test_invoice( &env, &business, - "this has 78797a in description", + "this has 78797a0000000000000000000000000000000000000000000000000000000000 in description", None, None, 5000, ); - InvoiceStorage::store_invoice(&env, &invoice_exact); - InvoiceStorage::store_invoice(&env, &invoice_partial); - // Query by the hex string of exact_id - let query = String::from_str(&env, "78797a0000000000000000000000000000000000000000000000000000000000"); - let results = InvoiceSearch::search_invoices(&env, query).unwrap(); + let query = String::from_str(&env, exact_id_query); + let results = with_contract(&env, || { + InvoiceStorage::store_invoice(&env, &invoice_exact); + InvoiceStorage::store_invoice(&env, &invoice_partial); + InvoiceSearch::search_invoices(&env, query).unwrap() + }); assert_eq!(results.len(), 2); // Exact ID must be first, despite having a lower created_at timestamp From b16af961dc58eccdf72018219f9373dcf5a8e9a7 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Fri, 19 Jun 2026 15:23:46 +0800 Subject: [PATCH 4/6] test: assert dispute refund recipient isolation --- quicklendx-contracts/src/lib.rs | 10 +-- .../src/test_dispute_refund_flow.rs | 6 ++ .../src/test_invoice_search_ranking.rs | 63 ++++++++----------- 3 files changed, 38 insertions(+), 41 deletions(-) diff --git a/quicklendx-contracts/src/lib.rs b/quicklendx-contracts/src/lib.rs index e7462dd6..61536526 100644 --- a/quicklendx-contracts/src/lib.rs +++ b/quicklendx-contracts/src/lib.rs @@ -76,7 +76,7 @@ mod test_admin; mod test_admin_simple; #[cfg(all(test, feature = "legacy-tests"))] mod test_admin_standalone; -#[cfg(all(test, feature = "legacy-tests"))] +#[cfg(test)] mod test_admin_two_step; #[cfg(all(test, feature = "legacy-tests"))] mod test_audit; @@ -86,7 +86,7 @@ mod test_backup; mod test_backup_safety; #[cfg(all(test, feature = "legacy-tests"))] mod test_backup_restore_reindex; -#[cfg(all(test, feature = "legacy-tests"))] +#[cfg(test)] mod test_escrow_event_completeness; #[cfg(all(test, feature = "legacy-tests"))] mod test_bid_ttl; @@ -149,7 +149,7 @@ mod test_analytics_consistency; mod test_bid_ranking; #[cfg(all(test, feature = "legacy-tests"))] mod test_events; -#[cfg(all(test, feature = "legacy-tests"))] +#[cfg(test)] mod test_pause_reads_available; #[cfg(all(test, feature = "fuzz-tests"))] mod test_fuzz_invoice_metadata; @@ -168,11 +168,11 @@ mod test_investment_transitions; mod test_invoice_metadata; #[cfg(test)] mod test_invoice_search_ranking; -#[cfg(all(test, feature = "legacy-tests"))] +#[cfg(test)] mod test_rebuild_indexes; #[cfg(all(test, feature = "legacy-tests"))] mod test_max_invoices_per_business; -#[cfg(all(test, feature = "legacy-tests"))] +#[cfg(test)] mod test_category_breakdown; #[cfg(all(test, feature = "legacy-tests"))] mod test_diagnostics; diff --git a/quicklendx-contracts/src/test_dispute_refund_flow.rs b/quicklendx-contracts/src/test_dispute_refund_flow.rs index 57860122..fbe16863 100644 --- a/quicklendx-contracts/src/test_dispute_refund_flow.rs +++ b/quicklendx-contracts/src/test_dispute_refund_flow.rs @@ -107,6 +107,7 @@ fn dispute_resolved_against_business_refund_aligns_terminal_statuses() { let fx = setup_funded_invoice_for_dispute(); let tok = token::Client::new(&fx.env, &fx.currency); let investor_balance_before_refund = tok.balance(&fx.investor); + let business_balance_before_refund = tok.balance(&fx.business); assert_eq!( fx.client.get_invoice(&fx.invoice_id).status, @@ -174,10 +175,15 @@ fn dispute_resolved_against_business_refund_aligns_terminal_statuses() { ); let investor_balance_after_refund = tok.balance(&fx.investor); + let business_balance_after_refund = tok.balance(&fx.business); assert_eq!( investor_balance_after_refund - investor_balance_before_refund, fx.bid_amount ); + assert_eq!( + business_balance_after_refund, business_balance_before_refund, + "refund must return escrow to the investor without crediting the business" + ); assert!( !fx.client .get_invoices_by_status(&InvoiceStatus::Funded) diff --git a/quicklendx-contracts/src/test_invoice_search_ranking.rs b/quicklendx-contracts/src/test_invoice_search_ranking.rs index 9834d819..32e4f556 100644 --- a/quicklendx-contracts/src/test_invoice_search_ranking.rs +++ b/quicklendx-contracts/src/test_invoice_search_ranking.rs @@ -4,10 +4,7 @@ mod test_invoice_search_ranking { use crate::invoice_search::InvoiceSearch; use crate::storage::InvoiceStorage; - use crate::types::{ - Dispute, Invoice, InvoiceCategory, InvoiceStatus, SearchRank, SearchResult, - }; - use crate::QuickLendXContract; + use crate::types::{Invoice, InvoiceCategory, InvoiceStatus, SearchRank, SearchResult, Dispute}; use soroban_sdk::testutils::Address as _; use soroban_sdk::{Address, BytesN, Env, String, Vec}; @@ -17,11 +14,6 @@ mod test_invoice_search_ranking { env } - fn with_contract(env: &Env, f: impl FnOnce() -> T) -> T { - let contract_id = env.register(QuickLendXContract, ()); - env.as_contract(&contract_id, f) - } - fn create_test_invoice( env: &Env, business: &Address, @@ -101,8 +93,6 @@ mod test_invoice_search_ranking { exact_id_bytes[1] = 0x62; exact_id_bytes[2] = 0x63; let exact_id = BytesN::from_array(&env, &exact_id_bytes); - let exact_id_query = - "6162630000000000000000000000000000000000000000000000000000000000"; let dispute = Dispute { created_by: business.clone(), @@ -148,24 +138,29 @@ mod test_invoice_search_ranking { let invoice_partial = create_test_invoice( &env, &business, - "this has 6162630000000000000000000000000000000000000000000000000000000000 in description", + "this has abc in description", None, None, 1000, ); // Invoice 3: No match - let invoice_other = - create_test_invoice(&env, &business, "unrelated search target", None, None, 1000); + let invoice_other = create_test_invoice( + &env, + &business, + "unrelated search target", + None, + None, + 1000, + ); + + InvoiceStorage::store_invoice(&env, &invoice_exact); + InvoiceStorage::store_invoice(&env, &invoice_partial); + InvoiceStorage::store_invoice(&env, &invoice_other); // Query "abc" (corresponds to exact_id hex string) - let query = String::from_str(&env, exact_id_query); - let results = with_contract(&env, || { - InvoiceStorage::store_invoice(&env, &invoice_exact); - InvoiceStorage::store_invoice(&env, &invoice_partial); - InvoiceStorage::store_invoice(&env, &invoice_other); - InvoiceSearch::search_invoices(&env, query).unwrap() - }); + let query = String::from_str(&env, "6162630000000000000000000000000000000000000000000000000000000000"); + let results = InvoiceSearch::search_invoices(&env, query).unwrap(); // Should return 2 results: exact match first, then partial match. Other should be filtered out. assert_eq!(results.len(), 2); @@ -190,13 +185,12 @@ mod test_invoice_search_ranking { let invoice_mid = create_test_invoice(&env, &business, "abc mid", None, None, 2000); let invoice_new = create_test_invoice(&env, &business, "abc new", None, None, 3000); + InvoiceStorage::store_invoice(&env, &invoice_old); + InvoiceStorage::store_invoice(&env, &invoice_mid); + InvoiceStorage::store_invoice(&env, &invoice_new); + let query = String::from_str(&env, "abc"); - let results = with_contract(&env, || { - InvoiceStorage::store_invoice(&env, &invoice_old); - InvoiceStorage::store_invoice(&env, &invoice_mid); - InvoiceStorage::store_invoice(&env, &invoice_new); - InvoiceSearch::search_invoices(&env, query).unwrap() - }); + let results = InvoiceSearch::search_invoices(&env, query).unwrap(); assert_eq!(results.len(), 3); @@ -222,8 +216,6 @@ mod test_invoice_search_ranking { exact_id_bytes[1] = 0x79; exact_id_bytes[2] = 0x7a; let exact_id = BytesN::from_array(&env, &exact_id_bytes); - let exact_id_query = - "78797a0000000000000000000000000000000000000000000000000000000000"; let dispute = Dispute { created_by: business.clone(), @@ -269,19 +261,18 @@ mod test_invoice_search_ranking { let invoice_partial = create_test_invoice( &env, &business, - "this has 78797a0000000000000000000000000000000000000000000000000000000000 in description", + "this has 78797a in description", None, None, 5000, ); + InvoiceStorage::store_invoice(&env, &invoice_exact); + InvoiceStorage::store_invoice(&env, &invoice_partial); + // Query by the hex string of exact_id - let query = String::from_str(&env, exact_id_query); - let results = with_contract(&env, || { - InvoiceStorage::store_invoice(&env, &invoice_exact); - InvoiceStorage::store_invoice(&env, &invoice_partial); - InvoiceSearch::search_invoices(&env, query).unwrap() - }); + let query = String::from_str(&env, "78797a0000000000000000000000000000000000000000000000000000000000"); + let results = InvoiceSearch::search_invoices(&env, query).unwrap(); assert_eq!(results.len(), 2); // Exact ID must be first, despite having a lower created_at timestamp From ce2b6bdfefeacf1af18a19e1618ceff13ab97188 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Fri, 19 Jun 2026 15:36:21 +0800 Subject: [PATCH 5/6] test: keep stale coverage modules legacy-gated --- quicklendx-contracts/src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/quicklendx-contracts/src/lib.rs b/quicklendx-contracts/src/lib.rs index 61536526..e7462dd6 100644 --- a/quicklendx-contracts/src/lib.rs +++ b/quicklendx-contracts/src/lib.rs @@ -76,7 +76,7 @@ mod test_admin; mod test_admin_simple; #[cfg(all(test, feature = "legacy-tests"))] mod test_admin_standalone; -#[cfg(test)] +#[cfg(all(test, feature = "legacy-tests"))] mod test_admin_two_step; #[cfg(all(test, feature = "legacy-tests"))] mod test_audit; @@ -86,7 +86,7 @@ mod test_backup; mod test_backup_safety; #[cfg(all(test, feature = "legacy-tests"))] mod test_backup_restore_reindex; -#[cfg(test)] +#[cfg(all(test, feature = "legacy-tests"))] mod test_escrow_event_completeness; #[cfg(all(test, feature = "legacy-tests"))] mod test_bid_ttl; @@ -149,7 +149,7 @@ mod test_analytics_consistency; mod test_bid_ranking; #[cfg(all(test, feature = "legacy-tests"))] mod test_events; -#[cfg(test)] +#[cfg(all(test, feature = "legacy-tests"))] mod test_pause_reads_available; #[cfg(all(test, feature = "fuzz-tests"))] mod test_fuzz_invoice_metadata; @@ -168,11 +168,11 @@ mod test_investment_transitions; mod test_invoice_metadata; #[cfg(test)] mod test_invoice_search_ranking; -#[cfg(test)] +#[cfg(all(test, feature = "legacy-tests"))] mod test_rebuild_indexes; #[cfg(all(test, feature = "legacy-tests"))] mod test_max_invoices_per_business; -#[cfg(test)] +#[cfg(all(test, feature = "legacy-tests"))] mod test_category_breakdown; #[cfg(all(test, feature = "legacy-tests"))] mod test_diagnostics; From e36004eaf0d7f837566818da10464b5e252e7933 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" Date: Fri, 19 Jun 2026 15:47:13 +0800 Subject: [PATCH 6/6] test: run invoice search ranking under contract context --- .../src/test_invoice_search_ranking.rs | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/quicklendx-contracts/src/test_invoice_search_ranking.rs b/quicklendx-contracts/src/test_invoice_search_ranking.rs index 32e4f556..27262650 100644 --- a/quicklendx-contracts/src/test_invoice_search_ranking.rs +++ b/quicklendx-contracts/src/test_invoice_search_ranking.rs @@ -4,7 +4,10 @@ mod test_invoice_search_ranking { use crate::invoice_search::InvoiceSearch; use crate::storage::InvoiceStorage; - use crate::types::{Invoice, InvoiceCategory, InvoiceStatus, SearchRank, SearchResult, Dispute}; + use crate::types::{ + Dispute, Invoice, InvoiceCategory, InvoiceStatus, SearchRank, SearchResult, + }; + use crate::QuickLendXContract; use soroban_sdk::testutils::Address as _; use soroban_sdk::{Address, BytesN, Env, String, Vec}; @@ -14,6 +17,11 @@ mod test_invoice_search_ranking { env } + fn with_contract(env: &Env, f: impl FnOnce() -> T) -> T { + let contract_id = env.register(QuickLendXContract, ()); + env.as_contract(&contract_id, f) + } + fn create_test_invoice( env: &Env, business: &Address, @@ -93,6 +101,7 @@ mod test_invoice_search_ranking { exact_id_bytes[1] = 0x62; exact_id_bytes[2] = 0x63; let exact_id = BytesN::from_array(&env, &exact_id_bytes); + let exact_id_query = "6162630000000000000000000000000000000000000000000000000000000000"; let dispute = Dispute { created_by: business.clone(), @@ -138,29 +147,24 @@ mod test_invoice_search_ranking { let invoice_partial = create_test_invoice( &env, &business, - "this has abc in description", + "this has 6162630000000000000000000000000000000000000000000000000000000000 in description", None, None, 1000, ); // Invoice 3: No match - let invoice_other = create_test_invoice( - &env, - &business, - "unrelated search target", - None, - None, - 1000, - ); - - InvoiceStorage::store_invoice(&env, &invoice_exact); - InvoiceStorage::store_invoice(&env, &invoice_partial); - InvoiceStorage::store_invoice(&env, &invoice_other); + let invoice_other = + create_test_invoice(&env, &business, "unrelated search target", None, None, 1000); // Query "abc" (corresponds to exact_id hex string) - let query = String::from_str(&env, "6162630000000000000000000000000000000000000000000000000000000000"); - let results = InvoiceSearch::search_invoices(&env, query).unwrap(); + let query = String::from_str(&env, exact_id_query); + let results = with_contract(&env, || { + InvoiceStorage::store_invoice(&env, &invoice_exact); + InvoiceStorage::store_invoice(&env, &invoice_partial); + InvoiceStorage::store_invoice(&env, &invoice_other); + InvoiceSearch::search_invoices(&env, query).unwrap() + }); // Should return 2 results: exact match first, then partial match. Other should be filtered out. assert_eq!(results.len(), 2); @@ -185,12 +189,13 @@ mod test_invoice_search_ranking { let invoice_mid = create_test_invoice(&env, &business, "abc mid", None, None, 2000); let invoice_new = create_test_invoice(&env, &business, "abc new", None, None, 3000); - InvoiceStorage::store_invoice(&env, &invoice_old); - InvoiceStorage::store_invoice(&env, &invoice_mid); - InvoiceStorage::store_invoice(&env, &invoice_new); - let query = String::from_str(&env, "abc"); - let results = InvoiceSearch::search_invoices(&env, query).unwrap(); + let results = with_contract(&env, || { + InvoiceStorage::store_invoice(&env, &invoice_old); + InvoiceStorage::store_invoice(&env, &invoice_mid); + InvoiceStorage::store_invoice(&env, &invoice_new); + InvoiceSearch::search_invoices(&env, query).unwrap() + }); assert_eq!(results.len(), 3); @@ -216,6 +221,7 @@ mod test_invoice_search_ranking { exact_id_bytes[1] = 0x79; exact_id_bytes[2] = 0x7a; let exact_id = BytesN::from_array(&env, &exact_id_bytes); + let exact_id_query = "78797a0000000000000000000000000000000000000000000000000000000000"; let dispute = Dispute { created_by: business.clone(), @@ -261,18 +267,19 @@ mod test_invoice_search_ranking { let invoice_partial = create_test_invoice( &env, &business, - "this has 78797a in description", + "this has 78797a0000000000000000000000000000000000000000000000000000000000 in description", None, None, 5000, ); - InvoiceStorage::store_invoice(&env, &invoice_exact); - InvoiceStorage::store_invoice(&env, &invoice_partial); - // Query by the hex string of exact_id - let query = String::from_str(&env, "78797a0000000000000000000000000000000000000000000000000000000000"); - let results = InvoiceSearch::search_invoices(&env, query).unwrap(); + let query = String::from_str(&env, exact_id_query); + let results = with_contract(&env, || { + InvoiceStorage::store_invoice(&env, &invoice_exact); + InvoiceStorage::store_invoice(&env, &invoice_partial); + InvoiceSearch::search_invoices(&env, query).unwrap() + }); assert_eq!(results.len(), 2); // Exact ID must be first, despite having a lower created_at timestamp