From 4636b1013460ae2da59c0893300b379d8fac1692 Mon Sep 17 00:00:00 2001 From: timiturn3r Date: Mon, 24 Aug 2026 22:58:29 +0100 Subject: [PATCH] test: add unit tests for retention policy view and configuration (#724) --- creator-keys/src/lib.rs | 145 ++++++++++++++++ creator-keys/tests/retention_policy_view.rs | 177 ++++++++++++++++++++ docs/storage-layout.md | 1 + 3 files changed, 323 insertions(+) create mode 100644 creator-keys/tests/retention_policy_view.rs diff --git a/creator-keys/src/lib.rs b/creator-keys/src/lib.rs index a9233135..b89c884b 100644 --- a/creator-keys/src/lib.rs +++ b/creator-keys/src/lib.rs @@ -314,6 +314,7 @@ pub mod constants { pub const PAUSED: DataKey = DataKey::Paused; pub const CURVE_SLOPE: DataKey = DataKey::CurveSlope; pub const TREASURY_BALANCE: DataKey = DataKey::TreasuryBalance; + pub const RETENTION_POLICY: DataKey = DataKey::RetentionPolicy; pub fn curve_preset(creator: &Address) -> DataKey { DataKey::CurvePreset(creator.clone()) @@ -611,6 +612,26 @@ pub enum CurvePreset { Flat = 2, } +/// Archive partition strategy for retention management. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[contracttype] +pub enum PartitionStrategy { + Daily = 0, + Weekly = 1, + Monthly = 2, + Ledger = 3, +} + +/// Archive retention policy configuration. +#[derive(Clone, Debug, Eq, PartialEq)] +#[contracttype] +pub struct RetentionPolicy { + pub retention_days: u32, + pub partition_strategy: PartitionStrategy, + pub compression_enabled: bool, + pub batch_size: u32, +} + /// Canonical storage key schema for persistent protocol state. /// /// For quote-related key usage and invariants, see @@ -651,6 +672,8 @@ pub enum DataKey { /// Wallet addresses the protocol admin has barred from buying, selling, /// or registering as a creator. Blacklisted(Address), + /// Archive retention policy configuration. + RetentionPolicy, } /// Time-locked key allocation for creator self-vesting. @@ -1155,6 +1178,38 @@ fn credit_treasury_balance(env: &Env, amount: i128) -> Result<(), ContractError> Ok(()) } +/// Archive retention configuration module with canonical defaults. +pub mod retention { + use super::PartitionStrategy; + + /// Default retention window in days (30 days). + pub const DEFAULT_RETENTION_DAYS: u32 = 30; + /// Default partition strategy (Daily). + pub const DEFAULT_PARTITION_STRATEGY: PartitionStrategy = PartitionStrategy::Daily; + /// Default compression enabled flag (true). + pub const DEFAULT_COMPRESSION_ENABLED: bool = true; + /// Default batch size for archive processing (100). + pub const DEFAULT_BATCH_SIZE: u32 = 100; +} + +/// Returns the canonical default [`RetentionPolicy`]. +pub fn default_retention_policy() -> RetentionPolicy { + RetentionPolicy { + retention_days: retention::DEFAULT_RETENTION_DAYS, + partition_strategy: retention::DEFAULT_PARTITION_STRATEGY, + compression_enabled: retention::DEFAULT_COMPRESSION_ENABLED, + batch_size: retention::DEFAULT_BATCH_SIZE, + } +} + +/// Reads the current archive retention policy from storage, falling back to defaults. +pub fn read_retention_policy(env: &Env) -> RetentionPolicy { + env.storage() + .persistent() + .get(&constants::storage::RETENTION_POLICY) + .unwrap_or_else(default_retention_policy) +} + fn assert_buy_price_slippage(price: i128, max_price: Option) -> Result<(), ContractError> { if let Some(max) = max_price { if price > max { @@ -2895,6 +2950,49 @@ impl CreatorKeysContract { Ok(()) } + /// Sets the archive retention policy configuration. + /// + /// Only callable by an authorized admin. + /// + /// Parameter validation: + /// - `admin`: must authorize the call (`require_auth`). + /// - `batch_size`: must be strictly positive; returns [`ContractError::NotPositiveAmount`]. + pub fn set_retention_policy( + env: Env, + admin: Address, + retention_days: u32, + partition_strategy: PartitionStrategy, + compression_enabled: bool, + batch_size: u32, + ) -> Result<(), ContractError> { + admin.require_auth(); + assert_is_admin(&env, &admin)?; + if batch_size == 0 { + return Err(ContractError::NotPositiveAmount); + } + + let policy = RetentionPolicy { + retention_days, + partition_strategy, + compression_enabled, + batch_size, + }; + + env.storage() + .persistent() + .set(&constants::storage::RETENTION_POLICY, &policy); + + Ok(()) + } + + /// Read-only view: returns the current archive retention configuration. + /// + /// Returns the configured [`RetentionPolicy`] or canonical defaults if unset. + /// Does not mutate contract state or panic when uninitialized. + pub fn get_retention_policy(env: Env) -> RetentionPolicy { + read_retention_policy(&env) + } + /// Read-only view: returns whether protocol configuration has been initialized. /// /// Returns `true` once a protocol fee configuration has been stored and `false` @@ -4732,6 +4830,53 @@ mod tests { let bps = env.as_contract(&contract_id, || super::read_protocol_fee_bps(&env)); assert_eq!(bps, 1000, "must return stored protocol_fee_bps"); } + + // --- retention policy unit tests (#724) --- + + #[test] + fn test_read_retention_policy_returns_default_when_unset() { + let env = Env::default(); + let contract_id = env.register(super::CreatorKeysContract, ()); + + let policy = env.as_contract(&contract_id, || super::read_retention_policy(&env)); + assert_eq!( + policy.retention_days, + super::retention::DEFAULT_RETENTION_DAYS + ); + assert_eq!( + policy.partition_strategy, + super::retention::DEFAULT_PARTITION_STRATEGY + ); + assert_eq!( + policy.compression_enabled, + super::retention::DEFAULT_COMPRESSION_ENABLED + ); + assert_eq!(policy.batch_size, super::retention::DEFAULT_BATCH_SIZE); + } + + #[test] + fn test_get_retention_policy_view_returns_configured_values() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(super::CreatorKeysContract, ()); + let client = super::CreatorKeysContractClient::new(&env, &contract_id); + let admin = Address::generate(&env); + + client.set_protocol_admin(&admin, &admin); + client.set_retention_policy( + &admin, + &90u32, + &super::PartitionStrategy::Monthly, + &false, + &500u32, + ); + + let policy = client.get_retention_policy(); + assert_eq!(policy.retention_days, 90); + assert_eq!(policy.partition_strategy, super::PartitionStrategy::Monthly); + assert!(!policy.compression_enabled); + assert_eq!(policy.batch_size, 500); + } } #[cfg(test)] diff --git a/creator-keys/tests/retention_policy_view.rs b/creator-keys/tests/retention_policy_view.rs new file mode 100644 index 00000000..d9218c5d --- /dev/null +++ b/creator-keys/tests/retention_policy_view.rs @@ -0,0 +1,177 @@ +//! Tests for get_retention_policy view and retention policy configuration (#724). + +mod contract_test_env; + +use contract_test_env::{register_creator_keys, test_env_with_auths}; +use creator_keys::{ + retention, ContractError, CreatorKeysContract, CreatorKeysContractClient, PartitionStrategy, +}; +use soroban_sdk::{testutils::Address as _, Address, Env}; + +#[test] +fn test_get_retention_policy_unconfigured_returns_defaults_no_panic() { + let env = Env::default(); + let contract_id = env.register(CreatorKeysContract, ()); + let client = CreatorKeysContractClient::new(&env, &contract_id); + + // Call get_retention_policy before any admin configuration + let policy = client.get_retention_policy(); + + // Assert no panic and matches default canonical configuration + assert_eq!(policy.retention_days, retention::DEFAULT_RETENTION_DAYS); + assert_eq!( + policy.partition_strategy, + retention::DEFAULT_PARTITION_STRATEGY + ); + assert_eq!( + policy.compression_enabled, + retention::DEFAULT_COMPRESSION_ENABLED + ); + assert_eq!(policy.batch_size, retention::DEFAULT_BATCH_SIZE); +} + +#[test] +fn test_get_retention_policy_returns_configured_values() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = Address::generate(&env); + client.set_protocol_admin(&admin, &admin); + + let configured_days = 90u32; + let configured_strategy = PartitionStrategy::Monthly; + let configured_compression = false; + let configured_batch = 500u32; + + client.set_retention_policy( + &admin, + &configured_days, + &configured_strategy, + &configured_compression, + &configured_batch, + ); + + let policy = client.get_retention_policy(); + + // Acceptance Criteria validations + assert_eq!(policy.retention_days, configured_days); + assert_eq!(policy.partition_strategy, configured_strategy); + assert_eq!(policy.compression_enabled, configured_compression); + assert_eq!(policy.batch_size, configured_batch); +} + +#[test] +fn test_get_retention_policy_all_partition_strategies() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = Address::generate(&env); + client.set_protocol_admin(&admin, &admin); + + let strategies = [ + PartitionStrategy::Daily, + PartitionStrategy::Weekly, + PartitionStrategy::Monthly, + PartitionStrategy::Ledger, + ]; + + for strategy in strategies { + client.set_retention_policy(&admin, &60u32, &strategy, &true, &250u32); + let policy = client.get_retention_policy(); + assert_eq!(policy.partition_strategy, strategy); + } +} + +#[test] +fn test_get_retention_policy_compression_enabled_variants() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = Address::generate(&env); + client.set_protocol_admin(&admin, &admin); + + // Test with compression enabled + client.set_retention_policy(&admin, &45u32, &PartitionStrategy::Weekly, &true, &150u32); + let policy_true = client.get_retention_policy(); + assert!(policy_true.compression_enabled); + + // Test with compression disabled + client.set_retention_policy(&admin, &45u32, &PartitionStrategy::Weekly, &false, &150u32); + let policy_false = client.get_retention_policy(); + assert!(!policy_false.compression_enabled); +} + +#[test] +fn test_get_retention_policy_is_read_only_and_idempotent() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = Address::generate(&env); + client.set_protocol_admin(&admin, &admin); + + client.set_retention_policy(&admin, &180u32, &PartitionStrategy::Ledger, &true, &1000u32); + + let first_read = client.get_retention_policy(); + let second_read = client.get_retention_policy(); + + assert_eq!(first_read, second_read); + assert_eq!(first_read.retention_days, 180); + assert_eq!(first_read.partition_strategy, PartitionStrategy::Ledger); + assert!(first_read.compression_enabled); + assert_eq!(first_read.batch_size, 1000); +} + +#[test] +fn test_get_retention_policy_updates_after_reconfiguration() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = Address::generate(&env); + client.set_protocol_admin(&admin, &admin); + + // Initial configuration + client.set_retention_policy(&admin, &30u32, &PartitionStrategy::Daily, &true, &100u32); + let v1 = client.get_retention_policy(); + assert_eq!(v1.retention_days, 30); + assert_eq!(v1.partition_strategy, PartitionStrategy::Daily); + + // Reconfiguration + client.set_retention_policy( + &admin, + &365u32, + &PartitionStrategy::Monthly, + &false, + &5000u32, + ); + let v2 = client.get_retention_policy(); + assert_eq!(v2.retention_days, 365); + assert_eq!(v2.partition_strategy, PartitionStrategy::Monthly); + assert!(!v2.compression_enabled); + assert_eq!(v2.batch_size, 5000); + assert_ne!(v1, v2); +} + +#[test] +fn test_set_retention_policy_unauthorized_reverts() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = Address::generate(&env); + let non_admin = Address::generate(&env); + client.set_protocol_admin(&admin, &admin); + + let result = client.try_set_retention_policy( + &non_admin, + &90u32, + &PartitionStrategy::Daily, + &true, + &100u32, + ); + assert_eq!(result, Err(Ok(ContractError::Unauthorized))); +} + +#[test] +fn test_set_retention_policy_zero_batch_size_rejected() { + let env = test_env_with_auths(); + let (client, _) = register_creator_keys(&env); + let admin = Address::generate(&env); + client.set_protocol_admin(&admin, &admin); + + let result = + client.try_set_retention_policy(&admin, &90u32, &PartitionStrategy::Daily, &true, &0u32); + assert_eq!(result, Err(Ok(ContractError::NotPositiveAmount))); +} diff --git a/docs/storage-layout.md b/docs/storage-layout.md index d49991cf..cd3a393d 100644 --- a/docs/storage-layout.md +++ b/docs/storage-layout.md @@ -73,6 +73,7 @@ distinguishable from other key types. | `ReferralFeeBps` | Global | `u32` | `set_referral_fee_bps` (admin) | `buy_key_with_referrer` (referral split), `get_referral_fee_bps` | | `DiscountTiers` | Global | `Vec` | `update_discount_tiers` (admin) | `get_discount_tiers` (volume-based fee discount evaluation) | | `CreatorVolume(Address)` | Per-creator | `i128` | *(not currently written by any entrypoint)* | `get_creator_volume` | +| `RetentionPolicy` | Global | `RetentionPolicy` | `set_retention_policy` (admin) | `read_retention_policy`, `get_retention_policy` | | `CreatorTtlLiveUntil(Address)` | Per-creator | `u32` | `register_creator` (initial write), `extend_creator_ttl` (updated after every trade) | `extend_creator_ttl` (TTL-extension event gate) | > **Note:** `CreatorVolume(Address)` is read by `get_creator_volume` but has no