Skip to content

Commit a7a3e59

Browse files
committed
test(contracts): add event snapshot coverage
1 parent bd9e7cb commit a7a3e59

7 files changed

Lines changed: 442 additions & 1 deletion

File tree

stellar-contracts/src/test.rs

Lines changed: 361 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ extern crate std;
55
use super::*;
66
use soroban_sdk::xdr::ToXdr;
77
use soroban_sdk::{
8-
testutils::{storage::Persistent as _, Address as _, Events as _, Ledger},
8+
testutils::{storage::Persistent as _, Address as _, EnvTestConfig, Events as _, Ledger},
99
token::{Client as TokenClient, StellarAssetClient},
1010
vec, Address, Bytes, BytesN, Env, IntoVal, Symbol,
1111
};
12+
use std::{fs, path::PathBuf, string::String, vec::Vec as StdVec};
1213

1314
// ── helpers ──────────────────────────────────────────────────────────
1415

@@ -67,6 +68,69 @@ fn setup_bridge_with_min(
6768
(contract_id, bridge, admin, token_addr, token, token_sac)
6869
}
6970

71+
struct SnapshotEvent {
72+
topics: StdVec<String>,
73+
data: String,
74+
}
75+
76+
fn new_snapshot_env() -> Env {
77+
Env::new_with_config(EnvTestConfig {
78+
capture_snapshot_at_drop: false,
79+
})
80+
}
81+
82+
fn snapshot_path(name: &str) -> PathBuf {
83+
PathBuf::from("test_snapshots")
84+
.join("events")
85+
.join(format!("{name}.json"))
86+
}
87+
88+
fn escape_json(value: &str) -> String {
89+
value.replace('\\', "\\\\").replace('"', "\\\"")
90+
}
91+
92+
fn render_snapshot(events: &[SnapshotEvent]) -> String {
93+
let mut out = String::from("[\n");
94+
for (index, event) in events.iter().enumerate() {
95+
out.push_str(" {\n");
96+
out.push_str(" \"contract\": \"bridge\",\n");
97+
out.push_str(" \"topics\": [\n");
98+
for (topic_index, topic) in event.topics.iter().enumerate() {
99+
out.push_str(&format!(
100+
" \"{}\"{}\n",
101+
escape_json(topic),
102+
if topic_index + 1 == event.topics.len() {
103+
""
104+
} else {
105+
","
106+
}
107+
));
108+
}
109+
out.push_str(" ],\n");
110+
out.push_str(&format!(" \"data\": \"{}\"\n", escape_json(&event.data)));
111+
out.push_str(" }");
112+
if index + 1 != events.len() {
113+
out.push(',');
114+
}
115+
out.push('\n');
116+
}
117+
out.push(']');
118+
out.push('\n');
119+
out
120+
}
121+
122+
fn assert_event_snapshot(name: &str, events: &[SnapshotEvent]) {
123+
let path = snapshot_path(name);
124+
let expected = fs::read_to_string(&path)
125+
.unwrap_or_else(|err| panic!("failed to read snapshot {}: {err}", path.display()));
126+
let actual = render_snapshot(events);
127+
assert_eq!(
128+
expected,
129+
actual,
130+
"snapshot mismatch for {name}. Update {} if this event change is intentional.",
131+
path.display()
132+
);
133+
}
70134

71135
// ── happy-path tests ──────────────────────────────────────────────────
72136

