From bb50d89797ec95cc543f098b57e7efe949131267 Mon Sep 17 00:00:00 2001 From: belloaliyu11 Date: Tue, 25 Aug 2026 10:16:53 +0100 Subject: [PATCH] test: add escrow security coverage --- .github/workflows/ci.yml | 14 +++ contracts/contracts/escrow/src/test.rs | 141 +++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65998a0..b3566e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,3 +39,17 @@ jobs: # during the build. No real DB connection is made at build time. DATABASE_URL: ${{ secrets.DATABASE_URL || 'postgres://build:build@localhost/build' }} JWT_SECRET: ${{ secrets.JWT_SECRET || 'build-time-placeholder-secret-32chars!!' }} + + contracts: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Run Soroban Contract Tests + working-directory: contracts + run: cargo test --workspace diff --git a/contracts/contracts/escrow/src/test.rs b/contracts/contracts/escrow/src/test.rs index 86fdeab..fa05762 100644 --- a/contracts/contracts/escrow/src/test.rs +++ b/contracts/contracts/escrow/src/test.rs @@ -51,6 +51,37 @@ fn setup_test() -> TestSetup { } } +fn milestone(env: &Env, id: u32, amount: i128) -> Milestone { + Milestone { + id, + deadline: 0, + amount, + status: MilestoneStatus::Pending, + description: String::from_str(env, "Security milestone"), + client_approved: false, + freelancer_approved: false, + } +} + +fn initialize_single_milestone(setup: &TestSetup, amount: i128) { + let milestones = vec![&setup.env, milestone(&setup.env, 1, amount)]; + setup.escrow_client.initialize( + &setup.admin, + &setup.client, + &setup.freelancer, + &setup.arbiter, + &setup.token_address, + &milestones, + ); +} + +fn fully_approve_single_milestone(setup: &TestSetup) { + setup.escrow_client.fund(); + setup.escrow_client.submit_milestone(&1); + setup.escrow_client.approve(&1); + setup.escrow_client.freelancer_confirm(&1); +} + #[test] fn test_happy_path() { let setup = setup_test(); @@ -439,3 +470,113 @@ fn test_dispute_clears_approvals() { assert_eq!(escrow.has_client_approval(&1), false); assert_eq!(escrow.has_freelancer_approval(&1), false); } + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #7)")] +fn test_unauthorized_refund_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + setup.escrow_client.fund(); + + setup.escrow_client.refund(&1, &setup.client); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #7)")] +fn test_unauthorized_dispute_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + setup.escrow_client.fund(); + + let stranger = Address::generate(&setup.env); + setup.escrow_client.dispute(&1, &stranger); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #5)")] +fn test_submit_invalid_milestone_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + setup.escrow_client.fund(); + + setup.escrow_client.submit_milestone(&99); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #5)")] +fn test_release_invalid_milestone_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + fully_approve_single_milestone(&setup); + + setup.escrow_client.release(&99, &setup.client); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #6)")] +fn test_double_release_replay_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + fully_approve_single_milestone(&setup); + setup.escrow_client.release(&1, &setup.client); + + setup.escrow_client.release(&1, &setup.client); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #6)")] +fn test_double_refund_replay_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + setup.escrow_client.fund(); + setup.escrow_client.refund(&1, &setup.freelancer); + + setup.escrow_client.refund(&1, &setup.freelancer); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #6)")] +fn test_dispute_after_release_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + fully_approve_single_milestone(&setup); + setup.escrow_client.release(&1, &setup.freelancer); + + setup.escrow_client.dispute(&1, &setup.client); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #6)")] +fn test_refund_after_release_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + fully_approve_single_milestone(&setup); + setup.escrow_client.release(&1, &setup.freelancer); + + setup.escrow_client.refund(&1, &setup.freelancer); +} + +#[test] +fn test_successful_security_events_are_emitted() { + let setup = setup_test(); + let env = setup.env.clone(); + let initial_event_count = env.events().all().len(); + + initialize_single_milestone(&setup, 150); + assert_eq!(env.events().all().len(), initial_event_count + 1); + + setup.escrow_client.fund(); + assert_eq!(env.events().all().len(), initial_event_count + 2); + + setup.escrow_client.submit_milestone(&1); + assert_eq!(env.events().all().len(), initial_event_count + 3); + + setup.escrow_client.approve(&1); + assert_eq!(env.events().all().len(), initial_event_count + 4); + + setup.escrow_client.freelancer_confirm(&1); + assert_eq!(env.events().all().len(), initial_event_count + 5); + + setup.escrow_client.release(&1, &setup.client); + assert_eq!(env.events().all().len(), initial_event_count + 6); +}