diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index b57fcb40..25511af2 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -396,7 +396,7 @@ impl GistRegistry { mod tests { use super::*; use soroban_sdk::testutils::{Address as _, Events, Ledger}; - use soroban_sdk::{Env, String}; + use soroban_sdk::{Env, String, TryIntoVal, Val}; fn setup(env: &Env) -> GistRegistryClient<'_> { let contract_id = env.register(GistRegistry, ()); @@ -883,4 +883,171 @@ mod tests { assert!(!client.is_active(&id)); assert_eq!(client.list_gists_by_cell(&location, &0, &10).len(), 0); } + + // -- event emission ------------------------------------------------------ + // + // Every state-changing entry point publishes exactly one contract event so + // the API layer and explorers can follow the registry. These tests pin down + // the topic and the payload of each one; a silent rename of a topic symbol + // breaks every downstream indexer, so the assertions are deliberately + // literal. + + /// Decode the most recently published event as `(topic, data)`. + /// + /// Works whether `env.events().all()` is cumulative or per-invocation, + /// because the interesting event is always the last one published. + fn last_event(env: &Env) -> (Symbol, Val) { + let events = env.events().all(); + let (_, topics, data) = events.get(events.len() - 1).unwrap(); + let topic = topics.get(0).unwrap().try_into_val(env).unwrap(); + (topic, data) + } + + /// `report_gist` publishes its payload as a `(gist_id, count)` pair. + fn decode_report_payload(env: &Env, data: Val) -> (u64, u32) { + let parts: Vec = data.try_into_val(env).unwrap(); + ( + parts.get(0).unwrap().try_into_val(env).unwrap(), + parts.get(1).unwrap().try_into_val(env).unwrap(), + ) + } + + // -- #1427: edit / delete ------------------------------------------------ + + #[test] + fn editing_a_gist_emits_gist_edited() { + let env = Env::default(); + env.mock_all_auths(); + let client = setup(&env); + client.initialize(&Address::generate(&env)); + + let author = Address::generate(&env); + let id = client.post_gist( + &Some(author), + &cell(&env, "r3gx"), + &cell(&env, "Qm1"), + &None, + ); + let new_hash = cell(&env, "Qm2"); + client.edit_gist(&id, &new_hash); + + let (topic, data) = last_event(&env); + assert_eq!(topic, Symbol::new(&env, "gist_edited")); + + // The payload is the full updated gist, so an indexer can pick up the + // new content pointer without a follow-up read. + let gist: Gist = data.try_into_val(&env).unwrap(); + assert_eq!(gist.gist_id, id); + assert_eq!(gist.content_hash, new_hash); + } + + #[test] + fn deleting_a_gist_emits_gist_deleted() { + let env = Env::default(); + env.mock_all_auths(); + let client = setup(&env); + client.initialize(&Address::generate(&env)); + + let author = Address::generate(&env); + let id = client.post_gist( + &Some(author), + &cell(&env, "r3gx"), + &cell(&env, "Qm1"), + &None, + ); + client.delete_gist(&id); + + let (topic, data) = last_event(&env); + assert_eq!(topic, Symbol::new(&env, "gist_deleted")); + + let payload: u64 = data.try_into_val(&env).unwrap(); + assert_eq!(payload, id); + + // and the deletion itself is observable + assert!(client.get_gist(&id).is_none()); + } + + // -- #1428: hide / unhide / remove / report ------------------------------ + + #[test] + fn hiding_a_gist_emits_gist_hidden() { + let env = Env::default(); + env.mock_all_auths(); + let client = setup(&env); + client.initialize(&Address::generate(&env)); + + let id = client.post_gist(&None, &cell(&env, "r3gx"), &cell(&env, "Qm1"), &None); + client.hide_gist(&id); + + let (topic, data) = last_event(&env); + assert_eq!(topic, Symbol::new(&env, "gist_hidden")); + + let payload: u64 = data.try_into_val(&env).unwrap(); + assert_eq!(payload, id); + } + + #[test] + fn unhiding_a_gist_emits_gist_unhidden() { + let env = Env::default(); + env.mock_all_auths(); + let client = setup(&env); + client.initialize(&Address::generate(&env)); + + let id = client.post_gist(&None, &cell(&env, "r3gx"), &cell(&env, "Qm1"), &None); + client.hide_gist(&id); + client.unhide_gist(&id); + + // the last of the two events is the unhide, not the hide + let (topic, data) = last_event(&env); + assert_eq!(topic, Symbol::new(&env, "gist_unhidden")); + + let payload: u64 = data.try_into_val(&env).unwrap(); + assert_eq!(payload, id); + } + + #[test] + fn removing_a_gist_emits_gist_removed() { + let env = Env::default(); + env.mock_all_auths(); + let client = setup(&env); + client.initialize(&Address::generate(&env)); + + let id = client.post_gist(&None, &cell(&env, "r3gx"), &cell(&env, "Qm1"), &None); + client.remove_gist(&id); + + let (topic, data) = last_event(&env); + assert_eq!(topic, Symbol::new(&env, "gist_removed")); + + let payload: u64 = data.try_into_val(&env).unwrap(); + assert_eq!(payload, id); + + assert!(client.get_gist(&id).is_none()); + } + + #[test] + fn reporting_a_gist_emits_gist_reported_with_a_running_count() { + let env = Env::default(); + env.mock_all_auths(); + let client = setup(&env); + client.initialize(&Address::generate(&env)); + + let id = client.post_gist(&None, &cell(&env, "r3gx"), &cell(&env, "Qm1"), &None); + + assert_eq!(client.report_gist(&id), 1); + let (topic, data) = last_event(&env); + assert_eq!(topic, Symbol::new(&env, "gist_reported")); + let (reported_id, count) = decode_report_payload(&env, data); + assert_eq!(reported_id, id); + assert_eq!(count, 1); + + // A second report moves both the return value and the published count. + assert_eq!(client.report_gist(&id), 2); + let (topic, data) = last_event(&env); + assert_eq!(topic, Symbol::new(&env, "gist_reported")); + let (reported_id, count) = decode_report_payload(&env, data); + assert_eq!(reported_id, id); + assert_eq!(count, 2); + + assert_eq!(client.report_count(&id), 2); + } }