This document provides guidance for contributors writing tests for quote operations (buy and sell quotes) in Access Layer contracts.
Quote tests verify that the contract returns consistent, predictable pricing and fee information. These tests are critical for ensuring users see accurate prices and that the pricing model behaves correctly under various conditions.
Determinism means a test produces the same result every time it runs, regardless of:
- Test execution order
- System time or random values
- Previous test state
For quote operations, determinism ensures:
- Users see consistent prices for the same market conditions
- Fee calculations are reproducible
- Pricing doesn't drift due to rounding errors
- Integration tests catch regressions reliably
#[test]
fn test_quote_behavior_description() {
// 1. Setup: Create test environment
let env = test_env_with_auths();
let (client, creator) = setup_with_fees(&env, price);
// 2. Execute: Get quote or perform operation
let quote = client.get_buy_quote(&creator);
// 3. Assert: Verify expected behavior
assert_eq!(quote.price, expected_price);
assert_eq!(quote.total_amount, expected_total);
}Use descriptive names that explain what is being tested:
Good names:
test_buy_quote_is_identical_across_consecutive_callstest_buy_quote_monotonic_with_zero_protocol_feetest_buy_quote_stable_across_50_sequential_purchases
Bad names:
test_quote(too vague)test_1(meaningless)test_buy(unclear what aspect is tested)
Access Layer uses a fixed price model:
- Global price: All creators share the same key price (stored in
KeyPricestorage key) - Fixed per key: Each key costs the same amount, regardless of supply
- Fees on top: Fees are calculated as a percentage of the price and added to the total
- Price stability: The price should never change unless an admin explicitly calls
set_key_price - Quote consistency: Multiple calls to
get_buy_quoteshould return identical results - Supply independence: Buying or selling keys should not affect the quote price
- Creator independence: All creators have the same price (but independent supplies)
For the fixed price model, sell quotes are expected to be stable across incremental sells until the holder runs out of keys:
- Calling
get_sell_quoterepeatedly for the same(creator, holder)should return the sameQuoteResponseas long asget_key_balance(creator, holder) > 0. - Once the holder balance reaches
0,get_sell_quoteshould reject withContractError::InsufficientBalance. - If fee configuration and price would imply a negative sell payout (fees exceed price),
get_sell_quoteshould reject withContractError::SellUnderflowrather than returning an invalid negative amount.
Understanding fee rules is essential for writing correct tests:
From fee::validate_fee_bps:
- Sum must equal 10,000:
creator_bps + protocol_bps == 10000(representing 100%) - Protocol max is 50%:
protocol_bps <= 5000(5,000 basis points)
| creator_bps | protocol_bps | Valid? | Notes |
|---|---|---|---|
| 9000 | 1000 | ✅ | 90% creator, 10% protocol |
| 5000 | 5000 | ✅ | 50/50 split (max protocol) |
| 10000 | 0 | ✅ | 100% creator, 0% protocol |
| 0 | 10000 | ❌ | Protocol exceeds 50% max |
| 9000 | 0 | ❌ | Sum is 9000, not 10000 |
| 8000 | 2000 | ✅ | 80% creator, 20% protocol |
Always test fee edge cases:
#[test]
fn test_buy_quote_with_max_protocol_fee() {
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);
// 50/50 split is the maximum allowed protocol fee
set_pricing_and_fees(&env, &client, 1000, 5000, 5000);
let creator = register_test_creator(&env, &client, "alice");
let quote = client.get_buy_quote(&creator);
assert_eq!(quote.creator_fee, quote.protocol_fee, "fees should be equal");
}
#[test]
fn test_buy_quote_with_zero_protocol_fee() {
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);
// 100% creator, 0% protocol
set_pricing_and_fees(&env, &client, 1000, 10000, 0);
let creator = register_test_creator(&env, &client, "bob");
let quote = client.get_buy_quote(&creator);
assert_eq!(quote.protocol_fee, 0, "protocol fee should be zero");
}The QuoteResponse struct contains:
pub struct QuoteResponse {
pub price: i128, // Base key price
pub creator_fee: i128, // Fee going to creator
pub protocol_fee: i128, // Fee going to protocol
pub total_amount: i128, // Total cost (buy) or net proceeds (sell)
}total_amount = price + creator_fee + protocol_fee
Where:
creator_fee = floor(price * creator_bps / 10000)protocol_fee = floor(price * protocol_bps / 10000)- Remainder from rounding goes to creator
total_amount = price - creator_fee - protocol_fee
Seller receives total_amount (price minus fees).
Verify that quotes remain unchanged after buy/sell operations:
#[test]
fn test_buy_quote_unchanged_after_purchase() {
let env = test_env_with_auths();
let (client, creator) = setup_with_fees(&env, 1000);
let quote_before = client.get_buy_quote(&creator);
// Perform a buy operation
let buyer = Address::generate(&env);
client.buy_key(&creator, &buyer, "e_before.total_amount);
let quote_after = client.get_buy_quote(&creator);
// Quote should be identical
assert_eq!(quote_before.price, quote_after.price);
assert_eq!(quote_before.total_amount, quote_after.total_amount);
}Verify that fees sum correctly:
#[test]
fn test_buy_quote_fees_sum_correctly() {
let env = test_env_with_auths();
let (client, creator) = setup_with_fees(&env, 1000);
let quote = client.get_buy_quote(&creator);
// For buy quotes: total = price + fees
assert_eq!(
quote.total_amount,
quote.price + quote.creator_fee + quote.protocol_fee,
"total must equal price plus all fees"
);
// Fees must be non-negative
assert!(quote.creator_fee >= 0);
assert!(quote.protocol_fee >= 0);
}Verify that quotes remain stable across multiple purchases:
#[test]
fn test_buy_quote_stable_across_multiple_purchases() {
let env = test_env_with_auths();
let (client, creator) = setup_with_fees(&env, 500);
let initial_quote = client.get_buy_quote(&creator);
// Perform multiple purchases
for i in 0..10 {
let buyer = Address::generate(&env);
client.buy_key(&creator, &buyer, &initial_quote.total_amount);
let current_quote = client.get_buy_quote(&creator);
assert_eq!(
current_quote.price,
initial_quote.price,
"price must remain constant after {} purchases",
i + 1
);
}
}Verify that creators maintain independent state despite sharing global price:
#[test]
fn test_quotes_independent_across_creators() {
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);
let price = 1000;
set_pricing_and_fees(&env, &client, price, 8000, 2000);
let creator_alice = register_test_creator(&env, &client, "alice");
let creator_bob = register_test_creator(&env, &client, "bob");
// Both should have same quote (global price)
let quote_alice = client.get_buy_quote(&creator_alice);
let quote_bob = client.get_buy_quote(&creator_bob);
assert_eq!(quote_alice.price, quote_bob.price);
// Buy from alice
let buyer = Address::generate(&env);
client.buy_key(&creator_alice, &buyer, "e_alice.total_amount);
// Bob's quote should be unchanged
let quote_bob_after = client.get_buy_quote(&creator_bob);
assert_eq!(quote_bob.price, quote_bob_after.price);
// But supplies should be independent
assert_eq!(client.get_total_key_supply(&creator_alice), 1);
assert_eq!(client.get_total_key_supply(&creator_bob), 0);
}The contract_test_env module provides helpers to reduce boilerplate:
// Create test environment with mocked auth
let env = test_env_with_auths();
// Override the default deterministic timestamp when a test needs a specific moment
set_test_timestamp(&env, 1_700_000_123);
// Register contract and get client
let (client, contract_id) = register_creator_keys(&env);
// Set key price only
let admin = set_key_price_for_tests(&env, &client, 1000);
// Set fee config only
let admin = set_protocol_fee_bps(&env, &client, 9000, 1000);
// Set both price and fees (common pattern)
let admin = set_pricing_and_fees(&env, &client, 1000, 9000, 1000);
// Register a test creator
let creator = register_test_creator(&env, &client, "alice");Use set_test_timestamp in tests that need a deterministic ledger time (for
example snapshot assertions or time-sensitive setup). A recommended default is
DEFAULT_TEST_TIMESTAMP (1700000000).
For quote tests, use this pattern:
fn setup_with_fees<'a>(env: &'a Env, price: i128) -> (CreatorKeysContractClient<'a>, Address) {
let (client, _) = register_creator_keys(env);
set_pricing_and_fees(env, &client, price, 9000, 1000);
let creator = register_test_creator(env, &client, "alice");
(client, creator)
}Critical: When calling buy_key, you must provide the total_amount, not just the price:
// ❌ WRONG: Using price directly
let quote = client.get_buy_quote(&creator);
client.buy_key(&creator, &buyer, "e.price); // Will fail with InsufficientPayment!
// ✅ CORRECT: Using total_amount
let quote = client.get_buy_quote(&creator);
client.buy_key(&creator, &buyer, "e.total_amount); // Includes price + feesQuote tests generate snapshot files in creator-keys/test_snapshots/:
Snapshots capture the complete test execution state, including:
- All contract calls and their parameters
- Storage changes
- Events emitted
- Return values
Update snapshots when:
- You intentionally change contract behavior
- You add new tests
- Fee calculations change
Do not update snapshots to "fix" failing tests without understanding why they changed.
When snapshots change in a PR:
- Review the diff carefully
- Verify the changes match your intended behavior
- Explain the changes in the PR description
// ❌ WRONG: Fees don't sum to 10,000
set_pricing_and_fees(&env, &client, 1000, 9000, 0); // Sum is 9000
// ✅ CORRECT: Fees sum to 10,000
set_pricing_and_fees(&env, &client, 1000, 10000, 0); // Sum is 10,000// ❌ WRONG: Protocol fee exceeds 50%
set_pricing_and_fees(&env, &client, 1000, 0, 10000); // Protocol is 100%
// ✅ CORRECT: Protocol fee at maximum
set_pricing_and_fees(&env, &client, 1000, 5000, 5000); // Protocol is 50%// ❌ WRONG: Insufficient payment
let quote = client.get_buy_quote(&creator);
client.buy_key(&creator, &buyer, "e.price);
// ✅ CORRECT: Full payment including fees
let quote = client.get_buy_quote(&creator);
client.buy_key(&creator, &buyer, "e.total_amount);// ❌ WRONG: Expecting different prices
set_pricing_and_fees(&env, &client, 500, 8000, 2000);
let creator_alice = register_test_creator(&env, &client, "alice");
set_pricing_and_fees(&env, &client, 1000, 8000, 2000); // Changes global price!
let creator_bob = register_test_creator(&env, &client, "bob");
// Both creators now have price 1000 (global price was updated)
// ✅ CORRECT: Understanding global price model
set_pricing_and_fees(&env, &client, 1000, 8000, 2000);
let creator_alice = register_test_creator(&env, &client, "alice");
let creator_bob = register_test_creator(&env, &client, "bob");
// Both creators share the same global priceBefore submitting a quote test PR, verify:
- Test name clearly describes what is being tested
- Fee configuration sums to 10,000 and protocol ≤ 5,000
- Using
total_amountforbuy_keycalls, notprice - Assertions include descriptive failure messages
- Test is deterministic (no random values, no time dependencies)
- Test uses helpers from
contract_test_env - Test verifies the specific behavior mentioned in the name
- Snapshot files are reviewed and understood
Here's a complete example demonstrating best practices:
#[test]
fn test_buy_quote_stable_across_50_sequential_purchases() {
// Setup: Create environment with fixed price and fees
let env = test_env_with_auths();
let price = 750_i128;
let (client, creator) = setup_with_fees(&env, price);
// Get initial quote before any purchases
let initial_quote = client.get_buy_quote(&creator);
// Execute: Perform 50 sequential purchases
for i in 0..50_u32 {
let buyer = Address::generate(&env);
client.buy_key(&creator, &buyer, &initial_quote.total_amount);
// Assert: Verify quote remains stable after each purchase
let current_quote = client.get_buy_quote(&creator);
assert_eq!(
current_quote.price,
initial_quote.price,
"price must remain constant after {} purchases",
i + 1
);
assert_eq!(
current_quote.total_amount,
initial_quote.total_amount,
"total_amount must remain constant after {} purchases",
i + 1
);
assert_eq!(
current_quote.creator_fee,
initial_quote.creator_fee,
"creator_fee must remain constant after {} purchases",
i + 1
);
assert_eq!(
current_quote.protocol_fee,
initial_quote.protocol_fee,
"protocol_fee must remain constant after {} purchases",
i + 1
);
}
}# Run all quote monotonicity tests
cargo test --test buy_quote_monotonicity
# Run a specific test
cargo test test_buy_quote_stable_across_50_sequential_purchases
# Run with output
cargo test test_buy_quote_stable_across_50_sequential_purchases -- --nocaptureFor questions about writing quote tests:
- Review existing tests in
creator-keys/tests/buy_quote_monotonicity.rs - Check docs/fee-assumptions.md for fee calculation details
- See docs/error-codes.md for error handling
- Ask in pull request comments or discussions