This guide documents standard testing practices for PayFlow contract development. It covers ledger time manipulation, external contract mocking, and Rust unit test patterns.
Tests simulate time-based billing logic (renewal periods, grace windows, trial expiration) by manipulating the ledger timestamp:
#[test]
fn test_subscription_renewal_after_interval() {
let env = Env::default();
let contract_id = env.register_contract(None, PayFlowContract);
// Create subscription at timestamp 1000
env.ledger().set_timestamp(1000);
let user = Address::random(&env);
let merchant = Address::random(&env);
env.invoke_contract(
&contract_id,
&Symbol::new(&env, "subscribe"),
&vec![
&env,
&user,
&merchant,
&1_000_000i128, // amount
&86400i64, // 24-hour interval
],
);
// Advance time to next billing window
env.ledger().set_timestamp(1000 + 86400 + 1); // Just past interval
// Verify charge is now eligible
let eligible = env.invoke_contract(
&contract_id,
&Symbol::new(&env, "is_charge_eligible"),
&vec![&env, &user],
);
assert_eq!(eligible, true);
}#[test]
fn test_grace_period_extends_charge_window() {
let env = Env::default();
let contract_id = env.register_contract(None, PayFlowContract);
env.ledger().set_timestamp(1000);
// Setup subscription
let user = Address::random(&env);
env.invoke_contract(&contract_id, &Symbol::new(&env, "subscribe"), &vec![/*...*/]);
// Simulate insufficient balance at charge time
env.ledger().set_timestamp(1000 + 86400 + 1);
// Charge fails due to low balance
let result = env.invoke_contract(
&contract_id,
&Symbol::new(&env, "charge"),
&vec![&env, &user],
);
assert!(result.is_err());
// Grace period window (1 hour) allows retry
env.ledger().set_timestamp(1000 + 86400 + 3600);
let grace_result = env.invoke_contract(
&contract_id,
&Symbol::new(&env, "charge"),
&vec![&env, &user],
);
assert!(grace_result.is_ok());
}#[test]
fn test_trial_expires_after_duration() {
let env = Env::default();
let contract_id = env.register_contract(None, PayFlowContract);
env.ledger().set_timestamp(1000);
// Activate trial (30 days = 2592000 seconds)
env.invoke_contract(
&contract_id,
&Symbol::new(&env, "activate_trial"),
&vec![&env, &user],
);
// Before expiration - trial active
env.ledger().set_timestamp(1000 + 2591999);
let status = check_subscription_status(&env, &contract_id, &user);
assert_eq!(status, SubscriptionStatus::TrialActive);
// After expiration - trial expires
env.ledger().set_timestamp(1000 + 2592001);
let status = check_subscription_status(&env, &contract_id, &user);
assert_eq!(status, SubscriptionStatus::Active);
}#[test]
fn test_charge_with_mock_token() {
let env = Env::default();
// Deploy mock token contract
let token = env.register_stellar_asset_contract(AssetType::Native);
let token_client = TokenClient::new(&env, &token);
let user = Address::random(&env);
let merchant = Address::random(&env);
// Mint initial balance to user
token_client.mint(&user, &1_000_000_000i128);
// Deploy PayFlow contract
let contract_id = env.register_contract(None, PayFlowContract);
// User approves PayFlow to spend tokens
token_client.approve(&user, &contract_id, &1_000_000_000i128, &10000i64);
// Execute subscription
env.invoke_contract(
&contract_id,
&Symbol::new(&env, "subscribe"),
&vec![
&env,
&user,
&merchant,
&100_000i128,
&86400i64,
],
);
// Verify token approval consumed
let balance = token_client.balance(&user);
assert!(balance < 1_000_000_000i128);
}#[test]
fn test_charge_fails_with_insufficient_balance() {
let env = Env::default();
let token = env.register_stellar_asset_contract(AssetType::Native);
let token_client = TokenClient::new(&env, &token);
let user = Address::random(&env);
let merchant = Address::random(&env);
// Mint only 50 tokens to user
token_client.mint(&user, &50i128);
let contract_id = env.register_contract(None, PayFlowContract);
token_client.approve(&user, &contract_id, &50i128, &10000i64);
// Attempt subscription requiring 100 tokens
let result = env.invoke_contract(
&contract_id,
&Symbol::new(&env, "subscribe"),
&vec![
&env,
&user,
&merchant,
&100i128, // Exceeds balance
&86400i64,
],
);
// Should fail
assert!(result.is_err());
}#[test]
fn test_subscription_ttl_extended_on_charge() {
let env = Env::default();
let contract_id = env.register_contract(None, PayFlowContract);
env.ledger().set_timestamp(1000);
// Create subscription
let user = Address::random(&env);
env.invoke_contract(&contract_id, &Symbol::new(&env, "subscribe"), &vec![/*...*/]);
// Get initial TTL
let initial_ttl = env.ledger().max_live_until();
// Advance time and charge
env.ledger().set_timestamp(1000 + 86400 + 1);
env.invoke_contract(&contract_id, &Symbol::new(&env, "charge"), &vec![&env, &user]);
// TTL should be extended
let new_ttl = env.ledger().max_live_until();
assert!(new_ttl > initial_ttl);
}#[test]
#[should_panic(expected = "Unauthorized")]
fn test_admin_freeze_panics_without_auth() {
let env = Env::default();
let contract_id = env.register_contract(None, PayFlowContract);
let unauthorized_user = Address::random(&env);
env.as_contract(&contract_id, || {
env.invoke_contract(
&contract_id,
&Symbol::new(&env, "admin_emergency_freeze"),
&vec![],
)
.unwrap(); // Will panic with Unauthorized error
});
}#[test]
fn test_invalid_amount_panics() {
let env = Env::default();
let contract_id = env.register_contract(None, PayFlowContract);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
env.invoke_contract(
&contract_id,
&Symbol::new(&env, "charge"),
&vec![
&env,
&Address::random(&env),
&0i128, // Invalid: zero amount
],
)
}));
assert!(result.is_err());
}# Run entire test suite
cargo test
# Run with output
cargo test -- --nocapture
# Run specific test
cargo test test_subscription_renewal_after_interval
# Run tests matching pattern
cargo test batch_charge# Run with backtrace
RUST_BACKTRACE=1 cargo test -- --nocapture
# Run single test with full output
cargo test test_name -- --nocapture --test-threads=1
# Check for panics
cargo test -- --test-threads=1# Generate snapshots
cargo test -- --test-threads=1 -- --nocapture
# Review snapshot updates
git diff test_snapshots/- Always set initial ledger timestamp to predictable value (e.g., 1000)
- Mock all external dependencies (tokens, oracle data)
- Test both success and failure paths for each function
- Use descriptive test names (e.g.,
test_grace_period_extends_charge_window) - Isolate tests - each test creates independent environment
- Keep tests deterministic - avoid non-deterministic time or random values
- Test edge cases - boundary conditions, off-by-one errors, overflow scenarios