Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ You can also use the helper targets from the `Makefile` at the repo root (`make

Shared setup for `creator-keys` integration tests lives in `creator-keys/tests/contract_test_env/`. Import the module with `mod contract_test_env;` and call the small helpers (env with mocked auths, register contract, set key price, set fees, register a test creator) instead of duplicating boilerplate in every file.

For the minimum test categories expected when adding a new contract function (happy path, error cases, state assertions), see [docs/minimum-viable-test-structure.md](./docs/minimum-viable-test-structure.md).

For guidance on writing deterministic quote tests, see [docs/deterministic-quote-tests.md](./docs/deterministic-quote-tests.md).

## Documentation for contributors

- **[CI Contract Checks](./docs/ci-contract-checks.md)**: Understanding the CI pipeline, running checks locally, and troubleshooting failures
- **[Storage Key Invariants](./docs/storage-key-invariants.md)**: Storage model, key structure, and invariants that must be maintained across all operations
- **[Minimum Viable Test Structure](./docs/minimum-viable-test-structure.md)**: Required test categories and example structures for new contract entrypoints
- **[Deterministic Quote Tests](./docs/deterministic-quote-tests.md)**: Guide for writing tests for quote operations with the fixed price model
- **[Quote Math Refactor Guidelines](./docs/quote-math-refactor-guidelines.md)**: Checklist for preserving quote invariants and required regression coverage during quote-path refactors
- **[Fee Assumptions](./docs/fee-assumptions.md)**: Fee split logic, rounding behavior, and integration points
Expand Down
3 changes: 3 additions & 0 deletions creator-keys/tests/contract_test_env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
//! Compose the small functions here instead of one monolithic setup so each test
//! can opt in only to what it needs (pricing without fees, fees, registered creators, etc.).
//!
//! For the minimum test categories and example structures when adding new entrypoints,
//! see `docs/minimum-viable-test-structure.md` in the repo root.
//!
//! Not every integration-test binary uses every helper; this crate is compiled once per
//! `tests/*.rs` target, so we allow dead code at module scope.
#![allow(dead_code)]
Expand Down
139 changes: 139 additions & 0 deletions docs/minimum-viable-test-structure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Minimum Viable Test Structure for New Contract Functions

Contributors adding new contract entrypoints should include a small, consistent set of tests before opening a pull request. This guide defines the minimum categories and shows example structures for read-only and state-changing functions.

For shared setup helpers, see [`creator-keys/tests/contract_test_env/`](../creator-keys/tests/contract_test_env/). For quote-specific coverage, see [deterministic-quote-tests.md](./deterministic-quote-tests.md). For read-only return semantics, see [read-only-methods.md](./read-only-methods.md).

## Minimum test categories

Every new contract function should have tests covering all categories that apply:

| Category | When required | What to verify |
|---|---|---|
| **Happy path** | Always | The function succeeds and returns or stores the expected value under normal preconditions. |
| **Error cases** | When the function can fail | Each documented error variant is returned for the input that triggers it. Use `try_*` client methods and assert on `ContractError`. |
| **State assertions** | Write functions; read functions that must not mutate storage | After a successful call, storage and derived views match expectations. For read-only entrypoints, confirm no storage changed (see [`capture_snapshot`](../creator-keys/tests/contract_test_env/mod.rs)). |

Optional but encouraged when relevant:

- **Authorization**: caller must be the expected admin or account (often covered by existing auth tests; add one if your entrypoint introduces a new auth rule).
- **Idempotency / overwrite**: setting the same value twice or updating an existing value behaves as documented.
- **Boundary inputs**: zero, max length, or config limits that the function explicitly validates.

## Test file placement

- **Integration tests** for public contract entrypoints: `creator-keys/tests/<feature>.rs`
- **Unit tests** for internal helpers: `creator-keys/src/test.rs` or the module's `#[cfg(test)]` block

Use `mod contract_test_env;` and the shared helpers instead of copying env setup boilerplate.

## Naming convention

Name tests so a reviewer can tell what behavior is covered without opening the function body:

- `test_<entrypoint>_<expected_outcome>_<condition>`
- Examples: `test_get_protocol_admin_returns_none_initially`, `test_buy_key_insufficient_payment_fails`

## Example: read-only function

Read-only entrypoints need at least a success case and each documented error path. When the function must not write storage, capture state before and after the call.

```rust
mod contract_test_env;

use contract_test_env::{register_creator_keys, register_test_creator, test_env_with_auths};
use creator_keys::ContractError;
use soroban_sdk::{testutils::Address as _, Address};

#[test]
fn test_get_key_symbol_success() {
// Happy path: registered creator returns stored handle.
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);
let creator = register_test_creator(&env, &client, "alice");

let symbol = client.get_key_symbol(&creator);
assert_eq!(symbol, soroban_sdk::String::from_str(&env, "alice"));
}

#[test]
fn test_get_key_symbol_fails_if_not_registered() {
// Error case: unregistered creator returns NotRegistered.
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);
let creator = Address::generate(&env);

let result = client.try_get_key_symbol(&creator);
assert_eq!(result, Err(Ok(ContractError::NotRegistered)));
}
```

For read-only calls that must not mutate contract state:

```rust
let before = contract_test_env::capture_snapshot(&client, &creator, &holder);
client.get_sell_quote(&creator, &holder); // or your read entrypoint
let after = contract_test_env::capture_snapshot(&client, &creator, &holder);
before.assert_unchanged(&after);
```

## Example: write function

State-changing entrypoints need a happy path with post-condition checks, plus tests for each validation or business-rule failure. Confirm that failed calls do not leave partial state behind when that matters.

```rust
mod contract_test_env;

use contract_test_env::{register_creator_keys, test_env_with_auths};
use creator_keys::ContractError;
use soroban_sdk::{testutils::Address as _, Address, String};

#[test]
fn test_set_protocol_fee_recipient_accepts_valid_address() {
// Happy path: valid input is stored and readable.
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);
let admin = Address::generate(&env);
let recipient = Address::generate(&env);

let result = client.try_set_protocol_fee_recipient(&admin, &recipient);
assert_eq!(result, Ok(Ok(())));

assert_eq!(client.get_protocol_fee_recipient(), Some(recipient));
}

#[test]
fn test_set_protocol_fee_recipient_rejects_zero_address() {
// Error case: invalid input returns the documented error.
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);
let admin = Address::generate(&env);
let zero_str = String::from_str(
&env,
"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
);
let zero_addr = Address::from_string(&zero_str);

let result = client.try_set_protocol_fee_recipient(&admin, &zero_addr);
assert_eq!(result, Err(Ok(ContractError::ZeroAddress)));

// State assertion: rejection must not persist partial updates.
assert_eq!(client.get_protocol_fee_recipient(), None);
}
```

For operations that change balances or supply, assert the observable views your callers rely on (for example `get_key_balance`, `get_total_key_supply`, or `get_creator`) in the same test as the happy path.

## Checklist before opening a PR

- [ ] Happy-path test exists for the new entrypoint.
- [ ] Each new or reachable `ContractError` variant has a dedicated test (or is covered by an existing shared test file).
- [ ] Write paths assert stored state and views after success; failure paths assert no unintended state when applicable.
- [ ] Read-only paths assert no storage mutation when the contract guarantees it.
- [ ] Tests use `contract_test_env` helpers where possible and run under `cargo test --workspace`.

## Related docs

- [CONTRIBUTING.md](../CONTRIBUTING.md) — verification commands and contribution rules
- [error-codes.md](./error-codes.md) — error variants to cover in failure tests
- [read-only-methods.md](./read-only-methods.md) — expected return values and edge cases for `get_*` / `is_*` entrypoints
Loading