From ac3b71ba65bb52b2d85f1ab9f6c00316a1a885ab Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Tue, 23 Dec 2025 15:20:00 +0000 Subject: test: prepare OwnerStateDataPushed fixture for purgatory This is the model for how to prepare all push tests for purgatory --- grasp-audit/src/client.rs | 45 ++++++++++++++++++- grasp-audit/src/fixtures.rs | 51 ++++++++++++++-------- .../src/specs/grasp01/push_authorization.rs | 7 +-- 3 files changed, 82 insertions(+), 21 deletions(-) (limited to 'grasp-audit/src') diff --git a/grasp-audit/src/client.rs b/grasp-audit/src/client.rs index 259a317..5995483 100644 --- a/grasp-audit/src/client.rs +++ b/grasp-audit/src/client.rs @@ -2,7 +2,7 @@ use crate::audit::{AuditConfig, AuditEventBuilder, AuditMode}; use crate::fixtures::FixtureKind; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use nostr_sdk::prelude::*; use std::collections::HashMap; use std::sync::{Arc, Mutex}; @@ -181,6 +181,49 @@ impl AuditClient { Ok(event_id) } + /// Send an event (with audit tags automatically added) and expect OK, success, 'purgatory:', verify not served - TODO the msg verificaiton is not implemented + pub async fn send_event_expect_purgatory_not_served(&self, event: Event) -> Result { + if self.config.read_only { + return Err(anyhow!("Client is in read-only mode")); + } + + let output = self.client.send_event(&event).await?; + let event_id = *output.id(); + + // Check if any relay rejected the event and return the error message + if !output.failed.is_empty() { + // Get the first failed relay error message + let (relay_url, error) = output.failed.iter().next().unwrap(); + return Err(anyhow!("Relay {} rejected event: {}", relay_url, error)); + } + + // Wait a bit for event to propagate + tokio::time::sleep(Duration::from_millis(300)).await; + + // ------------------------------------------------------ + // TODO Magically enable purgatory by uncommenting this: + // ------------------------------------------------------ + // ------------------------------------------------------ + // if !self.is_event_on_relay(event.id).await? { + // return Err(anyhow!( + // "event sent to relay was served instead of being put in purgatory" + // )); + // } + // ------------------------------------------------------ + + Ok(event_id) + } + + /// check if an event is on the relay + pub async fn is_event_on_relay(&self, id: EventId) -> Result { + Ok(!self + .client + .fetch_events(vec![Filter::new().id(id)], Duration::from_secs(1)) + .await + .context("error trying to query relay for event")? + .is_empty()) + } + /// Create an event builder that automatically includes audit tags /// /// All events built through this method will automatically have audit tags appended diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index 62f93e8..c5eb5f8 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs @@ -214,11 +214,12 @@ pub enum FixtureKind { /// Owner's state event with git data successfully pushed (full 4-stage fixture) /// - /// This fixture represents the complete flow for testing push authorization: + /// This fixture represents the complete flow for testing state push authorization: /// 1. **Generated**: Creates RepoState (repo announcement + state event) - /// 2. **Sent**: Sends events to relay - /// 3. **Verified**: Confirms events accepted by relay + /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message) + /// 3. **Verify Not Served**: Confirms event is not served by relays /// 4. **DataPushed**: Clones repo, creates deterministic commit, pushes to relay + /// 5. **Verified**: Confirms event is served by relay /// /// - Requires ValidRepo (uses same repo_id) /// - State event signed by owner keys (`client.keys()`) @@ -867,9 +868,10 @@ impl<'a> TestContext<'a> { /// /// This handles all stages of the fixture: /// 1. **Generated**: Creates RepoState (repo announcement + state event) - /// 2. **Sent**: Sends events to relay - /// 3. **Verified**: Confirms events accepted by relay + /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message) + /// 3. **Verify Not Served**: Confirms event is not served by relays /// 4. **DataPushed**: Clones repo, creates deterministic commit, pushes to relay + /// 5. **Verified**: Confirms event is served by relay /// /// # Returns /// The state event (kind 30618) after all stages complete successfully @@ -877,7 +879,7 @@ impl<'a> TestContext<'a> { use nostr_sdk::prelude::*; // ============================================================ - // Stage 1 & 2: ValidRepo is ensured by ensure_fixture before this is called + // Stage 1: ValidRepo is ensured by ensure_fixture before this is called // ============================================================ let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; let repo_id = self.extract_repo_id(&repo)?; @@ -902,13 +904,12 @@ impl<'a> TestContext<'a> { .build(self.client.keys()) .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e))?; - // Send state event to relay - self.client.send_event(state_event.clone()).await?; - // ============================================================ - // Stage 3: Verify state event was accepted + // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served // ============================================================ - tokio::time::sleep(std::time::Duration::from_millis(200)).await; + self.client + .send_event_expect_purgatory_not_served(state_event.clone()) + .await?; // ============================================================ // Stage 4: DataPushed - Clone repo, create commit, push @@ -1000,14 +1001,29 @@ impl<'a> TestContext<'a> { cleanup(&clone_path); match push_result { - Ok(true) => Ok(state_event), - Ok(false) => Err(anyhow::anyhow!( - "Push was rejected but should have been accepted. \ + Ok(res) => { + if !res { + return Err(anyhow::anyhow!( + "Push was rejected but should have been accepted. \ The state event points to commit {} which matches the pushed commit.", - DETERMINISTIC_COMMIT_HASH - )), - Err(e) => Err(anyhow::anyhow!("Push error: {}", e)), + DETERMINISTIC_COMMIT_HASH + )); + } + } + Err(e) => return Err(anyhow::anyhow!("Push error: {}", e)), + } + + // ============================================================ + // Stage 5: Verify state event is on relay + // ============================================================ + + tokio::time::sleep(Duration::from_millis(200)).await; + + if !self.client.is_event_on_relay(state_event.id).await? { + return Err(anyhow::anyhow!("state event not released from purgatory")); } + + Ok(state_event) } /// Build MaintainerStateDataPushed fixture: full 4-stage fixture for maintainer push authorization @@ -1667,6 +1683,7 @@ pub async fn send_and_verify_rejected( use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; +use std::time::Duration; /// Clone a repository from the relay and return the path /// diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index 4f21e70..6726fe5 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs @@ -486,9 +486,10 @@ impl PushAuthorizationTests { /// /// This test uses the OwnerStateDataPushed fixture which handles all 4 stages: /// 1. **Generated**: Creates RepoState (repo announcement + state event) - /// 2. **Sent**: Sends events to relay - /// 3. **Verified**: Confirms events accepted by relay + /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message) + /// 3. **Verify Not Served**: Confirms event is not served by relays /// 4. **DataPushed**: Clones repo, creates deterministic commit, pushes to relay + /// 5. **Verified**: Confirms event is served by relay /// /// The test wraps the fixture result in pass/fail using the error message. #[allow(unused_variables)] // relay_domain is now handled by fixture @@ -504,7 +505,7 @@ impl PushAuthorizationTests { match ctx.get_fixture(FixtureKind::OwnerStateDataPushed).await { Ok(_state_event) => TestResult::new( test_name, - "GRASP-01:git-http:36", + "GRASP-01:git-http:36", // TODO do we add purgatory line here? "Push authorized with matching state", ) .pass(), -- cgit v1.2.3