diff --git a/.github/workflows/soroban.yml b/.github/workflows/soroban.yml index 0cf262d7..3c2fcc31 100644 --- a/.github/workflows/soroban.yml +++ b/.github/workflows/soroban.yml @@ -115,7 +115,40 @@ jobs: manifest-path: quantara/soroban/contracts/Cargo.toml arguments: --config quantara/soroban/contracts/deny.toml - # ── 4. Build ──────────────────────────────────────────────────────────────── + # ── 4. Test ───────────────────────────────────────────────────────────────── + test: + name: Test (cargo test) + runs-on: ubuntu-latest + permissions: + contents: read + needs: [fmt, clippy] + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + targets: wasm32-unknown-unknown + + - name: Cache Cargo registry & build artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + quantara/soroban/contracts/target/ + key: ${{ runner.os }}-cargo-test-${{ hashFiles('quantara/soroban/contracts/Cargo.toml', 'quantara/soroban/contracts/**/Cargo.toml') }} + restore-keys: | + ${{ runner.os }}-cargo-test- + + - name: Run tests + run: cargo test + + # ── 5. Build ──────────────────────────────────────────────────────────────── build: name: Build (wasm32-unknown-unknown) runs-on: ubuntu-latest diff --git a/quantara/soroban/contracts/Cargo.lock b/quantara/soroban/contracts/Cargo.lock index ecd6ec96..92914a56 100644 --- a/quantara/soroban/contracts/Cargo.lock +++ b/quantara/soroban/contracts/Cargo.lock @@ -969,6 +969,8 @@ name = "looping" version = "0.1.0" dependencies = [ "common", + "ed25519-dalek", + "rand_core 0.6.4", "soroban-sdk", ] @@ -1857,6 +1859,8 @@ name = "vault" version = "0.1.0" dependencies = [ "common", + "ed25519-dalek", + "rand_core 0.6.4", "soroban-sdk", ] diff --git a/quantara/soroban/contracts/looping/Cargo.toml b/quantara/soroban/contracts/looping/Cargo.toml index 8571895d..a2bea3e7 100644 --- a/quantara/soroban/contracts/looping/Cargo.toml +++ b/quantara/soroban/contracts/looping/Cargo.toml @@ -12,3 +12,8 @@ doctest = false [dependencies] soroban-sdk = { version = "22.0.0" } common = { path = "../common", version = "0.1.0" } + +[dev-dependencies] +soroban-sdk = { version = "22.0.0", features = ["testutils"] } +ed25519-dalek = "<3" +rand_core = "<0.7" diff --git a/quantara/soroban/contracts/looping/src/lib.rs b/quantara/soroban/contracts/looping/src/lib.rs index fa276e4c..d5dd72a2 100644 --- a/quantara/soroban/contracts/looping/src/lib.rs +++ b/quantara/soroban/contracts/looping/src/lib.rs @@ -58,3 +58,116 @@ impl LoopingContract { // Stub: full unwind logic will be implemented in a future PR. } } + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + extern crate std; + + use super::*; + use soroban_sdk::testutils::Address as _; + + struct Fixture { + env: Env, + contract_id: Address, + user: Address, + } + + fn setup() -> Fixture { + let env = Env::default(); + env.mock_all_auths(); + + let user = Address::generate(&env); + let contract_id = env.register(LoopingContract, ()); + + Fixture { + env, + contract_id, + user, + } + } + + #[test] + fn test_open_position_returns_first_id() { + let fx = setup(); + let client = LoopingContractClient::new(&fx.env, &fx.contract_id); + + let id = client.open_position(&fx.user, &1_000, &200); + assert_eq!(id, 1); + } + + #[test] + fn test_open_position_returns_incrementing_ids() { + let fx = setup(); + let client = LoopingContractClient::new(&fx.env, &fx.contract_id); + + assert_eq!(client.open_position(&fx.user, &1_000, &200), 1); + assert_eq!(client.open_position(&fx.user, &2_000, &300), 2); + assert_eq!(client.open_position(&fx.user, &500, &100), 3); + } + + #[test] + fn test_open_position_rejects_zero_collateral() { + let fx = setup(); + let client = LoopingContractClient::new(&fx.env, &fx.contract_id); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + client.open_position(&fx.user, &0, &200); + })); + assert!(result.is_err()); + } + + #[test] + fn test_open_position_rejects_negative_collateral() { + let fx = setup(); + let client = LoopingContractClient::new(&fx.env, &fx.contract_id); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + client.open_position(&fx.user, &-1, &200); + })); + assert!(result.is_err()); + } + + #[test] + fn test_open_position_rejects_leverage_below_100() { + let fx = setup(); + let client = LoopingContractClient::new(&fx.env, &fx.contract_id); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + client.open_position(&fx.user, &1_000, &99); + })); + assert!(result.is_err()); + } + + #[test] + fn test_open_position_rejects_leverage_above_500() { + let fx = setup(); + let client = LoopingContractClient::new(&fx.env, &fx.contract_id); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + client.open_position(&fx.user, &1_000, &501); + })); + assert!(result.is_err()); + } + + #[test] + fn test_open_position_accepts_leverage_boundaries() { + let fx = setup(); + let client = LoopingContractClient::new(&fx.env, &fx.contract_id); + + assert_eq!(client.open_position(&fx.user, &1_000, &100), 1); + assert_eq!(client.open_position(&fx.user, &1_000, &500), 2); + } + + #[test] + fn test_close_position_does_not_panic() { + let fx = setup(); + let client = LoopingContractClient::new(&fx.env, &fx.contract_id); + + let id = client.open_position(&fx.user, &1_000, &200); + client.close_position(&fx.user, &id); + } +} diff --git a/quantara/soroban/contracts/rewards/src/lib.rs b/quantara/soroban/contracts/rewards/src/lib.rs index 0600bf1c..9f6d5581 100644 --- a/quantara/soroban/contracts/rewards/src/lib.rs +++ b/quantara/soroban/contracts/rewards/src/lib.rs @@ -150,7 +150,7 @@ mod tests { let init = MockAuthInvoke { contract: &contract_id, fn_name: "initialize", - args: vec![&env], + args: vec![&env, symbol_short!("init").to_val()], sub_invokes: &[], }; env.mock_auths(&[MockAuth { @@ -186,4 +186,58 @@ mod tests { let res = client.try_accrue(&user, &1_i128); assert!(res.is_err(), "overflow must surface an error"); } + + #[test] + fn test_claim_on_zero_balance_returns_zero() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let contract_id = env.register(RewardsContract, ()); + let client = RewardsContractClient::new(&env, &contract_id); + + client.initialize(&admin); + + let claimed = client.claim(&user); + assert_eq!(claimed, 0); + } + + #[test] + fn test_pending_rewards_query_without_claiming() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let contract_id = env.register(RewardsContract, ()); + let client = RewardsContractClient::new(&env, &contract_id); + + client.initialize(&admin); + client.accrue(&user, &500_i128); + let pending = client.pending_rewards(&user); + + assert_eq!(pending, 500); + assert_eq!(client.pending_rewards(&user), 500); + } + + #[test] + fn test_multiple_accrue_then_claim() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let contract_id = env.register(RewardsContract, ()); + let client = RewardsContractClient::new(&env, &contract_id); + + client.initialize(&admin); + client.accrue(&user, &100_i128); + client.accrue(&user, &200_i128); + client.accrue(&user, &50_i128); + + assert_eq!(client.pending_rewards(&user), 350); + assert_eq!(client.claim(&user), 350); + assert_eq!(client.pending_rewards(&user), 0); + } }