Skip to content

Commit 3a6899d

Browse files
committed
test(contract): add snapshot/restore consistency tests
Add three consistency tests to the creator-earnings contract: - test_snapshot_restore_consistency: deposit, withdraw, re-deposit restores the internal balance to the recorded mid-state snapshot - test_sequential_deposits_accumulate_consistently: repeated deposits sum correctly and a full withdrawal returns balance to zero - test_multi_creator_balances_independent: operations on one creator do not affect another creator's balance snapshot Also fixes a pre-existing compile error in content-likes: missing `contracttype` import and invalid `panic_with_error!` string literal (replaced with `Error::AlreadyInitialized`). Applies cargo fmt to pre-existing formatting violations across content-access, content-likes, subscription, test-consumer, and treasury. Closes #944
1 parent 5c21688 commit 3a6899d

12 files changed

Lines changed: 186 additions & 43 deletions

File tree

contract/contracts/content-access/src/lib.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,10 @@ impl ContentAccess {
127127

128128
// Check if already unlocked (idempotent) – but re-check expiry.
129129
let access_key = DataKey::Access(buyer.clone(), creator.clone(), content_id);
130-
if let Some(existing) = env.storage().instance().get::<DataKey, Purchase>(&access_key)
130+
if let Some(existing) = env
131+
.storage()
132+
.instance()
133+
.get::<DataKey, Purchase>(&access_key)
131134
{
132135
// If the existing purchase is still valid, treat as no-op.
133136
if existing.expiry > current_seq {
@@ -166,7 +169,11 @@ impl ContentAccess {
166169
/// Check if buyer has valid (non-expired) access to content.
167170
pub fn has_access(env: Env, buyer: Address, creator: Address, content_id: u64) -> bool {
168171
let access_key = DataKey::Access(buyer, creator, content_id);
169-
if let Some(purchase) = env.storage().instance().get::<DataKey, Purchase>(&access_key) {
172+
if let Some(purchase) = env
173+
.storage()
174+
.instance()
175+
.get::<DataKey, Purchase>(&access_key)
176+
{
170177
let current_seq: u64 = env.ledger().sequence() as u64;
171178
purchase.expiry > current_seq
172179
} else {
@@ -853,7 +860,10 @@ mod test {
853860
assert!(client.has_access(&buyer, &creator, &1));
854861
// verify_access should not panic (we test this by not expecting an error)
855862
let verify_result = client.try_verify_access(&buyer, &creator, &1);
856-
assert!(verify_result.is_ok(), "verify_access should succeed when has_access is true");
863+
assert!(
864+
verify_result.is_ok(),
865+
"verify_access should succeed when has_access is true"
866+
);
857867
}
858868

859869
/// Invariant: If verify_access succeeds, has_access should return true.
@@ -879,7 +889,10 @@ mod test {
879889
client.unlock_content(&buyer, &creator, &1, &NO_EXPIRY);
880890
assert!(client.has_access(&buyer, &creator, &1));
881891
let verify_result = client.try_verify_access(&buyer, &creator, &1);
882-
assert!(verify_result.is_ok(), "verify_access should succeed after unlock");
892+
assert!(
893+
verify_result.is_ok(),
894+
"verify_access should succeed after unlock"
895+
);
883896
}
884897

885898
/// Invariant: Price set by creator should be retrievable.

contract/contracts/content-access/src/tests/event_tests.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use crate::{
2-
events::{
3-
AdminTransferredEvent, ContentPriceSetEvent, MaxPriceClearedEvent, MaxPriceSetEvent,
4-
},
2+
events::{AdminTransferredEvent, ContentPriceSetEvent, MaxPriceClearedEvent, MaxPriceSetEvent},
53
ContentAccess, ContentAccessClient,
64
};
75
use soroban_sdk::{

contract/contracts/content-access/src/tests/init_admin_tests.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@
1313

1414
use crate::{ContentAccess, ContentAccessClient, Error};
1515
use soroban_sdk::{
16-
testutils::Address as _,
17-
xdr::SorobanAuthorizationEntry,
18-
Address, Env, Error as SorobanError,
16+
testutils::Address as _, xdr::SorobanAuthorizationEntry, Address, Env, Error as SorobanError,
1917
};
2018

2119
const EMPTY_AUTHS: &[SorobanAuthorizationEntry] = &[];
@@ -62,7 +60,11 @@ fn initialize_stores_admin() {
6260

6361
client.initialize(&admin, &token_id);
6462

65-
assert_eq!(client.admin(), admin, "admin() must return the initialized admin");
63+
assert_eq!(
64+
client.admin(),
65+
admin,
66+
"admin() must return the initialized admin"
67+
);
6668
}
6769

6870
/// initialize stores the token address; set_content_price succeeds after init.
@@ -136,7 +138,10 @@ fn admin_view_panics_when_uninitialized() {
136138
let client = ContentAccessClient::new(&env, &contract_id);
137139

138140
let result = client.try_admin();
139-
assert!(result.is_err(), "admin() must fail on uninitialized contract");
141+
assert!(
142+
result.is_err(),
143+
"admin() must fail on uninitialized contract"
144+
);
140145
}
141146

142147
// ── set_admin ─────────────────────────────────────────────────────────────────
@@ -152,7 +157,8 @@ fn set_admin_transfers_admin_role() {
152157
client.set_admin(&new_admin);
153158

154159
assert_eq!(
155-
client.admin(), new_admin,
160+
client.admin(),
161+
new_admin,
156162
"admin() must return new admin after set_admin"
157163
);
158164
}
@@ -246,7 +252,11 @@ fn set_max_price_zero_clears_cap() {
246252
assert_eq!(client.get_max_price(), Some(500_000));
247253

248254
client.set_max_price(&0);
249-
assert_eq!(client.get_max_price(), None, "cap must be removed after set_max_price(0)");
255+
assert_eq!(
256+
client.get_max_price(),
257+
None,
258+
"cap must be removed after set_max_price(0)"
259+
);
250260
}
251261

252262
/// Non-admin cannot call set_max_price.
@@ -264,7 +274,10 @@ fn set_max_price_rejected_for_non_admin() {
264274

265275
env.set_auths(EMPTY_AUTHS);
266276
let result = client.try_set_max_price(&500_000);
267-
assert!(result.is_err(), "set_max_price must fail without admin auth");
277+
assert!(
278+
result.is_err(),
279+
"set_max_price must fail without admin auth"
280+
);
268281
}
269282

270283
/// Prices above max_price are rejected when cap is configured.

contract/contracts/content-access/src/tests/unauthorized_tests.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ fn setup(env: &Env) -> (ContentAccessClient<'_>, Address, Address) {
3030
(client, admin, token_address)
3131
}
3232

33-
fn mock_rogue_auth(env: &Env, rogue: &Address, contract: &Address, fn_name: &'static str, args: soroban_sdk::Vec<Val>) {
33+
fn mock_rogue_auth(
34+
env: &Env,
35+
rogue: &Address,
36+
contract: &Address,
37+
fn_name: &'static str,
38+
args: soroban_sdk::Vec<Val>,
39+
) {
3440
env.mock_auths(&[MockAuth {
3541
address: rogue,
3642
invoke: &MockAuthInvoke {
@@ -163,7 +169,9 @@ fn initialize_reverts_if_already_initialized() {
163169
let second_admin = Address::generate(&env);
164170

165171
env.mock_all_auths();
166-
assert!(client.try_initialize(&second_admin, &token_address).is_err());
172+
assert!(client
173+
.try_initialize(&second_admin, &token_address)
174+
.is_err());
167175
}
168176

169177
#[test]

contract/contracts/content-likes/src/lib.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![no_std]
22
use soroban_sdk::{
3-
contract, contracterror, contractimpl, panic_with_error, Address, Env, Map, Symbol, Vec,
3+
contract, contracterror, contractimpl, contracttype, panic_with_error, Address, Env, Map,
4+
Symbol, Vec,
45
};
56

67
mod events;
@@ -24,11 +25,14 @@ pub enum DataKey {
2425
/// | Code | Variant |
2526
/// |------|---------|
2627
/// | 1 | `NotLiked` |
28+
/// | 2 | `AlreadyInitialized` |
2729
#[contracterror]
2830
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2931
pub enum Error {
3032
/// Code 1 – user has not liked this content; `unlike` was called without a prior `like`.
3133
NotLiked = 1,
34+
/// Code 2 – contract was already initialized.
35+
AlreadyInitialized = 2,
3236
}
3337

3438
#[contract]
@@ -40,7 +44,7 @@ impl ContentLikes {
4044
pub fn initialize(env: Env, admin: Address) {
4145
admin.require_auth();
4246
if env.storage().instance().has(&DataKey::Admin) {
43-
panic_with_error!(&env, "already initialized");
47+
panic_with_error!(&env, Error::AlreadyInitialized);
4448
}
4549
env.storage().instance().set(&DataKey::Admin, &admin);
4650
}
@@ -584,7 +588,10 @@ mod test {
584588

585589
// Verify events were published
586590
let events = env.events().all();
587-
assert!(events.len() >= 2, "Expected at least 2 events (like and unlike)");
591+
assert!(
592+
events.len() >= 2,
593+
"Expected at least 2 events (like and unlike)"
594+
);
588595
}
589596

590597
#[test]

contract/contracts/content-likes/tests/contract_integration.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ fn test_error_unlike_without_like() {
6363

6464
// Try to unlike without ever liking — should fail with NotLiked error (code 1)
6565
let result = client.try_unlike(&user, &content_id);
66-
assert!(result.is_err(), "Expected unlike without prior like to fail with NotLiked error");
66+
assert!(
67+
result.is_err(),
68+
"Expected unlike without prior like to fail with NotLiked error"
69+
);
6770
}
6871

6972
/// Test multiple users liking the same content.

contract/contracts/creator-earnings/src/test.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,85 @@ fn withdraw_failed_emits_no_event() {
240240
assert_eq!(client.balance(&creator), 500);
241241
assert!(env.events().all().len() >= events_before);
242242
}
243+
244+
// -------- Snapshot / restore consistency tests for issue #944 --------
245+
246+
/// Verify that depositing, withdrawing, then re-depositing the same amount
247+
/// restores the internal balance to the intermediate snapshot.
248+
#[test]
249+
fn test_snapshot_restore_consistency() {
250+
let env = Env::default();
251+
let (_admin, creator, depositor, client, _, token_admin_client) = setup(&env);
252+
253+
// Initial snapshot: balance is zero
254+
assert_eq!(client.balance(&creator), 0);
255+
256+
// Deposit to a known state
257+
client.deposit(&depositor, &creator, &400);
258+
let mid_snapshot = client.balance(&creator);
259+
assert_eq!(mid_snapshot, 400);
260+
261+
// Partial withdrawal moves balance below snapshot
262+
client.withdraw(&creator, &150);
263+
assert_eq!(client.balance(&creator), 250);
264+
265+
// Re-mint so depositor can fund the restore deposit
266+
token_admin_client.mint(&depositor, &150);
267+
268+
// Re-deposit the withdrawn amount to restore to mid-snapshot
269+
client.deposit(&depositor, &creator, &150);
270+
assert_eq!(client.balance(&creator), mid_snapshot);
271+
}
272+
273+
/// Verify that sequential deposits accumulate correctly and a full withdrawal
274+
/// returns the balance to zero.
275+
#[test]
276+
fn test_sequential_deposits_accumulate_consistently() {
277+
let env = Env::default();
278+
let (_admin, creator, depositor, client, _, token_admin_client) = setup(&env);
279+
280+
client.deposit(&depositor, &creator, &100);
281+
assert_eq!(client.balance(&creator), 100);
282+
283+
token_admin_client.mint(&depositor, &200);
284+
client.deposit(&depositor, &creator, &200);
285+
assert_eq!(client.balance(&creator), 300);
286+
287+
token_admin_client.mint(&depositor, &300);
288+
client.deposit(&depositor, &creator, &300);
289+
assert_eq!(client.balance(&creator), 600);
290+
291+
// Full withdrawal restores balance to zero
292+
client.withdraw(&creator, &600);
293+
assert_eq!(client.balance(&creator), 0);
294+
}
295+
296+
/// Verify that balances across multiple creators remain independent; a deposit
297+
/// or withdrawal for one creator does not affect the snapshot of another.
298+
#[test]
299+
fn test_multi_creator_balances_independent() {
300+
let env = Env::default();
301+
let (_admin, _creator, depositor, client, _, _) = setup(&env);
302+
303+
let creator_a = Address::generate(&env);
304+
let creator_b = Address::generate(&env);
305+
306+
// Snapshot: both creators start at 0
307+
assert_eq!(client.balance(&creator_a), 0);
308+
assert_eq!(client.balance(&creator_b), 0);
309+
310+
// Deposit to creator_a only (depositor has 1_000 from setup)
311+
client.deposit(&depositor, &creator_a, &300);
312+
assert_eq!(client.balance(&creator_a), 300);
313+
assert_eq!(client.balance(&creator_b), 0);
314+
315+
// Deposit to creator_b
316+
client.deposit(&depositor, &creator_b, &200);
317+
assert_eq!(client.balance(&creator_a), 300);
318+
assert_eq!(client.balance(&creator_b), 200);
319+
320+
// Withdrawal from creator_a must not affect creator_b
321+
client.withdraw(&creator_a, &100);
322+
assert_eq!(client.balance(&creator_a), 200);
323+
assert_eq!(client.balance(&creator_b), 200);
324+
}

contract/contracts/subscription/src/test.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,7 +1183,10 @@ fn test_pause_non_admin_rejected() {
11831183

11841184
let result = client.try_pause();
11851185
assert!(result.is_err(), "non-admin must not pause the contract");
1186-
assert!(!client.is_paused(), "contract must remain unpaused after unauthorized pause attempt");
1186+
assert!(
1187+
!client.is_paused(),
1188+
"contract must remain unpaused after unauthorized pause attempt"
1189+
);
11871190
}
11881191

11891192
#[test]
@@ -1198,7 +1201,10 @@ fn test_unpause_non_admin_rejected() {
11981201
env.set_auths(&[]);
11991202
let result = client.try_unpause();
12001203
assert!(result.is_err(), "non-admin must not unpause the contract");
1201-
assert!(client.is_paused(), "contract must remain paused after unauthorized unpause attempt");
1204+
assert!(
1205+
client.is_paused(),
1206+
"contract must remain paused after unauthorized unpause attempt"
1207+
);
12021208
}
12031209

12041210
// ── set_fee_recipient (admin fee recipient rotation) ─────────────────────────

contract/contracts/test-consumer/src/lib.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,7 @@ mod test {
217217
mod creator_deposits_integration {
218218
use creator_deposits::{CreatorDeposits, CreatorDepositsClient, Error as DepositError};
219219
use myfans_token::{MyFansToken, MyFansTokenClient};
220-
use soroban_sdk::{
221-
testutils::Address as _,
222-
Address, Env, String,
223-
};
220+
use soroban_sdk::{testutils::Address as _, Address, Env, String};
224221

225222
fn deploy_token(env: &Env) -> (MyFansTokenClient<'_>, Address) {
226223
let admin = Address::generate(env);
@@ -640,7 +637,10 @@ mod test {
640637
content_access.set_content_price(&creator, &content_id, &100);
641638

642639
// Verify price is set
643-
assert_eq!(content_access.get_content_price(&creator, &content_id), Some(100));
640+
assert_eq!(
641+
content_access.get_content_price(&creator, &content_id),
642+
Some(100)
643+
);
644644

645645
// Buyer unlocks content
646646
content_access.unlock_content(&buyer, &creator, content_id, &2000); // expiry far in future
@@ -739,12 +739,22 @@ mod test {
739739
let addr = env
740740
.register_stellar_asset_contract_v2(admin.clone())
741741
.address();
742-
(addr.clone(), TokenClient::new(env, &addr), StellarAssetClient::new(env, &addr))
742+
(
743+
addr.clone(),
744+
TokenClient::new(env, &addr),
745+
StellarAssetClient::new(env, &addr),
746+
)
743747
}
744748

745749
fn setup(
746750
env: &Env,
747-
) -> (TreasuryClient<'_>, Address, Address, TokenClient<'_>, Address) {
751+
) -> (
752+
TreasuryClient<'_>,
753+
Address,
754+
Address,
755+
TokenClient<'_>,
756+
Address,
757+
) {
748758
env.mock_all_auths();
749759
let admin = Address::generate(env);
750760
let depositor = Address::generate(env);

contract/contracts/treasury/src/lib.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,8 @@ impl Treasury {
3131
env.storage().instance().set(&PAUSED, &false);
3232
env.storage().instance().set(&MIN_BALANCE, &0i128);
3333

34-
env.events().publish(
35-
(Symbol::new(&env, "initialized"),),
36-
(admin, token_address),
37-
);
34+
env.events()
35+
.publish((Symbol::new(&env, "initialized"),), (admin, token_address));
3836
}
3937

4038
/// Pause (`true`) or unpause (`false`) the contract.

0 commit comments

Comments
 (0)