Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions creator-keys/tests/assert_event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
//! Unit tests for the `assert_event` test helper.
//!
//! Confirms the helper:
//! - Passes silently when contract ID, topic, and all fields match.
//! - Panics when the topic does not match (filters by topic).
//! - Panics when the contract ID does not match (filters by contract).
//! - Panics when a field value mismatches, displaying the topic, expected, and actual fields.
//! - Panics when no events have been emitted at all.

mod contract_test_env;

use contract_test_env::{assert_event, register_creator_keys, test_env_with_auths};
use creator_keys::events;
use soroban_sdk::{
symbol_short,
testutils::{Address as _, Events, Ledger},
Address, Env,
};
use std::panic;

fn setup_initialized_contract(
env: &Env,
) -> (Address, Address, Address, events::ContractInitializedEvent) {
let (client, contract_id) = register_creator_keys(env);
let admin = Address::generate(env);
let recipient = Address::generate(env);
client.set_protocol_admin(&admin, &admin);
client.set_protocol_fee_recipient(&admin, &recipient);

let test_ledger = 42u32;
let mut ledger_info = env.ledger().get();
ledger_info.sequence_number = test_ledger;
env.ledger().set(ledger_info);

env.events().all();
client.set_fee_config(&admin, &9000u32, &1000u32);

let expected = events::ContractInitializedEvent {
admin,
protocol_fee_bps: 1000u32,
protocol_fee_recipient: recipient,
initialized_at_ledger: test_ledger,
};

(
contract_id,
expected.admin.clone(),
expected.protocol_fee_recipient.clone(),
expected,
)
}

#[test]
fn test_assert_event_matching_succeeds() {
let env = test_env_with_auths();
let (contract_id, _, _, expected) = setup_initialized_contract(&env);

assert_event(
&env,
&contract_id,
events::CONTRACT_INITIALIZED_EVENT_NAME,
&expected,
);
}

#[test]
fn test_assert_event_filters_out_wrong_topic() {
let env = test_env_with_auths();
let (contract_id, _, _, expected) = setup_initialized_contract(&env);

let wrong_topic = symbol_short!("wrong_top");

let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
assert_event(&env, &contract_id, wrong_topic, &expected);
}));

assert!(
result.is_err(),
"assert_event should panic when topic does not match"
);
let err = result.unwrap_err();
let msg = err
.downcast_ref::<std::string::String>()
.cloned()
.or_else(|| {
err.downcast_ref::<&str>()
.map(|s| std::string::String::from(*s))
})
.unwrap_or_default();

assert!(
msg.contains("no matching event found"),
"panic message should state that no matching event was found: {msg}"
);
assert!(
msg.contains("wrong_top"),
"panic message should mention the queried topic: {msg}"
);
}

#[test]
fn test_assert_event_filters_out_wrong_contract() {
let env = test_env_with_auths();
let (_contract_id, _, _, expected) = setup_initialized_contract(&env);
let wrong_contract_id = Address::generate(&env);

let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
assert_event(
&env,
&wrong_contract_id,
events::CONTRACT_INITIALIZED_EVENT_NAME,
&expected,
);
}));

assert!(
result.is_err(),
"assert_event should panic when contract_id does not match"
);
let err = result.unwrap_err();
let msg = err
.downcast_ref::<std::string::String>()
.cloned()
.or_else(|| {
err.downcast_ref::<&str>()
.map(|s| std::string::String::from(*s))
})
.unwrap_or_default();

assert!(
msg.contains("no matching event found"),
"panic message should state that no matching event was found: {msg}"
);
}

#[test]
fn test_assert_event_fails_on_field_mismatch() {
let env = test_env_with_auths();
let (contract_id, _, _, mut expected) = setup_initialized_contract(&env);

// Tamper with one field
expected.protocol_fee_bps = 9999u32;

let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
assert_event(
&env,
&contract_id,
events::CONTRACT_INITIALIZED_EVENT_NAME,
&expected,
);
}));

assert!(
result.is_err(),
"assert_event should panic on field value mismatch"
);
let err = result.unwrap_err();
let msg = err
.downcast_ref::<std::string::String>()
.cloned()
.or_else(|| {
err.downcast_ref::<&str>()
.map(|s| std::string::String::from(*s))
})
.unwrap_or_default();

assert!(
msg.contains("event field mismatch"),
"panic message should indicate field mismatch: {msg}"
);
assert!(
msg.contains("init"),
"panic message should include the topic: {msg}"
);
assert!(
msg.contains("9999"),
"panic message should include expected fields with the tampered value: {msg}"
);
assert!(
msg.contains("1000"),
"panic message should include actual fields with the real value: {msg}"
);
}

#[test]
fn test_assert_event_panics_when_no_events_at_all() {
let env = test_env_with_auths();
let contract_id = Address::generate(&env);
let expected = events::ContractInitializedEvent {
admin: Address::generate(&env),
protocol_fee_bps: 1000u32,
protocol_fee_recipient: Address::generate(&env),
initialized_at_ledger: 1,
};

let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
assert_event(
&env,
&contract_id,
events::CONTRACT_INITIALIZED_EVENT_NAME,
&expected,
);
}));

