Skip to content

Commit 610a059

Browse files
authored
Merge pull request #406 from edehvictor/codex/test-contract-event-snapshots-322
test(contracts): add event snapshot coverage
2 parents bd9e7cb + e68abc5 commit 610a059

7 files changed

Lines changed: 435 additions & 1 deletion

File tree

stellar-contracts/src/test.rs

Lines changed: 354 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::{format, 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,295 @@ 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, admin, token_addr, _, token_sac) = setup_bridge(&env, 10_000);
3021+
let user = Address::generate(&env);
3022+
3023+
token_sac.mint(&user, &5_000);
3024+
bridge.deposit(&user, &2_000, &token_addr, &Bytes::new(&env), &0, &0, &None);
3025+
bridge.set_withdrawal_quota(&500);
3026+
bridge.withdraw(&admin, &user, &500, &token_addr);
3027+
3028+
let result = bridge.try_withdraw(&admin, &user, &100, &token_addr);
3029+
assert_eq!(result, Err(Ok(Error::WithdrawalQuotaExceeded)));
3030+
3031+
let start_ledger = env.ledger().sequence();
3032+
env.ledger().with_mut(|li| {
3033+
li.sequence_number = start_ledger + 17_280;
3034+
});
3035+
3036+
bridge.withdraw(&admin, &user, &500, &token_addr);
3037+
3038+
assert_eq!(
3039+
env.events().all().filter_by_contract(&contract_id),
3040+
vec![
3041+
&env,
3042+
(
3043+
contract_id.clone(),
3044+
vec![
3045+
&env,
3046+
EVENT_VERSION.into_val(&env),
3047+
Symbol::new(&env, "quota_reset").into_val(&env)
3048+
],
3049+
(user.clone(), start_ledger + 17_280).into_val(&env)
3050+
),
3051+
(
3052+
contract_id,
3053+
vec![
3054+
&env,
3055+
EVENT_VERSION.into_val(&env),
3056+
Symbol::new(&env, "withdraw").into_val(&env),
3057+
user.into_val(&env)
3058+
],
3059+
500i128.into_val(&env)
3060+
)
3061+
]
3062+
);
3063+
3064+
assert_event_snapshot(
3065+
"quota_reset",
3066+
&[
3067+
SnapshotEvent {
3068+
topics: StdVec::from(["u32:1".into(), "symbol:quota_reset".into()]),
3069+
data: "tuple:[address:user,u32:17281]".into(),
3070+
},
3071+
SnapshotEvent {
3072+
topics: StdVec::from([
3073+
"u32:1".into(),
3074+
"symbol:withdraw".into(),
3075+
"address:user".into(),
3076+
]),
3077+
data: "i128:500".into(),
3078+
},
3079+
],
3080+
);
3081+
}
3082+
3083+
#[test]
3084+
fn test_event_snapshot_fee_accrued() {
3085+
let env = new_snapshot_env();
3086+
env.mock_all_auths();
3087+
let (contract_id, bridge, _, token_addr, _, _) = setup_bridge(&env, 1_000);
3088+
3089+
bridge.accrue_fee(&token_addr, &250);
3090+
3091+
assert_eq!(
3092+
env.events().all().filter_by_contract(&contract_id),
3093+
vec![
3094+
&env,
3095+
(
3096+
contract_id,
3097+
vec![
3098+
&env,
3099+
EVENT_VERSION.into_val(&env),
3100+
Symbol::new(&env, "fee_accrue").into_val(&env),
3101+
token_addr.into_val(&env)
3102+
],
3103+
250i128.into_val(&env)
3104+
)
3105+
]
3106+
);
3107+
3108+
assert_event_snapshot(
3109+
"fee_accrued",
3110+
&[SnapshotEvent {
3111+
topics: StdVec::from([
3112+
"u32:1".into(),
3113+
"symbol:fee_accrue".into(),
3114+
"address:token".into(),
3115+
]),
3116+
data: "i128:250".into(),
3117+
}],
3118+
);
3119+
}
3120+
3121+
#[test]
3122+
fn test_event_snapshot_fees_withdrawn() {
3123+
let env = new_snapshot_env();
3124+
env.mock_all_auths();
3125+
let (contract_id, bridge, _, token_addr, _, token_sac) = setup_bridge(&env, 1_000);
3126+
let recipient = Address::generate(&env);
3127+
3128+
token_sac.mint(&contract_id, &400);
3129+
env.as_contract(&contract_id, || {
3130+
env.storage()
3131+
.persistent()
3132+
.set(&DataKey::FeeVault(token_addr.clone()), &400i128);
3133+
});
3134+
3135+
bridge.withdraw_fees(&recipient, &token_addr, &150);
3136+
3137+
assert_eq!(
3138+
env.events().all().filter_by_contract(&contract_id),
3139+
vec![
3140+
&env,
3141+
(
3142+
contract_id,
3143+
vec![
3144+
&env,
3145+
EVENT_VERSION.into_val(&env),
3146+
Symbol::new(&env, "fee_wdrw").into_val(&env),
3147+
recipient.into_val(&env)
3148+
],
3149+
150i128.into_val(&env)
3150+
)
3151+
]
3152+
);
3153+
3154+
assert_event_snapshot(
3155+
"fees_withdrawn",
3156+
&[SnapshotEvent {
3157+
topics: StdVec::from([
3158+
"u32:1".into(),
3159+
"symbol:fee_wdrw".into(),
3160+
"address:recipient".into(),
3161+
]),
3162+
data: "i128:150".into(),
3163+
}],
3164+
);
3165+
}
3166+
28143167
#[cfg(test)]
28153168
mod proptest_deposit {
28163169
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)