@@ -2811,6 +2875,302 @@ fn test_event_version_deny_add_remove() {
28112875

28122876
// ── Property-based tests (proptest) ──────────────────────────────────────────
28132877

2878+
#[test]
2879+
fn test_event_snapshot_heartbeat() {
2880+
let env = new_snapshot_env();
2881+
env.mock_all_auths();
2882+
let (contract_id, bridge, _, _, _, _) = setup_bridge(&env, 1_000);
2883+
let operator = Address::generate(&env);
2884+
2885+
bridge.set_operator(&operator, &true);
2886+
env.ledger().with_mut(|li| {
2887+
li.sequence_number = 12_345;
2888+
});
2889+
2890+
bridge.heartbeat(&operator, &0);
2891+
2892+
assert_eq!(
2893+
env.events().all().filter_by_contract(&contract_id),
2894+
vec![
2895+
&env,
2896+
(
2897+
contract_id.clone(),
2898+
vec![
2899+
&env,
2900+
EVENT_VERSION.into_val(&env),
2901+
Symbol::new(&env, "nonce_inc").into_val(&env),
2902+
operator.clone().into_val(&env)
2903+
],
2904+
1u64.into_val(&env)
2905+
),
2906+
(
2907+
contract_id,
2908+
vec![
2909+
&env,
2910+
EVENT_VERSION.into_val(&env),
2911+
Symbol::new(&env, "heartbeat").into_val(&env),
2912+
operator.into_val(&env)
2913+
],
2914+
12_345u32.into_val(&env)
2915+
)
2916+
]
2917+
);
2918+
2919+
assert_event_snapshot(
2920+
"heartbeat",
2921+
&[
2922+
SnapshotEvent {
2923+
topics: StdVec::from([
2924+
"u32:1".into(),
2925+
"symbol:nonce_inc".into(),
2926+
"address:operator".into(),
2927+
]),
2928+
data: "u64:1".into(),
2929+
},
2930+
SnapshotEvent {
2931+
topics: StdVec::from([
2932+
"u32:1".into(),
2933+
"symbol:heartbeat".into(),
2934+
"address:operator".into(),
2935+
]),
2936+
data: "u32:12345".into(),
2937+
},
2938+
],
2939+
);
2940+
}
2941+
2942+
#[test]
2943+
fn test_event_snapshot_deny_add() {
2944+
let env = new_snapshot_env();
2945+
env.mock_all_auths();
2946+
let (contract_id, bridge, _, _, _, _) = setup_bridge(&env, 1_000);
2947+
let target = Address::generate(&env);
2948+
2949+
bridge.deny_address(&target);
2950+
2951+
assert_eq!(
2952+
env.events().all().filter_by_contract(&contract_id),
2953+
vec![
2954+
&env,
2955+
(
2956+
contract_id,
2957+
vec![
2958+
&env,
2959+
EVENT_VERSION.into_val(&env),
2960+
Symbol::new(&env, "deny_add").into_val(&env)
2961+
],
2962+
target.into_val(&env)
2963+
)
2964+
]
2965+
);
2966+
2967+
assert_event_snapshot(
2968+
"deny_add",
2969+
&[SnapshotEvent {
2970+
topics: StdVec::from(["u32:1".into(), "symbol:deny_add".into()]),
2971+
data: "address:target".into(),
2972+
}],
2973+
);
2974+
}
2975+
2976+
#[test]
2977+
fn test_event_snapshot_deny_rem() {
2978+
let env = new_snapshot_env();
2979+
env.mock_all_auths();
2980+
let (contract_id, bridge, _, _, _, _) = setup_bridge(&env, 1_000);
2981+
let target = Address::generate(&env);
2982+
2983+
env.as_contract(&contract_id, || {
2984+
env.storage()
2985+
.persistent()
2986+
.set(&DataKey::Denied(target.clone()), &true);
2987+
});
2988+
2989+
bridge.remove_denied_address(&target);
2990+
2991+
assert_eq!(
2992+
env.events().all().filter_by_contract(&contract_id),
2993+
vec![
2994+
&env,
2995+
(
2996+
contract_id,
2997+
vec![
2998+
&env,
2999+
EVENT_VERSION.into_val(&env),
3000+
Symbol::new(&env, "deny_rem").into_val(&env)
3001+
],
3002+
target.into_val(&env)
3003+
)
3004+
]
3005+
);
3006+
3007+
assert_event_snapshot(
3008+
"deny_rem",
3009+
&[SnapshotEvent {
3010+
topics: StdVec::from(["u32:1".into(), "symbol:deny_rem".into()]),
3011+
data: "address:target".into(),
3012+
}],
3013+
);
3014+
}
3015+
3016+
#[test]
3017+
fn test_event_snapshot_quota_reset() {
3018+
let env = new_snapshot_env();
3019+
env.mock_all_auths();
3020+
let (contract_id, bridge, _, token_addr, _, token_sac) = setup_bridge(&env, 10_000);
3021+
let user = Address::generate(&env);
3022+
3023+
token_sac.mint(&contract_id, &1_000);
3024+
env.as_contract(&contract_id, || {
3025+
env.storage()
3026+
.instance()
3027+
.set(&DataKey::UserDeposited(user.clone()), &1_000i128);
3028+
env.storage()
3029+
.instance()
3030+
.set(&DataKey::WithdrawalQuota, &500i128);
3031+
env.storage().instance().set(
3032+
&DataKey::UserDailyWithdrawal(user.clone()),
3033+
&UserDailyWithdrawal {
3034+
amount: 500,
3035+
window_start: 100,
3036+
},
3037+
);
3038+
});
3039+
env.ledger().with_mut(|li| {
3040+
li.sequence_number = 17_380;
3041+
});
3042+
3043+
bridge.withdraw(&user, &500, &token_addr);
3044+
3045+
assert_eq!(
3046+
env.events().all().filter_by_contract(&contract_id),
3047+
vec![
3048+
&env,
3049+
(
3050+
contract_id.clone(),
3051+
vec![
3052+
&env,
3053+
EVENT_VERSION.into_val(&env),
3054+
Symbol::new(&env, "quota_reset").into_val(&env)
3055+
],
3056+
(user.clone(), 17_380u32).into_val(&env)
3057+
),
3058+
(
3059+
contract_id,
3060+
vec![
3061+
&env,
3062+
EVENT_VERSION.into_val(&env),
3063+
Symbol::new(&env, "withdraw").into_val(&env),
3064+
user.into_val(&env)
3065+
],
3066+
500i128.into_val(&env)
3067+
)
3068+
]
3069+
);
3070+
3071+
assert_event_snapshot(
3072+
"quota_reset",
3073+
&[
3074+
SnapshotEvent {
3075+
topics: StdVec::from(["u32:1".into(), "symbol:quota_reset".into()]),
3076+
data: "tuple:[address:user,u32:17380]".into(),
3077+
},
3078+
SnapshotEvent {
3079+
topics: StdVec::from([
3080+
"u32:1".into(),
3081+
"symbol:withdraw".into(),
3082+
"address:user".into(),
3083+
]),
3084+
data: "i128:500".into(),
3085+
},
3086+
],
3087+
);
3088+
}
3089+
3090+
#[test]
3091+
fn test_event_snapshot_fee_accrued() {
3092+
let env = new_snapshot_env();
3093+
env.mock_all_auths();
3094+
let (contract_id, bridge, _, token_addr, _, _) = setup_bridge(&env, 1_000);
3095+
3096+
bridge.accrue_fee(&token_addr, &250);
3097+
3098+
assert_eq!(
3099+
env.events().all().filter_by_contract(&contract_id),
3100+
vec![
3101+
&env,
3102+
(
3103+
contract_id,
3104+
vec![
3105+
&env,
3106+
EVENT_VERSION.into_val(&env),
3107+
Symbol::new(&env, "fee_accrue").into_val(&env),
3108+
token_addr.into_val(&env)
3109+
],
3110+
250i128.into_val(&env)
3111+
)
3112+
]
3113+
);
3114+
3115+
assert_event_snapshot(
3116+
"fee_accrued",
3117+
&[SnapshotEvent {
3118+
topics: StdVec::from([
3119+
"u32:1".into(),
3120+
"symbol:fee_accrue".into(),
3121+
"address:token".into(),
3122+
]),
3123+
data: "i128:250".into(),
3124+
}],
3125+
);
3126+
}
3127+
3128+
#[test]
3129+
fn test_event_snapshot_fees_withdrawn() {
3130+
let env = new_snapshot_env();
3131+
env.mock_all_auths();
3132+
let (contract_id, bridge, _, token_addr, _, token_sac) = setup_bridge(&env, 1_000);
3133+
let recipient = Address::generate(&env);
3134+
3135+
token_sac.mint(&contract_id, &400);
3136+
env.as_contract(&contract_id, || {
3137+
env.storage()
3138+
.persistent()
3139+
.set(&DataKey::FeeVault(token_addr.clone()), &400i128);
3140+
});
3141+
3142+
bridge.withdraw_fees(&recipient, &token_addr, &150);
3143+
3144+
assert_eq!(
3145+
env.events().all().filter_by_contract(&contract_id),
3146+
vec![
3147+
&env,
3148+
(
3149+
contract_id,
3150+
vec![
3151+
&env,
3152+
EVENT_VERSION.into_val(&env),
3153+
Symbol::new(&env, "fee_wdrw").into_val(&env),
3154+
recipient.into_val(&env)
3155+
],
3156+
150i128.into_val(&env)
3157+
)
3158+
]
3159+
);
3160+
3161+
assert_event_snapshot(
3162+
"fees_withdrawn",
3163+
&[SnapshotEvent {
3164+
topics: StdVec::from([
3165+
"u32:1".into(),
3166+
"symbol:fee_wdrw".into(),
3167+
"address:recipient".into(),
3168+
]),
3169+
data: "i128:150".into(),
3170+
}],
3171+
);
3172+
}
3173+
28143174
#[cfg(test)]
28153175
mod proptest_deposit {
28163176
use super::*;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"contract": "bridge",
4+
"topics": [
5+
"u32:1",
6+
"symbol:deny_add"
7+
],
8+
"data": "address:target"
9+
}
10+
]

0 commit comments

Comments
 (0)