assert!(
result.is_err(),
"assert_event should panic when no events exist"
);
let err = result.unwrap_err();
let msg = err
.downcast_ref::<std::string::String>()
.cloned()
.or_else(|| {
err.downcast_ref::<&str>()
.map(|s| std::string::String::from(*s))
})
.unwrap_or_default();

assert!(
msg.contains("no matching event found"),
"panic message should state that no matching event was found: {msg}"
);
}
44 changes: 14 additions & 30 deletions creator-keys/tests/contract_initialization_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

mod contract_test_env;

use contract_test_env::{register_creator_keys, test_env_with_auths};
use contract_test_env::{assert_event, register_creator_keys, test_env_with_auths};
use creator_keys::events;
use soroban_sdk::{
testutils::{Address as _, Events, Ledger},
Expand All @@ -16,7 +16,7 @@ use soroban_sdk::{
#[test]
fn test_initialization_event_emitted_on_first_set_fee_config() {
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);
let (client, contract_id) = register_creator_keys(&env);

let admin = Address::generate(&env);
let recipient = Address::generate(&env);
Expand All @@ -31,34 +31,18 @@ fn test_initialization_event_emitted_on_first_set_fee_config() {
env.events().all();
client.set_fee_config(&admin, &9000u32, &1000u32);

let event_log = env.events().all();
let (_, _, data) = event_log
.iter()
.find(|(_, topics, _)| {
topics
.get(events::TOPIC_EVENT_NAME_INDEX)
.map(|v| {
let name: Symbol = v.into_val(&env);
name == events::CONTRACT_INITIALIZED_EVENT_NAME
})
.unwrap_or(false)
})
.expect("initialization event must be emitted on first set_fee_config");

let payload: events::ContractInitializedEvent = data.into_val(&env);

assert_eq!(payload.admin, admin, "admin field must match caller");
assert_eq!(
payload.protocol_fee_bps, 1000u32,
"protocol_fee_bps must match"
);
assert_eq!(
payload.protocol_fee_recipient, recipient,
"protocol_fee_recipient must match stored recipient"
);
assert_eq!(
payload.initialized_at_ledger, test_ledger,
"initialized_at_ledger must match current ledger"
let expected = events::ContractInitializedEvent {
admin,
protocol_fee_bps: 1000u32,
protocol_fee_recipient: recipient,
initialized_at_ledger: test_ledger,
};

assert_event(
&env,
&contract_id,
events::CONTRACT_INITIALIZED_EVENT_NAME,
&expected,
);
}

Expand Down
54 changes: 52 additions & 2 deletions creator-keys/tests/contract_test_env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

use creator_keys::{constants, CreatorKeysContract, CreatorKeysContractClient};
use soroban_sdk::{
testutils::{Address as _, Ledger},
Address, Env, IntoVal, String, Val,
testutils::{Address as _, Events, Ledger},
Address, Env, IntoVal, String, Symbol, TryFromVal, Val,
};
use std::string::String as StdString;

Expand Down Expand Up @@ -189,6 +189,56 @@ where
);
}

/// Asserts that a contract event matching `contract_id` and `topic` was emitted,
/// and that its payload fields match `expected_fields`.
///
/// Filters the environment's event log for events emitted by `contract_id` where the
/// canonical event topic (index 0) equals `topic`. If no matching event is found, or if
/// the event payload does not equal `expected_fields`, panics with a detailed, descriptive message.
///
/// # Panics
/// - If no event from `contract_id` with `topic` is found in the log.
/// - If the event payload cannot be deserialized into `T`.
/// - If the deserialized payload does not equal `expected_fields`.
pub fn assert_event<T>(env: &Env, contract_id: &Address, topic: Symbol, expected_fields: &T)
where
T: TryFromVal<Env, Val> + PartialEq + core::fmt::Debug,
{
let matching_event = env.events().all().iter().rev().find(|(c, topics, _)| {
c == contract_id
&& topics
.get(0)
.and_then(|v| Symbol::try_from_val(env, &v).ok())
.map(|s| s == topic)
.unwrap_or(false)
});

let (_, _, data) = match matching_event {
Some(e) => e,
None => panic!(
"assert_event failed: no matching event found for contract {:?} and topic {:?}.\nExpected fields: {:#?}",
contract_id, topic, expected_fields
),
};

let actual: T = match T::try_from_val(env, &data) {
Ok(val) => val,
Err(_) => panic!(
"assert_event failed: could not deserialize event data for contract {:?} and topic {:?}.\nExpected fields: {:#?}",
contract_id, topic, expected_fields
),
};

assert_eq!(
&actual,
expected_fields,
"assert_event failed: event field mismatch for topic {:?}.\nExpected: {:#?}\nActual: {:#?}",
topic,
expected_fields,
actual
);
}

fn account_strkey_from_seed(seed: &str) -> StdString {
let mut payload = [0u8; 32];
let mut state = 0xcbf2_9ce4_8422_2325u64;
Expand Down
Loading
Loading