diff --git a/contracts/test-utils/Cargo.toml b/contracts/test-utils/Cargo.toml new file mode 100644 index 00000000..e204ef6a --- /dev/null +++ b/contracts/test-utils/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "test-utils" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["rlib"] + +[dependencies] +soroban-sdk = "22.0.8" + +[dev-dependencies] +bounty-escrow = { path = "../bounty_escrow/contracts/escrow" } diff --git a/contracts/test-utils/README.md b/contracts/test-utils/README.md new file mode 100644 index 00000000..6b00633e --- /dev/null +++ b/contracts/test-utils/README.md @@ -0,0 +1,198 @@ +# Test Utilities Library + +Comprehensive testing utilities for Soroban smart contract development. This library provides reusable helpers, factories, and assertion utilities to simplify test development and reduce boilerplate code. + +## Features + +- **Contract Factories**: Easy creation of test contracts (escrow, token) +- **Test Setup Helpers**: Comprehensive `TestSetup` struct with all common components +- **Assertion Utilities**: Common assertions for escrow status, amounts, balances, etc. +- **Test Data Generators**: Generate test data (addresses, amounts, deadlines, etc.) +- **Time Manipulation**: Helpers for advancing time and creating deadlines +- **Balance Verification**: Utilities for checking and verifying token balances + +## Installation + +Add to your `Cargo.toml`: + +```toml +[dev-dependencies] +test-utils = { path = "../test-utils" } +bounty-escrow = { path = "../bounty_escrow/contracts/escrow" } +``` + +## Usage Examples + +### Basic Test Setup + +```rust +use test_utils::TestSetup; +use test_utils::assertions::*; +use test_utils::time::*; +use bounty_escrow::EscrowStatus; + +#[test] +fn test_lock_funds() { + let setup = TestSetup::new(); + let bounty_id = 1; + let amount = 1000; + let deadline = future_deadline(&setup.env, Some(3600)); + + setup.lock_funds(bounty_id, amount, deadline); + + assert_escrow_status(&setup.escrow, bounty_id, EscrowStatus::Locked); + assert_escrow_amount(&setup.escrow, bounty_id, amount); +} +``` + +### Using Generators + +```rust +use test_utils::generators::*; +use test_utils::TestSetup; + +#[test] +fn test_with_generated_data() { + let setup = TestSetup::new(); + let bounty_id = generate_bounty_id(None); + let amount = generate_amount(1000, Some(10)); // 10000 + let deadline = generate_deadline(&setup.env, Some(86400)); + + setup.lock_funds(bounty_id, amount, deadline); +} +``` + +### Time Manipulation + +```rust +use test_utils::time::*; +use test_utils::TestSetup; + +#[test] +fn test_deadline_passed() { + let setup = TestSetup::new(); + let deadline = past_deadline(&setup.env, Some(100)); + + setup.lock_funds(1, 1000, deadline); + + // Advance time to pass deadline + advance_time(&setup.env, 200); + + // Now refund should be allowed +} +``` + +### Balance Verification + +```rust +use test_utils::balances::*; +use test_utils::TestSetup; + +#[test] +fn test_balance_changes() { + let setup = TestSetup::new(); + let initial = get_initial_balance(&setup.token, &setup.contributor); + + setup.lock_funds(1, 1000, future_deadline(&setup.env, None)); + setup.release_funds(1, None); + + verify_balance_change(&setup.token, &setup.contributor, initial, 1000); +} +``` + +### Multiple Contributors + +```rust +use test_utils::TestSetup; + +#[test] +fn test_multiple_contributors() { + let (setup, contributors) = TestSetup::with_contributors(3); + + setup.lock_funds(1, 3000, future_deadline(&setup.env, None)); + + // Release to each contributor + for contributor in contributors.iter() { + setup.escrow.release_funds(&1, contributor); + } +} +``` + +### Custom Mint Amount + +```rust +use test_utils::TestSetup; + +#[test] +fn test_custom_mint() { + let setup = TestSetup::with_mint_amount(5_000_000); + // Depositor now has 5,000,000 tokens +} +``` + +## Module Reference + +### `factories` + +- `create_token_contract(env, admin)` - Create a token contract +- `create_escrow_contract(env)` - Create an escrow contract +- `create_initialized_escrow(env, admin)` - Create fully initialized escrow with token + +### `setup` + +- `TestSetup::new()` - Create standard test setup +- `TestSetup::with_mint_amount(amount)` - Create setup with custom mint amount +- `TestSetup::with_contributors(count)` - Create setup with multiple contributors +- `TestSetup::lock_funds(bounty_id, amount, deadline)` - Convenience method +- `TestSetup::release_funds(bounty_id, contributor)` - Convenience method + +### `assertions` + +- `assert_escrow_status(escrow, bounty_id, status)` - Assert escrow status +- `assert_escrow_amount(escrow, bounty_id, amount)` - Assert escrow amount +- `assert_escrow_depositor(escrow, bounty_id, depositor)` - Assert depositor +- `assert_escrow_deadline(escrow, bounty_id, deadline)` - Assert deadline +- `assert_balance(token, address, balance)` - Assert token balance +- `assert_balances(token, expected_balances)` - Assert multiple balances +- `assert_escrow_exists(escrow, bounty_id)` - Assert escrow exists + +### `generators` + +- `generate_bounty_id(index)` - Generate bounty ID +- `generate_amount(base, multiplier)` - Generate amount +- `generate_deadline(env, offset_seconds)` - Generate deadline +- `generate_addresses(env, count)` - Generate multiple addresses +- `standard_amount()` - Standard test amount (1000) +- `large_amount()` - Large test amount (1,000,000) +- `small_amount()` - Small test amount (100) + +### `time` + +- `advance_time(env, seconds)` - Advance ledger timestamp +- `set_time(env, timestamp)` - Set ledger timestamp +- `current_time(env)` - Get current timestamp +- `past_deadline(env, seconds_ago)` - Create past deadline +- `future_deadline(env, seconds_from_now)` - Create future deadline + +### `balances` + +- `get_initial_balance(token, address)` - Get initial balance +- `verify_balance_change(token, address, initial, expected_change)` - Verify balance change +- `verify_all_zero(token, addresses)` - Verify all addresses have zero balance + +## Best Practices + +1. **Use TestSetup for comprehensive tests**: It provides all commonly needed components +2. **Use generators for test data**: Makes tests more readable and maintainable +3. **Use assertion utilities**: Provides better error messages and reduces boilerplate +4. **Use time helpers**: Makes time-based tests more reliable +5. **Verify balances**: Always verify balances after transactions + +## Contributing + +When adding new utilities: + +1. Add to the appropriate module +2. Include comprehensive documentation +3. Add usage examples +4. Update this README diff --git a/contracts/test-utils/examples/test_example.rs b/contracts/test-utils/examples/test_example.rs new file mode 100644 index 00000000..44db5875 --- /dev/null +++ b/contracts/test-utils/examples/test_example.rs @@ -0,0 +1,91 @@ +//! Example test file demonstrating usage of test-utils library. +//! +//! This file shows how to use the test utilities to write cleaner, more maintainable tests. + +#![cfg(test)] + +use test_utils::*; +use bounty_escrow::EscrowStatus; + +#[test] +fn example_basic_test() { + // Create a test setup with all components initialized + let setup = TestSetup::new(); + + // Use generators for test data + let bounty_id = generate_bounty_id(None); + let amount = standard_amount(); + let deadline = future_deadline(&setup.env, Some(3600)); + + // Lock funds using convenience method + setup.lock_funds(bounty_id, amount, deadline); + + // Use assertion utilities + assert_escrow_status(&setup.escrow, bounty_id, EscrowStatus::Locked); + assert_escrow_amount(&setup.escrow, bounty_id, amount); + assert_escrow_depositor(&setup.escrow, bounty_id, &setup.depositor); +} + +#[test] +fn example_balance_verification() { + let setup = TestSetup::new(); + let bounty_id = 1; + let amount = 1000; + let deadline = future_deadline(&setup.env, None); + + // Get initial balances + let initial_contributor = get_initial_balance(&setup.token, &setup.contributor); + let initial_escrow = get_initial_balance(&setup.token, &setup.escrow_address); + + // Lock funds + setup.lock_funds(bounty_id, amount, deadline); + + // Verify escrow received funds + verify_balance_change(&setup.token, &setup.escrow_address, initial_escrow, amount); + + // Release funds + setup.release_funds(bounty_id, None); + + // Verify contributor received funds + verify_balance_change(&setup.token, &setup.contributor, initial_contributor, amount); +} + +#[test] +fn example_multiple_contributors() { + let (setup, contributors) = TestSetup::with_contributors(3); + let bounty_id = 1; + let total_amount = 3000; + let deadline = future_deadline(&setup.env, None); + + setup.lock_funds(bounty_id, total_amount, deadline); + + // Release to each contributor + for contributor in contributors.iter() { + setup.escrow.release_funds(&bounty_id, contributor); + } +} + +#[test] +fn example_time_manipulation() { + let setup = TestSetup::new(); + let bounty_id = 1; + let amount = 1000; + + // Create a deadline in the past + let past_deadline = past_deadline(&setup.env, Some(100)); + setup.lock_funds(bounty_id, amount, past_deadline); + + // Advance time + advance_time(&setup.env, 200); + + // Now refund should be allowed + // (This would require refund functionality to be tested) +} + +#[test] +fn example_custom_mint_amount() { + let setup = TestSetup::with_mint_amount(5_000_000); + + // Depositor now has 5,000,000 tokens + assert_balance(&setup.token, &setup.depositor, 5_000_000); +} diff --git a/contracts/test-utils/src/assertions.rs b/contracts/test-utils/src/assertions.rs new file mode 100644 index 00000000..c1740113 --- /dev/null +++ b/contracts/test-utils/src/assertions.rs @@ -0,0 +1,174 @@ +//! Assertion utilities for common test scenarios. +//! +//! Provides helper functions for common assertions in contract tests. + +use soroban_sdk::{token, Address}; + +#[cfg(test)] +use bounty_escrow::{BountyEscrowContractClient, EscrowStatus}; + +#[cfg(test)] +/// Asserts that an escrow has the expected status. +/// +/// # Arguments +/// * `escrow_client` - The escrow contract client +/// * `bounty_id` - The bounty ID +/// * `expected_status` - The expected escrow status +/// +/// # Panics +/// Panics if the escrow status doesn't match the expected status. +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::Env; +/// # use bounty_escrow::{BountyEscrowContractClient, EscrowStatus}; +/// # use test_utils::assertions::assert_escrow_status; +/// # let env = Env::default(); +/// # let escrow = BountyEscrowContractClient::new(&env, &Address::generate(&env)); +/// assert_escrow_status(&escrow, 1, EscrowStatus::Locked); +/// ``` +pub fn assert_escrow_status( + escrow_client: &BountyEscrowContractClient, + bounty_id: u64, + expected_status: EscrowStatus, +) { + let escrow = escrow_client.get_escrow_info(&bounty_id); + assert_eq!( + escrow.status, expected_status, + "Expected escrow {} to have status {:?}, but got {:?}", + bounty_id, expected_status, escrow.status + ); +} + +#[cfg(test)] +/// Asserts that an escrow has the expected amount. +/// +/// # Arguments +/// * `escrow_client` - The escrow contract client +/// * `bounty_id` - The bounty ID +/// * `expected_amount` - The expected amount +/// +/// # Panics +/// Panics if the escrow amount doesn't match the expected amount. +pub fn assert_escrow_amount( + escrow_client: &BountyEscrowContractClient, + bounty_id: u64, + expected_amount: i128, +) { + let escrow = escrow_client.get_escrow_info(&bounty_id); + assert_eq!( + escrow.amount, expected_amount, + "Expected escrow {} to have amount {}, but got {}", + bounty_id, expected_amount, escrow.amount + ); +} + +/// Asserts that a token balance matches the expected value. +/// +/// # Arguments +/// * `token_client` - The token client +/// * `address` - The address to check +/// * `expected_balance` - The expected balance +/// +/// # Panics +/// Panics if the balance doesn't match the expected value. +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::{token, Address, Env}; +/// # use test_utils::assertions::assert_balance; +/// # let env = Env::default(); +/// # let token = token::Client::new(&env, &Address::generate(&env)); +/// # let addr = Address::generate(&env); +/// assert_balance(&token, &addr, 1000); +/// ``` +pub fn assert_balance(token_client: &token::Client, address: &Address, expected_balance: i128) { + let balance = token_client.balance(address); + assert_eq!( + balance, expected_balance, + "Expected address {:?} to have balance {}, but got {}", + address, expected_balance, balance + ); +} + +#[cfg(test)] +/// Asserts that an escrow exists. +/// +/// # Arguments +/// * `escrow_client` - The escrow contract client +/// * `bounty_id` - The bounty ID +/// +/// # Panics +/// Panics if the escrow doesn't exist (will panic on get_escrow_info). +pub fn assert_escrow_exists(escrow_client: &BountyEscrowContractClient, bounty_id: u64) { + let _escrow = escrow_client.get_escrow_info(&bounty_id); + // If we get here, the escrow exists +} + +/// Asserts that balances match expected values after a transaction. +/// +/// # Arguments +/// * `token_client` - The token client +/// * `expected_balances` - A slice of (address, expected_balance) tuples +/// +/// # Panics +/// Panics if any balance doesn't match the expected value. +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::{token, Address, Env}; +/// # use test_utils::assertions::assert_balances; +/// # let env = Env::default(); +/// # let token = token::Client::new(&env, &Address::generate(&env)); +/// # let addr1 = Address::generate(&env); +/// # let addr2 = Address::generate(&env); +/// assert_balances(&token, &[(&addr1, 1000), (&addr2, 500)]); +/// ``` +pub fn assert_balances( + token_client: &token::Client, + expected_balances: &[(&Address, i128)], +) { + for (address, expected_balance) in expected_balances { + assert_balance(token_client, address, *expected_balance); + } +} + +#[cfg(test)] +/// Asserts that an escrow has the expected depositor. +/// +/// # Arguments +/// * `escrow_client` - The escrow contract client +/// * `bounty_id` - The bounty ID +/// * `expected_depositor` - The expected depositor address +pub fn assert_escrow_depositor( + escrow_client: &BountyEscrowContractClient, + bounty_id: u64, + expected_depositor: &Address, +) { + let escrow = escrow_client.get_escrow_info(&bounty_id); + assert_eq!( + escrow.depositor, *expected_depositor, + "Expected escrow {} to have depositor {:?}, but got {:?}", + bounty_id, expected_depositor, escrow.depositor + ); +} + +#[cfg(test)] +/// Asserts that an escrow has the expected deadline. +/// +/// # Arguments +/// * `escrow_client` - The escrow contract client +/// * `bounty_id` - The bounty ID +/// * `expected_deadline` - The expected deadline timestamp +pub fn assert_escrow_deadline( + escrow_client: &BountyEscrowContractClient, + bounty_id: u64, + expected_deadline: u64, +) { + let escrow = escrow_client.get_escrow_info(&bounty_id); + assert_eq!( + escrow.deadline, expected_deadline, + "Expected escrow {} to have deadline {}, but got {}", + bounty_id, expected_deadline, escrow.deadline + ); +} diff --git a/contracts/test-utils/src/balances.rs b/contracts/test-utils/src/balances.rs new file mode 100644 index 00000000..f0a51ee9 --- /dev/null +++ b/contracts/test-utils/src/balances.rs @@ -0,0 +1,103 @@ +//! Balance verification helpers. +//! +//! Provides functions to verify token balances in tests. + +use soroban_sdk::{token, Address}; + +/// Verifies that a balance change occurred. +/// +/// # Arguments +/// * `token_client` - The token client +/// * `address` - The address to check +/// * `initial_balance` - The initial balance +/// * `expected_change` - The expected change (positive or negative) +/// +/// # Returns +/// The new balance (i128) +/// +/// # Panics +/// Panics if the balance change doesn't match the expected change. +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::{token, Address, Env}; +/// # use test_utils::balances::verify_balance_change; +/// # let env = Env::default(); +/// # let token = token::Client::new(&env, &Address::generate(&env)); +/// # let addr = Address::generate(&env); +/// let initial = token.balance(&addr); +/// // ... perform transaction ... +/// let new_balance = verify_balance_change(&token, &addr, initial, 1000); +/// ``` +pub fn verify_balance_change( + token_client: &token::Client, + address: &Address, + initial_balance: i128, + expected_change: i128, +) -> i128 { + let new_balance = token_client.balance(address); + let actual_change = new_balance - initial_balance; + + assert_eq!( + actual_change, expected_change, + "Expected balance change of {} for address {:?}, but got {} (initial: {}, new: {})", + expected_change, address, actual_change, initial_balance, new_balance + ); + + new_balance +} + +/// Gets the initial balance before a transaction. +/// +/// # Arguments +/// * `token_client` - The token client +/// * `address` - The address to check +/// +/// # Returns +/// The initial balance (i128) +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::{token, Address, Env}; +/// # use test_utils::balances::get_initial_balance; +/// # let env = Env::default(); +/// # let token = token::Client::new(&env, &Address::generate(&env)); +/// # let addr = Address::generate(&env); +/// let initial = get_initial_balance(&token, &addr); +/// // ... perform transaction ... +/// let new = token.balance(&addr); +/// assert_eq!(new - initial, 1000); +/// ``` +pub fn get_initial_balance(token_client: &token::Client, address: &Address) -> i128 { + token_client.balance(address) +} + +/// Verifies that balances are zero for multiple addresses. +/// +/// # Arguments +/// * `token_client` - The token client +/// * `addresses` - A slice of addresses to check +/// +/// # Panics +/// Panics if any address has a non-zero balance. +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::{token, Address, Env}; +/// # use test_utils::balances::verify_all_zero; +/// # let env = Env::default(); +/// # let token = token::Client::new(&env, &Address::generate(&env)); +/// # let addr1 = Address::generate(&env); +/// # let addr2 = Address::generate(&env); +/// verify_all_zero(&token, &[&addr1, &addr2]); +/// ``` +pub fn verify_all_zero(token_client: &token::Client, addresses: &[&Address]) { + for address in addresses { + let balance = token_client.balance(address); + assert_eq!( + balance, 0, + "Expected address {:?} to have zero balance, but got {}", + address, balance + ); + } +} diff --git a/contracts/test-utils/src/factories.rs b/contracts/test-utils/src/factories.rs new file mode 100644 index 00000000..802681ab --- /dev/null +++ b/contracts/test-utils/src/factories.rs @@ -0,0 +1,106 @@ +//! Contract factory functions for creating test contracts. +//! +//! These functions simplify the creation of contracts and tokens for testing. + +use soroban_sdk::{token, Address, Env}; + +/// Creates a token contract for testing. +/// +/// # Arguments +/// * `env` - The contract environment +/// * `admin` - The admin address for the token +/// +/// # Returns +/// A tuple containing: +/// - Token address +/// - Token client +/// - Token admin client +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::{Address, Env}; +/// # use test_utils::factories::create_token_contract; +/// # let env = Env::default(); +/// # let admin = Address::generate(&env); +/// let (token_address, token_client, token_admin) = create_token_contract(&env, &admin); +/// ``` +pub fn create_token_contract<'a>( + env: &'a Env, + admin: &Address, +) -> (Address, token::Client<'a>, token::StellarAssetClient<'a>) { + let token_id = env.register_stellar_asset_contract_v2(admin.clone()); + let token = token_id.address(); + let token_client = token::Client::new(env, &token); + let token_admin_client = token::StellarAssetClient::new(env, &token); + (token, token_client, token_admin_client) +} + +#[cfg(test)] +/// Creates an escrow contract for testing. +/// +/// # Arguments +/// * `env` - The contract environment +/// +/// # Returns +/// A tuple containing: +/// - Escrow contract client +/// - Escrow contract address +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::Env; +/// # use test_utils::factories::create_escrow_contract; +/// # let env = Env::default(); +/// let (escrow_client, escrow_address) = create_escrow_contract(&env); +/// ``` +pub fn create_escrow_contract<'a>( + env: &Env, +) -> (bounty_escrow::BountyEscrowContractClient<'a>, Address) { + use bounty_escrow::{BountyEscrowContract, BountyEscrowContractClient}; + let contract_id = env.register_contract(None, BountyEscrowContract); + let client = BountyEscrowContractClient::new(env, &contract_id); + (client, contract_id) +} + +#[cfg(test)] +/// Creates a fully initialized escrow contract with token. +/// +/// # Arguments +/// * `env` - The contract environment +/// * `admin` - The admin address +/// +/// # Returns +/// A tuple containing: +/// - Escrow contract client +/// - Escrow contract address +/// - Token address +/// - Token client +/// - Token admin client +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::{Address, Env}; +/// # use test_utils::factories::create_initialized_escrow; +/// # let env = Env::default(); +/// # let admin = Address::generate(&env); +/// let (escrow, escrow_addr, token_addr, token, token_admin) = +/// create_initialized_escrow(&env, &admin); +/// ``` +pub fn create_initialized_escrow<'a>( + env: &'a Env, + admin: &Address, +) -> ( + bounty_escrow::BountyEscrowContractClient<'a>, + Address, + Address, + token::Client<'a>, + token::StellarAssetClient<'a>, +) { + use bounty_escrow::BountyEscrowContractClient; + let (escrow, escrow_address) = create_escrow_contract(env); + let (token_address, token_client, token_admin_client) = create_token_contract(env, admin); + + escrow.init(admin, &token_address); + + (escrow, escrow_address, token_address, token_client, token_admin_client) +} diff --git a/contracts/test-utils/src/generators.rs b/contracts/test-utils/src/generators.rs new file mode 100644 index 00000000..3d131f5a --- /dev/null +++ b/contracts/test-utils/src/generators.rs @@ -0,0 +1,108 @@ +//! Test data generators. +//! +//! Provides functions to generate common test data values. + +use soroban_sdk::{Address, Env}; + +/// Generates a test bounty ID. +/// +/// # Arguments +/// * `index` - Optional index for generating different IDs (defaults to 1) +/// +/// # Returns +/// A bounty ID (u64) +/// +/// # Example +/// ```rust,no_run +/// # use test_utils::generators::generate_bounty_id; +/// let bounty_id = generate_bounty_id(None); +/// ``` +pub fn generate_bounty_id(index: Option) -> u64 { + index.unwrap_or(1) +} + +/// Generates a test amount. +/// +/// # Arguments +/// * `base` - Base amount (defaults to 1000) +/// * `multiplier` - Optional multiplier +/// +/// # Returns +/// An amount (i128) +/// +/// # Example +/// ```rust,no_run +/// # use test_utils::generators::generate_amount; +/// let amount = generate_amount(1000, Some(10)); // Returns 10000 +/// ``` +pub fn generate_amount(base: i128, multiplier: Option) -> i128 { + base * multiplier.unwrap_or(1) +} + +/// Generates a deadline timestamp. +/// +/// # Arguments +/// * `env` - The contract environment +/// * `offset_seconds` - Offset in seconds from current time (defaults to 1000) +/// +/// # Returns +/// A deadline timestamp (u64) +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::Env; +/// # use test_utils::generators::generate_deadline; +/// # let env = Env::default(); +/// let deadline = generate_deadline(&env, Some(3600)); // 1 hour from now +/// ``` +pub fn generate_deadline(env: &Env, offset_seconds: Option) -> u64 { + env.ledger().timestamp() + offset_seconds.unwrap_or(1000) +} + +/// Generates multiple addresses. +/// +/// # Arguments +/// * `env` - The contract environment +/// * `count` - Number of addresses to generate +/// +/// # Returns +/// A vector of addresses +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::Env; +/// # use test_utils::generators::generate_addresses; +/// # let env = Env::default(); +/// let addresses = generate_addresses(&env, 5); +/// ``` +pub fn generate_addresses(env: &Env, count: u32) -> Vec
{ + let mut addresses = Vec::new(env); + for _ in 0..count { + addresses.push_back(Address::generate(env)); + } + addresses.to_array() +} + +/// Generates a standard test amount (1000). +/// +/// # Returns +/// A standard test amount (i128) +pub fn standard_amount() -> i128 { + 1000 +} + +/// Generates a large test amount (1,000,000). +/// +/// # Returns +/// A large test amount (i128) +pub fn large_amount() -> i128 { + 1_000_000 +} + +/// Generates a small test amount (100). +/// +/// # Returns +/// A small test amount (i128) +pub fn small_amount() -> i128 { + 100 +} diff --git a/contracts/test-utils/src/lib.rs b/contracts/test-utils/src/lib.rs new file mode 100644 index 00000000..da6cc2aa --- /dev/null +++ b/contracts/test-utils/src/lib.rs @@ -0,0 +1,32 @@ +//! # Test Utilities Library +//! +//! Comprehensive testing utilities for Soroban smart contract development. +//! This library provides reusable helpers, factories, and assertion utilities +//! to simplify test development and reduce boilerplate code. +//! +//! ## Modules +//! +//! - [`factories`] - Contract factory functions for creating test contracts +//! - [`setup`] - Test setup helpers and TestSetup struct +//! - [`assertions`] - Assertion utilities for common test scenarios +//! - [`generators`] - Test data generators +//! - [`time`] - Time manipulation helpers +//! - [`balances`] - Balance verification helpers + +pub mod factories; +pub mod setup; +pub mod assertions; +pub mod generators; +pub mod time; +pub mod balances; + +// Re-export commonly used items (only in test context) +#[cfg(test)] +pub use factories::*; +#[cfg(test)] +pub use setup::*; +#[cfg(test)] +pub use assertions::*; +pub use generators::*; +pub use time::*; +pub use balances::*; diff --git a/contracts/test-utils/src/setup.rs b/contracts/test-utils/src/setup.rs new file mode 100644 index 00000000..a74e71a4 --- /dev/null +++ b/contracts/test-utils/src/setup.rs @@ -0,0 +1,188 @@ +//! Test setup helpers and TestSetup struct. +//! +//! Provides a comprehensive test setup structure that includes all commonly +//! needed components for testing escrow contracts. + +use soroban_sdk::{token, Address, Env}; +use super::factories::create_token_contract; + +#[cfg(test)] +use bounty_escrow::BountyEscrowContractClient; +#[cfg(test)] +use super::factories::create_escrow_contract; + +/// Comprehensive test setup structure. +/// +/// This struct contains all commonly needed components for testing: +/// - Environment +/// - Admin, depositor, and contributor addresses +/// - Token contract and admin client +/// - Escrow contract client and address +/// +/// # Example +/// ```rust,no_run +/// # use test_utils::setup::TestSetup; +/// let setup = TestSetup::new(); +/// setup.escrow.lock_funds(&setup.depositor, &1, &1000, &10000); +/// ``` +#[cfg(test)] +pub struct TestSetup<'a> { + pub env: Env, + pub admin: Address, + pub depositor: Address, + pub contributor: Address, + pub token: token::Client<'a>, + pub token_admin: token::StellarAssetClient<'a>, + pub escrow: BountyEscrowContractClient<'a>, + pub escrow_address: Address, + pub token_address: Address, +} + +#[cfg(test)] +impl<'a> TestSetup<'a> { + /// Creates a new test setup with all components initialized. + /// + /// This will: + /// - Create a new environment + /// - Mock all auths + /// - Generate admin, depositor, and contributor addresses + /// - Create and initialize token contract + /// - Create and initialize escrow contract + /// - Mint tokens to depositor (1,000,000 by default) + /// + /// # Example + /// ```rust,no_run + /// # use test_utils::setup::TestSetup; + /// let setup = TestSetup::new(); + /// ``` + pub fn new() -> Self { + Self::with_mint_amount(1_000_000) + } + + /// Creates a new test setup with a custom mint amount. + /// + /// # Arguments + /// * `mint_amount` - Amount to mint to the depositor + /// + /// # Example + /// ```rust,no_run + /// # use test_utils::setup::TestSetup; + /// let setup = TestSetup::with_mint_amount(5_000_000); + /// ``` + pub fn with_mint_amount(mint_amount: i128) -> Self { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let depositor = Address::generate(&env); + let contributor = Address::generate(&env); + + let (token_address, token, token_admin) = create_token_contract(&env, &admin); + let (escrow, escrow_address) = create_escrow_contract(&env); + + escrow.init(&admin, &token_address); + + // Mint tokens to depositor + token_admin.mint(&depositor, &mint_amount); + + Self { + env, + admin, + depositor, + contributor, + token, + token_admin, + escrow, + escrow_address, + token_address, + } + } + + /// Creates a test setup with multiple contributors. + /// + /// # Arguments + /// * `contributor_count` - Number of contributors to generate + /// + /// # Returns + /// A tuple containing the setup and a vector of contributor addresses + /// + /// # Example + /// ```rust,no_run + /// # use test_utils::setup::TestSetup; + /// let (setup, contributors) = TestSetup::with_contributors(3); + /// ``` + pub fn with_contributors(contributor_count: u32) -> (Self, Vec
) { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let depositor = Address::generate(&env); + + let mut contributors = Vec::new(&env); + for _ in 0..contributor_count { + contributors.push_back(Address::generate(&env)); + } + + let (token_address, token, token_admin) = create_token_contract(&env, &admin); + let (escrow, escrow_address) = create_escrow_contract(&env); + + escrow.init(&admin, &token_address); + + // Mint tokens to depositor + token_admin.mint(&depositor, &1_000_000); + + let first_contributor = contributors.get(0).unwrap().clone(); + let contributors_array = contributors.to_array(); + + let setup = Self { + env, + admin, + depositor, + contributor: first_contributor, + token, + token_admin, + escrow, + escrow_address, + token_address, + }; + + (setup, contributors_array) + } + + /// Locks funds for a bounty (convenience method). + /// + /// # Arguments + /// * `bounty_id` - The bounty ID + /// * `amount` - The amount to lock + /// * `deadline` - The deadline timestamp + /// + /// # Example + /// ```rust,no_run + /// # use test_utils::setup::TestSetup; + /// # let setup = TestSetup::new(); + /// let deadline = setup.env.ledger().timestamp() + 1000; + /// setup.lock_funds(1, 1000, deadline); + /// ``` + pub fn lock_funds(&self, bounty_id: u64, amount: i128, deadline: u64) { + self.escrow.lock_funds(&self.depositor, &bounty_id, &amount, &deadline); + } + + /// Releases funds for a bounty (convenience method). + /// + /// # Arguments + /// * `bounty_id` - The bounty ID + /// * `contributor` - The contributor address (defaults to self.contributor) + /// + /// # Example + /// ```rust,no_run + /// # use test_utils::setup::TestSetup; + /// # let setup = TestSetup::new(); + /// # let deadline = setup.env.ledger().timestamp() + 1000; + /// # setup.lock_funds(1, 1000, deadline); + /// setup.release_funds(1, None); + /// ``` + pub fn release_funds(&self, bounty_id: u64, contributor: Option<&Address>) { + let contributor_addr = contributor.unwrap_or(&self.contributor); + self.escrow.release_funds(&bounty_id, contributor_addr); + } +} diff --git a/contracts/test-utils/src/time.rs b/contracts/test-utils/src/time.rs new file mode 100644 index 00000000..4a38403a --- /dev/null +++ b/contracts/test-utils/src/time.rs @@ -0,0 +1,100 @@ +//! Time manipulation helpers. +//! +//! Provides functions to manipulate time in tests. + +use soroban_sdk::Env; + +/// Gets the current ledger timestamp. +/// +/// # Arguments +/// * `env` - The contract environment +/// +/// # Returns +/// The current timestamp (u64) +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::Env; +/// # use test_utils::time::current_time; +/// # let env = Env::default(); +/// let now = current_time(&env); +/// ``` +pub fn current_time(env: &Env) -> u64 { + env.ledger().timestamp() +} + +/// Advances the ledger timestamp by the specified number of seconds. +/// +/// # Arguments +/// * `env` - The contract environment +/// * `seconds` - Number of seconds to advance +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::Env; +/// # use test_utils::time::advance_time; +/// # let env = Env::default(); +/// advance_time(&env, 3600); // Advance by 1 hour +/// ``` +pub fn advance_time(env: &Env, seconds: u64) { + let current = env.ledger().timestamp(); + env.ledger().set_timestamp(current + seconds); +} + +/// Sets the ledger timestamp to a specific value. +/// +/// # Arguments +/// * `env` - The contract environment +/// * `timestamp` - The timestamp to set +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::Env; +/// # use test_utils::time::set_time; +/// # let env = Env::default(); +/// set_time(&env, 1000000); +/// ``` +pub fn set_time(env: &Env, timestamp: u64) { + env.ledger().set_timestamp(timestamp); +} + +/// Creates a deadline that is in the past. +/// +/// # Arguments +/// * `env` - The contract environment +/// * `seconds_ago` - How many seconds in the past (defaults to 100) +/// +/// # Returns +/// A timestamp in the past (u64) +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::Env; +/// # use test_utils::time::past_deadline; +/// # let env = Env::default(); +/// let deadline = past_deadline(&env, Some(3600)); // 1 hour ago +/// ``` +pub fn past_deadline(env: &Env, seconds_ago: Option) -> u64 { + let current = env.ledger().timestamp(); + current.saturating_sub(seconds_ago.unwrap_or(100)) +} + +/// Creates a deadline that is in the future. +/// +/// # Arguments +/// * `env` - The contract environment +/// * `seconds_from_now` - How many seconds in the future (defaults to 1000) +/// +/// # Returns +/// A timestamp in the future (u64) +/// +/// # Example +/// ```rust,no_run +/// # use soroban_sdk::Env; +/// # use test_utils::time::future_deadline; +/// # let env = Env::default(); +/// let deadline = future_deadline(&env, Some(86400)); // 1 day from now +/// ``` +pub fn future_deadline(env: &Env, seconds_from_now: Option) -> u64 { + env.ledger().timestamp() + seconds_from_now.unwrap_or(1000) +}