From ff4cbff71df6f946caa4222f37064f0095a4a96c Mon Sep 17 00:00:00 2001 From: Nzube polycap Date: Sat, 29 Aug 2026 13:28:30 +0000 Subject: [PATCH] feature:Add a mutation-testing setup for compliance and invoice, extending #94's treasury-only coverage --- .github/workflows/test.yml | 52 ++++++++++ Cargo.lock | 8 ++ contracts/compliance/src/lib.rs | 3 +- .../tests/invoice_invalid_amount_test.rs | 96 +++++++++++++++++-- contracts/invoice/tests/invoice_test.rs | 41 +++++--- contracts/treasury/src/lib.rs | 1 + .../tests/compliance_gate_integration_test.rs | 6 +- .../treasury/tests/duplicate_approval_test.rs | 2 +- .../treasury/tests/partial_settlement_test.rs | 2 +- .../tests/settlement_pagination_test.rs | 2 +- .../tests/settlement_result_types_test.rs | 2 +- contracts/treasury/tests/task1_tests.rs | 4 +- .../treasury/tests/threshold_update_test.rs | 2 +- ...reasury_deposit_withdraw_roundtrip_test.rs | 2 +- contracts/treasury/tests/treasury_test.rs | 2 +- .../treasury_threshold_rejection_test.rs | 2 +- .../tests/unauthorized_signer_test.rs | 4 +- crates/compliance-client/Cargo.toml | 2 +- tests/tests/full_lifecycle_smoke_test.rs | 1 - .../invoice_treasury_integration_test.rs | 6 +- 20 files changed, 195 insertions(+), 45 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1cc47d3..b138ef0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,7 @@ on: branches: [main] pull_request: branches: [main] + workflow_dispatch: env: CARGO_TERM_COLOR: always @@ -23,3 +24,54 @@ jobs: - name: Run tests run: cargo test --workspace + + mutants-treasury: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.95.0" + + - name: Install cargo-mutants + run: cargo install cargo-mutants --locked + + - name: Run mutation tests for treasury settlement + run: cargo mutants --package comebackhere-treasury --file contracts/treasury/src/settlement.rs + + mutants-compliance: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.95.0" + + - name: Install cargo-mutants + run: cargo install cargo-mutants --locked + + - name: Run mutation tests for compliance allowlist + run: cargo mutants --package comebackhere-compliance --file contracts/compliance/src/allowlist.rs + + mutants-invoice: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.95.0" + + - name: Install cargo-mutants + run: cargo install cargo-mutants --locked + + - name: Run mutation tests for invoice status guards + run: cargo mutants --package comebackhere-invoice --file contracts/invoice/src/invoice.rs diff --git a/Cargo.lock b/Cargo.lock index 9026f1e..0fc6bd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,6 +155,14 @@ dependencies = [ "soroban-sdk", ] +[[package]] +name = "comebackhere-compliance-client" +version = "0.1.0" +dependencies = [ + "comebackhere-compliance", + "soroban-sdk", +] + [[package]] name = "comebackhere-invoice" version = "1.0.0" diff --git a/contracts/compliance/src/lib.rs b/contracts/compliance/src/lib.rs index e00b23b..50d4c27 100644 --- a/contracts/compliance/src/lib.rs +++ b/contracts/compliance/src/lib.rs @@ -128,7 +128,7 @@ impl ComplianceContract { reason: Option, ) -> Result<(), ContractError> { Self::require_admin(&env, &admin)?; - let was_blocked: bool = env + let _was_blocked: bool = env .storage() .persistent() .get(&DataKey::Blocked(address.clone())) @@ -494,6 +494,7 @@ impl ComplianceContract { } /// Compute the current [`AddressState`] for a single address without auth. + #[allow(dead_code)] fn address_state(env: &Env, addr: &Address) -> AddressState { let blocked: bool = env .storage() diff --git a/contracts/invoice/tests/invoice_invalid_amount_test.rs b/contracts/invoice/tests/invoice_invalid_amount_test.rs index 3c3b987..505d4b7 100644 --- a/contracts/invoice/tests/invoice_invalid_amount_test.rs +++ b/contracts/invoice/tests/invoice_invalid_amount_test.rs @@ -1,4 +1,4 @@ -use invoice::{InvoiceContract, InvoiceContractClient}; +use invoice::{InvoiceContract, InvoiceContractClient, MaybeBytes}; use soroban_sdk::{testutils::Address as _, Address, Env}; fn setup() -> (Env, Address, InvoiceContractClient<'static>) { @@ -15,7 +15,17 @@ fn setup() -> (Env, Address, InvoiceContractClient<'static>) { fn test_zero_amount_rejected() { let (env, _, client) = setup(); let merchant = Address::generate(&env); - assert!(client.try_create_invoice(&merchant, &0, &0, &3600).is_err()); + assert!(client + .try_create_invoice( + &merchant, + &0, + &0, + &3600, + &MaybeBytes::None, + &MaybeBytes::None, + &0, + ) + .is_err()); } #[test] @@ -23,7 +33,15 @@ fn test_negative_amount_rejected() { let (env, _, client) = setup(); let merchant = Address::generate(&env); assert!(client - .try_create_invoice(&merchant, &-1, &-1, &3600) + .try_create_invoice( + &merchant, + &-1, + &-1, + &3600, + &MaybeBytes::None, + &MaybeBytes::None, + &0, + ) .is_err()); } @@ -32,7 +50,15 @@ fn test_large_negative_amount_rejected() { let (env, _, client) = setup(); let merchant = Address::generate(&env); assert!(client - .try_create_invoice(&merchant, &i128::MIN, &i128::MIN, &3600) + .try_create_invoice( + &merchant, + &i128::MIN, + &i128::MIN, + &3600, + &MaybeBytes::None, + &MaybeBytes::None, + &0, + ) .is_err()); } @@ -42,7 +68,15 @@ fn test_gross_less_than_amount_rejected() { let (env, _, client) = setup(); let merchant = Address::generate(&env); assert!(client - .try_create_invoice(&merchant, &10_000_000, &9_999_999, &3600) + .try_create_invoice( + &merchant, + &10_000_000, + &9_999_999, + &3600, + &MaybeBytes::None, + &MaybeBytes::None, + &0, + ) .is_err()); } @@ -51,7 +85,15 @@ fn test_zero_gross_with_positive_amount_rejected() { let (env, _, client) = setup(); let merchant = Address::generate(&env); assert!(client - .try_create_invoice(&merchant, &10_000_000, &0, &3600) + .try_create_invoice( + &merchant, + &10_000_000, + &0, + &3600, + &MaybeBytes::None, + &MaybeBytes::None, + &0, + ) .is_err()); } @@ -60,7 +102,15 @@ fn test_negative_gross_rejected() { let (env, _, client) = setup(); let merchant = Address::generate(&env); assert!(client - .try_create_invoice(&merchant, &10_000_000, &-1, &3600) + .try_create_invoice( + &merchant, + &10_000_000, + &-1, + &3600, + &MaybeBytes::None, + &MaybeBytes::None, + &0, + ) .is_err()); } @@ -68,7 +118,17 @@ fn test_negative_gross_rejected() { fn test_amount_one_gross_zero_rejected() { let (env, _, client) = setup(); let merchant = Address::generate(&env); - assert!(client.try_create_invoice(&merchant, &1, &0, &3600).is_err()); + assert!(client + .try_create_invoice( + &merchant, + &1, + &0, + &3600, + &MaybeBytes::None, + &MaybeBytes::None, + &0, + ) + .is_err()); } #[test] @@ -100,7 +160,15 @@ fn test_amount_matrix() { let client = InvoiceContractClient::new(&env, &contract_id); client.initialize(&admin); let merchant = Address::generate(&env); - let result = client.try_create_invoice(&merchant, &amount, &gross, &3600); + let result = client.try_create_invoice( + &merchant, + &amount, + &gross, + &3600, + &MaybeBytes::None, + &MaybeBytes::None, + &0, + ); if expect_valid { assert!( result.is_ok(), @@ -120,6 +188,14 @@ fn test_overflow_amount_i128_max_accepted() { // i128::MAX is a valid positive amount when gross == amount let (env, _, client) = setup(); let merchant = Address::generate(&env); - let result = client.try_create_invoice(&merchant, &i128::MAX, &i128::MAX, &3600); + let result = client.try_create_invoice( + &merchant, + &i128::MAX, + &i128::MAX, + &3600, + &MaybeBytes::None, + &MaybeBytes::None, + &0, + ); assert!(result.is_ok()); } diff --git a/contracts/invoice/tests/invoice_test.rs b/contracts/invoice/tests/invoice_test.rs index aaae3e8..d782e62 100644 --- a/contracts/invoice/tests/invoice_test.rs +++ b/contracts/invoice/tests/invoice_test.rs @@ -448,6 +448,20 @@ fn test_release_escrow_transitions_paid_to_released() { let (env, admin, client) = setup(); let merchant = Address::generate(&env); let payer = Address::generate(&env); + let id = client.create_invoice( + &merchant, + &10_000_000, + &10_250_000, + &3600, + &MaybeBytes::None, + &MaybeBytes::None, + &0, + ); + client.mark_paid(&admin, &id, &payer); + client.release_escrow(&admin, &id); + assert_eq!(client.get_invoice(&id).status, InvoiceStatus::Released); +} + #[test] fn test_cancel_invoice_transitions_to_cancelled() { let (env, _admin, client) = setup(); @@ -459,6 +473,7 @@ fn test_cancel_invoice_transitions_to_cancelled() { &3600, &MaybeBytes::None, &MaybeBytes::None, + &0, ); client.cancel_invoice(&merchant, &invoice_id); @@ -483,6 +498,7 @@ fn test_cancelled_invoice_cannot_be_marked_paid() { &3600, &MaybeBytes::None, &MaybeBytes::None, + &0, ); client.cancel_invoice(&merchant, &invoice_id); @@ -498,9 +514,10 @@ fn test_cancelled_invoice_cannot_be_marked_paid() { #[test] fn test_cancel_invoice_unauthorized_rejected() { - let (env, _admin, client) = setup(); + let (env, admin, client) = setup(); let merchant = Address::generate(&env); let unauthorized = Address::generate(&env); + let payer = Address::generate(&env); let id = client.create_invoice( &merchant, &10_000_000, @@ -513,6 +530,15 @@ fn test_cancel_invoice_unauthorized_rejected() { client.mark_paid(&admin, &id, &payer); client.release_escrow(&admin, &id); assert_eq!(client.get_invoice(&id).status, InvoiceStatus::Released); + + let err = client + .try_cancel_invoice(&unauthorized, &id) + .unwrap_err() + .unwrap(); + assert_eq!(err, InvoiceError::Unauthorized); + + let invoice = client.get_invoice(&id); + assert_eq!(invoice.status, InvoiceStatus::Released); } #[test] @@ -551,19 +577,6 @@ fn test_release_escrow_requires_admin() { assert!(client.try_release_escrow(&rogue, &id).is_err()); } -// ABI snapshot comparison - ); - - let err = client - .try_cancel_invoice(&unauthorized, &id) - .unwrap_err() - .unwrap(); - assert_eq!(err, InvoiceError::Unauthorized); - - let invoice = client.get_invoice(&id); - assert_eq!(invoice.status, InvoiceStatus::Pending); -} - // ABI snapshot comparison: asserts abis/invoice.json stays in sync with the // contract's public surface. Run via `cargo test` or `make check-abi-snapshots`. #[test] diff --git a/contracts/treasury/src/lib.rs b/contracts/treasury/src/lib.rs index a74e57c..f0a5b91 100644 --- a/contracts/treasury/src/lib.rs +++ b/contracts/treasury/src/lib.rs @@ -12,6 +12,7 @@ use settlement::{require_authorized_signer, signer_weight}; use soroban_sdk::{contract, contractimpl, token, Address, Env, Symbol, Vec}; impl TreasuryError { + #[allow(dead_code)] fn panic(&self) -> ! { match self { TreasuryError::AlreadyInitialized => panic!("AlreadyInitialized"), diff --git a/contracts/treasury/tests/compliance_gate_integration_test.rs b/contracts/treasury/tests/compliance_gate_integration_test.rs index 64c5ced..e785d77 100644 --- a/contracts/treasury/tests/compliance_gate_integration_test.rs +++ b/contracts/treasury/tests/compliance_gate_integration_test.rs @@ -213,7 +213,7 @@ fn settlement_rejected_when_merchant_blocked() { // Then explicitly block — block_address is permitted while paused or unpaused // and overrides any existing allow status. - compliance.block_address(&admin, &merchant); + compliance.block_address(&admin, &merchant, &None); let settlement_id = treasury.propose_settlement(&admin, &merchant, &10_000_000); @@ -263,7 +263,7 @@ fn blocked_merchant_rejected_even_when_already_settled() { .is_ok()); // Now block the merchant and create a *new* settlement. - compliance.block_address(&admin, &merchant); + compliance.block_address(&admin, &merchant, &None); let settlement_id_2 = treasury.propose_settlement(&admin, &merchant, &5_000_000); // Mint additional tokens @@ -297,7 +297,7 @@ fn execute_settlement_fails_when_merchant_blocked_mid_flight() { token.mint(&treasury_id, &10_000_000); // Block the merchant after the settlement is already proposed (mid-flight). - compliance.block_address(&admin, &merchant); + compliance.block_address(&admin, &merchant, &None); let workflow_id = env.register_contract(None, SettlementWorkflow); let workflow = SettlementWorkflowClient::new(&env, &workflow_id); diff --git a/contracts/treasury/tests/duplicate_approval_test.rs b/contracts/treasury/tests/duplicate_approval_test.rs index bff17a7..229daa0 100644 --- a/contracts/treasury/tests/duplicate_approval_test.rs +++ b/contracts/treasury/tests/duplicate_approval_test.rs @@ -1,7 +1,7 @@ use soroban_sdk::{testutils::Address as _, Address, Env}; use treasury::{SettlementStatus, TreasuryContract, TreasuryContractClient}; -fn setup(env: &Env) -> (TreasuryContractClient, Address, u64) { +fn setup(env: &Env) -> (TreasuryContractClient<'_>, Address, u64) { let admin = Address::generate(env); let merchant = Address::generate(env); let contract_id = env.register_contract(None, TreasuryContract); diff --git a/contracts/treasury/tests/partial_settlement_test.rs b/contracts/treasury/tests/partial_settlement_test.rs index 7829989..2cb3d1f 100644 --- a/contracts/treasury/tests/partial_settlement_test.rs +++ b/contracts/treasury/tests/partial_settlement_test.rs @@ -1,7 +1,7 @@ use soroban_sdk::{testutils::Address as _, Address, Env}; use treasury::{SettlementStatus, TreasuryContract, TreasuryContractClient}; -fn setup(env: &Env, total: i128) -> (TreasuryContractClient, Address, Address, u64) { +fn setup(env: &Env, total: i128) -> (TreasuryContractClient<'_>, Address, Address, u64) { let admin = Address::generate(env); let merchant = Address::generate(env); let contract_id = env.register_contract(None, TreasuryContract); diff --git a/contracts/treasury/tests/settlement_pagination_test.rs b/contracts/treasury/tests/settlement_pagination_test.rs index fd6d571..170b5b0 100644 --- a/contracts/treasury/tests/settlement_pagination_test.rs +++ b/contracts/treasury/tests/settlement_pagination_test.rs @@ -1,7 +1,7 @@ use soroban_sdk::{testutils::Address as _, Address, Env}; use treasury::{TreasuryContract, TreasuryContractClient}; -fn setup_with_settlements(env: &Env, n: u64) -> (TreasuryContractClient, Address) { +fn setup_with_settlements(env: &Env, n: u64) -> (TreasuryContractClient<'_>, Address) { let admin = Address::generate(env); let contract_id = env.register_contract(None, TreasuryContract); let client = TreasuryContractClient::new(env, &contract_id); diff --git a/contracts/treasury/tests/settlement_result_types_test.rs b/contracts/treasury/tests/settlement_result_types_test.rs index b920377..9ed6a22 100644 --- a/contracts/treasury/tests/settlement_result_types_test.rs +++ b/contracts/treasury/tests/settlement_result_types_test.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contract, contractimpl, testutils::Address as _, Address, Env}; +use soroban_sdk::{testutils::Address as _, Address, Env}; use treasury::{TreasuryContract, TreasuryContractClient, SettlementStatus}; mod test_token { diff --git a/contracts/treasury/tests/task1_tests.rs b/contracts/treasury/tests/task1_tests.rs index dde30fc..62b41af 100644 --- a/contracts/treasury/tests/task1_tests.rs +++ b/contracts/treasury/tests/task1_tests.rs @@ -8,9 +8,9 @@ use soroban_sdk::{ testutils::{Address as _, Events}, Address, Env, FromVal, Symbol, }; -use treasury::{SettlementStatus, TreasuryContract, TreasuryContractClient}; +use treasury::{TreasuryContract, TreasuryContractClient}; -fn setup(env: &Env, threshold: u32) -> (TreasuryContractClient, Address, Address) { +fn setup(env: &Env, threshold: u32) -> (TreasuryContractClient<'_>, Address, Address) { env.mock_all_auths(); let admin = Address::generate(env); let id = env.register_contract(None, TreasuryContract); diff --git a/contracts/treasury/tests/threshold_update_test.rs b/contracts/treasury/tests/threshold_update_test.rs index 7afc306..d0d358e 100644 --- a/contracts/treasury/tests/threshold_update_test.rs +++ b/contracts/treasury/tests/threshold_update_test.rs @@ -1,7 +1,7 @@ use soroban_sdk::{testutils::Address as _, Address, Env}; use treasury::{TreasuryContract, TreasuryContractClient}; -fn setup(env: &Env, threshold: u32) -> (TreasuryContractClient, Address) { +fn setup(env: &Env, threshold: u32) -> (TreasuryContractClient<'_>, Address) { let admin = Address::generate(env); let contract_id = env.register_contract(None, TreasuryContract); let client = TreasuryContractClient::new(env, &contract_id); diff --git a/contracts/treasury/tests/treasury_deposit_withdraw_roundtrip_test.rs b/contracts/treasury/tests/treasury_deposit_withdraw_roundtrip_test.rs index 3f60fbd..83b27b9 100644 --- a/contracts/treasury/tests/treasury_deposit_withdraw_roundtrip_test.rs +++ b/contracts/treasury/tests/treasury_deposit_withdraw_roundtrip_test.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contract, contractimpl, testutils::Address as _, Address, Env}; +use soroban_sdk::{testutils::Address as _, Address, Env}; use treasury::{TreasuryContract, TreasuryContractClient}; mod test_token { diff --git a/contracts/treasury/tests/treasury_test.rs b/contracts/treasury/tests/treasury_test.rs index eb0fa6e..dc8f67d 100644 --- a/contracts/treasury/tests/treasury_test.rs +++ b/contracts/treasury/tests/treasury_test.rs @@ -1,7 +1,7 @@ use soroban_sdk::{testutils::Address as _, Address, Env}; use treasury::{SettlementStatus, TreasuryContract, TreasuryContractClient}; -fn setup(env: &Env, threshold: u32) -> (TreasuryContractClient, Address, Address) { +fn setup(env: &Env, threshold: u32) -> (TreasuryContractClient<'_>, Address, Address) { env.mock_all_auths(); let admin = Address::generate(env); let id = env.register_contract(None, TreasuryContract); diff --git a/contracts/treasury/tests/treasury_threshold_rejection_test.rs b/contracts/treasury/tests/treasury_threshold_rejection_test.rs index d1713bf..3c49b99 100644 --- a/contracts/treasury/tests/treasury_threshold_rejection_test.rs +++ b/contracts/treasury/tests/treasury_threshold_rejection_test.rs @@ -1,7 +1,7 @@ use soroban_sdk::{testutils::Address as _, Address, Env}; use treasury::{SettlementStatus, TreasuryContract, TreasuryContractClient}; -fn setup(env: &Env, threshold: u32) -> (TreasuryContractClient, Address, Address, Address) { +fn setup(env: &Env, threshold: u32) -> (TreasuryContractClient<'_>, Address, Address, Address) { let admin = Address::generate(env); let merchant = Address::generate(env); let contract_id = env.register_contract(None, TreasuryContract); diff --git a/contracts/treasury/tests/unauthorized_signer_test.rs b/contracts/treasury/tests/unauthorized_signer_test.rs index 66f2d03..dec19d2 100644 --- a/contracts/treasury/tests/unauthorized_signer_test.rs +++ b/contracts/treasury/tests/unauthorized_signer_test.rs @@ -1,7 +1,7 @@ use soroban_sdk::{testutils::Address as _, Address, Env}; use treasury::{TreasuryContract, TreasuryContractClient}; -fn setup_treasury(env: &Env, threshold: u32) -> (TreasuryContractClient, Address, Address) { +fn setup_treasury(env: &Env, threshold: u32) -> (TreasuryContractClient<'_>, Address, Address) { env.mock_all_auths(); let admin = Address::generate(env); let contract_id = env.register_contract(None, TreasuryContract); @@ -62,7 +62,7 @@ fn unauthorized_signer_cannot_approve_partial_settlement() { #[should_panic(expected = "UnauthorizedSigner")] fn unauthorized_signer_cannot_execute_settlement() { let env = Env::default(); - let (client, admin, contract_id) = setup_treasury(&env, 1); + let (client, admin, _contract_id) = setup_treasury(&env, 1); // Create and approve a settlement let merchant = Address::generate(&env); diff --git a/crates/compliance-client/Cargo.toml b/crates/compliance-client/Cargo.toml index 382cb3c..e70c06a 100644 --- a/crates/compliance-client/Cargo.toml +++ b/crates/compliance-client/Cargo.toml @@ -11,7 +11,7 @@ name = "compliance_client" [dependencies] soroban-sdk.workspace = true +compliance = { package = "comebackhere-compliance", path = "../../contracts/compliance" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } -compliance = { package = "comebackhere-compliance", path = "../../contracts/compliance" } diff --git a/tests/tests/full_lifecycle_smoke_test.rs b/tests/tests/full_lifecycle_smoke_test.rs index ebe50c6..96437eb 100644 --- a/tests/tests/full_lifecycle_smoke_test.rs +++ b/tests/tests/full_lifecycle_smoke_test.rs @@ -64,7 +64,6 @@ impl ComplianceGatedSettlement { } } -use compliance_gated_settlement::{ComplianceGatedSettlement, ComplianceGatedSettlementClient}; struct TestContext { _env: Env, diff --git a/tests/tests/invoice_treasury_integration_test.rs b/tests/tests/invoice_treasury_integration_test.rs index f28c91c..93631e7 100644 --- a/tests/tests/invoice_treasury_integration_test.rs +++ b/tests/tests/invoice_treasury_integration_test.rs @@ -1,7 +1,7 @@ use invoice::{ - InvoiceContract, InvoiceContractClient, InvoiceError, InvoiceStatus, MaybeAddress, MaybeBytes, + InvoiceContract, InvoiceContractClient, InvoiceStatus, MaybeAddress, MaybeBytes, }; -use soroban_sdk::{contract, contractimpl, testutils::Address as _, Address, Env}; +use soroban_sdk::{testutils::Address as _, Address, Env}; use treasury::{TreasuryContract, TreasuryContractClient}; mod test_token { @@ -70,7 +70,7 @@ fn setup() -> ( #[test] fn invoice_created_paid_released() { - let (env, admin, merchant, payer, invoice, _treasury_id, _treasury, _token_id) = setup(); + let (_env, admin, merchant, payer, invoice, _treasury_id, _treasury, _token_id) = setup(); let id = invoice.create_invoice( &merchant,