diff --git a/creator-keys/tests/assert_event.rs b/creator-keys/tests/assert_event.rs new file mode 100644 index 00000000..bb371c94 --- /dev/null +++ b/creator-keys/tests/assert_event.rs @@ -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::() + .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::() + .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::() + .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::() + .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}" + ); +} diff --git a/creator-keys/tests/contract_initialization_event.rs b/creator-keys/tests/contract_initialization_event.rs index 641e70fa..6b23b51b 100644 --- a/creator-keys/tests/contract_initialization_event.rs +++ b/creator-keys/tests/contract_initialization_event.rs @@ -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}, @@ -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); @@ -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, ); } diff --git a/creator-keys/tests/contract_test_env/mod.rs b/creator-keys/tests/contract_test_env/mod.rs index 3160fe82..b8f68424 100644 --- a/creator-keys/tests/contract_test_env/mod.rs +++ b/creator-keys/tests/contract_test_env/mod.rs @@ -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; @@ -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(env: &Env, contract_id: &Address, topic: Symbol, expected_fields: &T) +where + T: TryFromVal + 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; diff --git a/creator-keys/tests/creator_registered_event_fields.rs b/creator-keys/tests/creator_registered_event_fields.rs index 5d411051..b48e5a48 100644 --- a/creator-keys/tests/creator_registered_event_fields.rs +++ b/creator-keys/tests/creator_registered_event_fields.rs @@ -10,7 +10,9 @@ mod contract_test_env; -use contract_test_env::{register_creator_keys, set_ledger_sequence, test_env_with_auths}; +use contract_test_env::{ + assert_event, register_creator_keys, set_ledger_sequence, test_env_with_auths, +}; use creator_keys::events; use soroban_sdk::{ testutils::{Address as _, Events}, @@ -158,7 +160,7 @@ fn test_creator_registered_event_registered_at_ledger_matches_current_ledger() { #[test] fn test_creator_registered_event_all_four_fields_present_in_one_registration() { let env = test_env_with_auths(); - let (client, _) = register_creator_keys(&env); + let (client, contract_id) = register_creator_keys(&env); let ledger_seq: u32 = 100; set_ledger_sequence(&env, ledger_seq); @@ -167,25 +169,18 @@ fn test_creator_registered_event_all_four_fields_present_in_one_registration() { let handle_str = "frank"; register_creator(&client, &env, &creator, handle_str); - let payload = last_registration_event(&env); - - assert_eq!( - payload.creator, creator, - "creator (creator_wallet) field mismatch" - ); - assert_eq!( - payload.handle, - String::from_str(&env, handle_str), - "handle (display_name) field mismatch" - ); - assert_eq!( - payload.fee_recipient, creator, - "fee_recipient field mismatch — must default to creator wallet" - ); - assert_eq!( - payload.registered_at_ledger, ledger_seq, - "registered_at_ledger field mismatch" - ); + let expected = events::CreatorRegisteredEvent { + creator: creator.clone(), + handle: String::from_str(&env, handle_str), + supply: 0, + holder_count: 0, + creator_bps: 0, + protocol_bps: 0, + fee_recipient: creator, + registered_at_ledger: ledger_seq, + }; + + assert_event(&env, &contract_id, events::REGISTER_EVENT_NAME, &expected); } // ── AC2: no event emitted on failed registration (duplicate wallet) ───────────