From bf7f4d5381203d5c27b2811d62c5b1781533aa2b Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 19 Nov 2025 17:01:36 +0000 Subject: fix some clippy fmt warnings --- grasp-audit/src/audit.rs | 84 +-- grasp-audit/src/bin/grasp-audit.rs | 36 +- grasp-audit/src/client.rs | 174 +++-- grasp-audit/src/fixtures.rs | 162 +++-- grasp-audit/src/isolation.rs | 12 +- grasp-audit/src/lib.rs | 4 +- grasp-audit/src/result.rs | 57 +- .../src/specs/grasp01/event_acceptance_policy.rs | 729 +++++++++++++-------- grasp-audit/src/specs/grasp01/mod.rs | 2 +- grasp-audit/src/specs/grasp01/nip01_smoke.rs | 112 ++-- grasp-audit/src/specs/grasp01/nip11_document.rs | 71 +- grasp-audit/src/specs/mod.rs | 6 +- 12 files changed, 844 insertions(+), 605 deletions(-) (limited to 'grasp-audit/src') diff --git a/grasp-audit/src/audit.rs b/grasp-audit/src/audit.rs index 8afe660..5e84409 100644 --- a/grasp-audit/src/audit.rs +++ b/grasp-audit/src/audit.rs @@ -7,13 +7,13 @@ use nostr_sdk::prelude::*; pub struct AuditConfig { /// Unique ID for this audit run pub run_id: String, - + /// Mode: CI (isolated) or Production (live) pub mode: AuditMode, - + /// Cleanup timestamp (events can be cleaned after this) pub cleanup_after: Timestamp, - + /// Whether to actually create events or just query pub read_only: bool, } @@ -23,7 +23,7 @@ pub struct AuditConfig { pub enum AuditMode { /// Isolated CI/CD tests - only see own events CI, - + /// Production audit - see all events, minimal writes Production, } @@ -39,7 +39,7 @@ impl AuditConfig { read_only: false, } } - + /// Create config for production audit pub fn production() -> Self { let run_id = format!("prod-audit-{}", Timestamp::now().as_u64()); @@ -47,10 +47,10 @@ impl AuditConfig { run_id, mode: AuditMode::Production, cleanup_after: Timestamp::now() + 300, // 5 minutes from now - read_only: true, // Default to read-only for production + read_only: true, // Default to read-only for production } } - + /// Create config with custom run ID pub fn with_run_id(run_id: String, mode: AuditMode) -> Self { Self { @@ -60,7 +60,7 @@ impl AuditConfig { read_only: mode == AuditMode::Production, } } - + /// Get audit tags that are automatically added to all events /// /// These tags are automatically added to all events created via [`AuditEventBuilder`]. @@ -102,22 +102,22 @@ impl AuditConfig { /// ``` pub fn audit_tags(&self) -> Vec { use nostr_sdk::prelude::{Alphabet, SingleLetterTag}; - + // Use "t" tags for categorization (standard NIP-01 hashtag type) let t_tag = SingleLetterTag::lowercase(Alphabet::T); - + vec![ + Tag::custom(TagKind::SingleLetter(t_tag), vec!["grasp-audit-test-event"]), Tag::custom( TagKind::SingleLetter(t_tag), - vec!["grasp-audit-test-event"] + vec![format!("audit-{}", self.run_id)], ), Tag::custom( TagKind::SingleLetter(t_tag), - vec![format!("audit-{}", self.run_id)] - ), - Tag::custom( - TagKind::SingleLetter(t_tag), - vec![format!("audit-cleanup-after-{}", self.cleanup_after.as_u64())] + vec![format!( + "audit-cleanup-after-{}", + self.cleanup_after.as_u64() + )], ), ] } @@ -141,28 +141,28 @@ impl AuditEventBuilder { config, } } - + /// Add a tag pub fn tag(mut self, tag: Tag) -> Self { self.tags.push(tag); self } - + /// Add multiple tags pub fn tags(mut self, tags: Vec) -> Self { self.tags.extend(tags); self } - + /// Build the event with audit tags pub fn build(self, keys: &Keys) -> anyhow::Result { let mut all_tags = self.tags; all_tags.extend(self.config.audit_tags()); - + let event = EventBuilder::new(self.kind, self.content) .tags(all_tags) .sign_with_keys(keys)?; - + Ok(event) } } @@ -170,7 +170,7 @@ impl AuditEventBuilder { #[cfg(test)] mod tests { use super::*; - + #[test] fn test_ci_config() { let config = AuditConfig::ci(); @@ -178,7 +178,7 @@ mod tests { assert!(!config.read_only); assert!(config.run_id.starts_with("ci-")); } - + #[test] fn test_production_config() { let config = AuditConfig::production(); @@ -186,18 +186,18 @@ mod tests { assert!(config.read_only); assert!(config.run_id.starts_with("prod-audit-")); } - + #[test] fn test_audit_tags() { use nostr_sdk::prelude::{Alphabet, SingleLetterTag}; - + let config = AuditConfig::ci(); let tags = config.audit_tags(); - + assert_eq!(tags.len(), 3); - + let t_tag = SingleLetterTag::lowercase(Alphabet::T); - + // All tags should be "t" tags (hashtags) for tag in &tags { if let TagKind::SingleLetter(letter) = tag.kind() { @@ -206,36 +206,40 @@ mod tests { panic!("Expected SingleLetter tag"); } } - + // Check for "t" tag with "grasp-audit-test-event" - assert!(tags.iter().any(|t| { - t.content() == Some("grasp-audit-test-event") - })); - + assert!(tags + .iter() + .any(|t| { t.content() == Some("grasp-audit-test-event") })); + // Check for "t" tag with "audit-{run_id}" assert!(tags.iter().any(|t| { - t.content().map(|c| c.starts_with("audit-ci-")).unwrap_or(false) + t.content() + .map(|c| c.starts_with("audit-ci-")) + .unwrap_or(false) })); - + // Check for "t" tag with "audit-cleanup-after-{timestamp}" assert!(tags.iter().any(|t| { - t.content().map(|c| c.starts_with("audit-cleanup-after-")).unwrap_or(false) + t.content() + .map(|c| c.starts_with("audit-cleanup-after-")) + .unwrap_or(false) })); } - + #[test] fn test_audit_event_builder() { let config = AuditConfig::ci(); let keys = Keys::generate(); - + let event = AuditEventBuilder::new(Kind::TextNote, "test", config.clone()) .tag(Tag::custom(TagKind::Custom("test".into()), vec!["value"])) .build(&keys) .unwrap(); - + // Should have our custom tag + 3 audit tags assert!(event.tags.len() >= 4); - + // Verify event is valid assert!(event.verify().is_ok()); } diff --git a/grasp-audit/src/bin/grasp-audit.rs b/grasp-audit/src/bin/grasp-audit.rs index 6c063db..b56a8e3 100644 --- a/grasp-audit/src/bin/grasp-audit.rs +++ b/grasp-audit/src/bin/grasp-audit.rs @@ -18,11 +18,11 @@ enum Commands { /// Relay URL (e.g., ws://localhost:7000) #[arg(short, long)] relay: String, - + /// Mode: ci or production #[arg(short, long, default_value = "ci")] mode: String, - + /// Spec to test (nip01-smoke, all) #[arg(short, long, default_value = "nip01-smoke")] spec: String, @@ -35,12 +35,12 @@ async fn main() -> Result<()> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::from_default_env() - .add_directive(tracing::Level::INFO.into()) + .add_directive(tracing::Level::INFO.into()), ) .init(); - + let cli = Cli::parse(); - + match cli.command { Commands::Audit { relay, mode, spec } => { let config = match mode.as_str() { @@ -48,7 +48,7 @@ async fn main() -> Result<()> { "production" => AuditConfig::production(), _ => return Err(anyhow!("Invalid mode: {}. Use 'ci' or 'production'", mode)), }; - + println!("🔍 GRASP Audit Tool"); println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); println!("Relay: {}", relay); @@ -57,17 +57,18 @@ async fn main() -> Result<()> { println!("Run ID: {}", config.run_id); println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); println!(); - + println!("Connecting to relay..."); - let client = AuditClient::new(&relay, config).await + let client = AuditClient::new(&relay, config) + .await .map_err(|e| anyhow!("Failed to connect to relay: {}", e))?; - + if !client.is_connected().await { return Err(anyhow!("Could not establish connection to relay")); } - + println!("✓ Connected\n"); - + let results = match spec.as_str() { "nip01-smoke" => { println!("Running NIP-01 smoke tests...\n"); @@ -77,11 +78,16 @@ async fn main() -> Result<()> { println!("Running all tests...\n"); specs::Nip01SmokeTests::run_all(&client).await } - _ => return Err(anyhow!("Unknown spec: {}. Use 'nip01-smoke' or 'all'", spec)), + _ => { + return Err(anyhow!( + "Unknown spec: {}. Use 'nip01-smoke' or 'all'", + spec + )) + } }; - + results.print_report(); - + if !results.all_passed() { println!("❌ Some tests failed"); std::process::exit(1); @@ -90,6 +96,6 @@ async fn main() -> Result<()> { } } } - + Ok(()) } diff --git a/grasp-audit/src/client.rs b/grasp-audit/src/client.rs index 1f6f0fb..019f4cb 100644 --- a/grasp-audit/src/client.rs +++ b/grasp-audit/src/client.rs @@ -24,32 +24,32 @@ impl AuditClient { keys, } } - + /// Create a new audit client pub async fn new(relay_url: &str, config: AuditConfig) -> Result { let keys = Keys::generate(); let client = Client::new(keys.clone()); - + // Add relay and connect client.add_relay(relay_url).await?; client.connect().await; - + // Wait for connection to establish (with retries) let mut attempts = 0; let mut connected = false; while attempts < 20 { tokio::time::sleep(Duration::from_millis(100)).await; - + let relays = client.relays().await; connected = relays.values().any(|r| r.is_connected()); - + if connected { break; } - + attempts += 1; } - + // Verify we actually connected if !connected { return Err(anyhow!( @@ -68,22 +68,22 @@ impl AuditClient { relay_url )); } - + // Give it a bit more time to stabilize tokio::time::sleep(Duration::from_millis(200)).await; - + Ok(Self { client, config, keys, }) } - + /// Get the public key for this audit client pub fn public_key(&self) -> PublicKey { self.keys.public_key() } - + /// Check if connected to relay pub async fn is_connected(&self) -> bool { // Check if we have any connected relays @@ -95,29 +95,29 @@ impl AuditClient { } false } - + /// Send an event (with audit tags automatically added) pub async fn send_event(&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(100)).await; - + Ok(event_id) } - + /// Create an event builder that automatically includes audit tags /// /// All events built through this method will automatically have audit tags appended @@ -153,11 +153,11 @@ impl AuditClient { pub fn event_builder(&self, kind: Kind, content: impl Into) -> AuditEventBuilder { AuditEventBuilder::new(kind, content, self.config.clone()) } - + /// Query events, optionally filtered to this audit run pub async fn query(&self, mut filter: Filter) -> Result> { use nostr_sdk::prelude::{Alphabet, SingleLetterTag}; - + if self.config.mode == AuditMode::CI { // In CI mode, only see our own audit events // Filter by "t" tags (hashtags) @@ -167,14 +167,15 @@ impl AuditClient { .custom_tag(t_tag, format!("audit-{}", self.config.run_id)); } // In Production mode, see all events (no filter modification) - - let events = self.client + + let events = self + .client .fetch_events(filter, Duration::from_secs(5)) .await?; - + Ok(events.into_iter().collect()) } - + /// Subscribe to events with a callback pub async fn subscribe( &self, @@ -183,27 +184,25 @@ impl AuditClient { ) -> Result> { let timeout = timeout.unwrap_or(Duration::from_secs(5)); let mut all_events = Vec::new(); - + for filter in filters { - let events = self.client - .fetch_events(filter, timeout) - .await?; + let events = self.client.fetch_events(filter, timeout).await?; all_events.extend(events.into_iter()); } - + Ok(all_events) } - + /// Get the underlying nostr client (for advanced usage) pub fn client(&self) -> &Client { &self.client } - + /// Get the keys (for signing custom events) pub fn keys(&self) -> &Keys { &self.keys } - + /// Create a NIP-34 repository announcement event /// /// This helper creates a properly formatted NIP-34 announcement that will be @@ -216,37 +215,58 @@ impl AuditClient { /// A built and signed Event ready to be sent to the relay pub async fn create_repo_announcement(&self, test_name: &str) -> Result { // Get relay URL from client - let relay_url = self.client.relays().await + let relay_url = self + .client + .relays() + .await .keys() .next() .ok_or_else(|| anyhow!("No relay connected"))? .to_string(); - + // Convert WebSocket URL to HTTP URL for clone tag let http_url = relay_url .replace("ws://", "http://") .replace("wss://", "https://"); - + // Create unique repository identifier using UUID for consistency let repo_id = format!("{}-{}", test_name, &uuid::Uuid::new_v4().to_string()[..8]); - + // Get npub for clone URL - let npub = self.public_key().to_bech32() + let npub = self + .public_key() + .to_bech32() .map_err(|e| anyhow!("Failed to convert public key to bech32 npub format: {}", e))?; - + // Build kind 30617 repository announcement - let event = self.event_builder(Kind::GitRepoAnnouncement, format!("Test repository for {}", test_name)) + let event = self + .event_builder( + Kind::GitRepoAnnouncement, + format!("Test repository for {}", test_name), + ) .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom(TagKind::custom("name"), vec![format!("{} Test Repository", test_name)])) - .tag(Tag::custom(TagKind::custom("description"), vec![format!("Repository for {} testing", test_name)])) - .tag(Tag::custom(TagKind::custom("clone"), vec![format!("{}/{}/{}.git", http_url, npub, repo_id)])) - .tag(Tag::custom(TagKind::custom("relays"), vec![relay_url.clone()])) + .tag(Tag::custom( + TagKind::custom("name"), + vec![format!("{} Test Repository", test_name)], + )) + .tag(Tag::custom( + TagKind::custom("description"), + vec![format!("Repository for {} testing", test_name)], + )) + .tag(Tag::custom( + TagKind::custom("clone"), + vec![format!("{}/{}/{}.git", http_url, npub, repo_id)], + )) + .tag(Tag::custom( + TagKind::custom("relays"), + vec![relay_url.clone()], + )) .build(self.keys()) .map_err(|e| anyhow!("Failed to build repository announcement event: {}", e))?; - + Ok(event) } - + /// Create an issue (kind 1621) that references a repository /// /// # Arguments @@ -265,29 +285,31 @@ impl AuditClient { additional_tags: Vec, ) -> Result { // Extract repo_id from the d tag - let repo_id = repo_event.tags.iter() + let repo_id = repo_event + .tags + .iter() .find(|t| t.kind() == TagKind::d()) .and_then(|t| t.content()) .ok_or_else(|| anyhow!("Repository event must have a 'd' tag"))? .to_string(); - + let repo_pubkey = repo_event.pubkey; let a_tag_value = format!("30617:{}:{}", repo_pubkey, repo_id); - + let mut tags = vec![ Tag::custom(TagKind::custom("a"), vec![a_tag_value]), Tag::custom(TagKind::custom("subject"), vec![issue_title]), ]; - + // Add any additional tags tags.extend(additional_tags); - + self.event_builder(Kind::Custom(1621), content) .tags(tags) .build(self.keys()) .map_err(|e| anyhow!("Failed to build issue event: {}", e)) } - + /// Create a NIP-22 comment (kind 1111) for an event /// /// # Arguments @@ -306,17 +328,20 @@ impl AuditClient { let event_kind = event.kind; let event_pubkey = event.pubkey; let event_id = event.id; - + let mut tags = vec![ - Tag::custom(TagKind::custom("E"), vec![event_id.to_hex(), "".to_string(), "root".to_string()]), + Tag::custom( + TagKind::custom("E"), + vec![event_id.to_hex(), "".to_string(), "root".to_string()], + ), Tag::event(event_id), Tag::custom(TagKind::custom("K"), vec![event_kind.as_u16().to_string()]), Tag::public_key(event_pubkey), ]; - + // Add any additional tags tags.extend(additional_tags); - + self.event_builder(Kind::Custom(1111), content) .tags(tags) .build(self.keys()) @@ -327,22 +352,22 @@ impl AuditClient { #[cfg(test)] mod tests { use super::*; - + #[tokio::test] async fn test_client_creation() { let config = AuditConfig::ci(); - + // This will fail if no relay is running, which is expected in tests // In real usage, there should be a relay at the URL let result = AuditClient::new("ws://localhost:7000", config).await; - + // We can't test connection without a running relay // But we can test that the client is created if let Ok(client) = result { assert_eq!(client.config.mode, AuditMode::CI); } } - + #[test] fn test_event_builder() { let config = AuditConfig::ci(); @@ -352,13 +377,13 @@ mod tests { config: config.clone(), keys: keys.clone(), }; - + let _builder = client.event_builder(Kind::TextNote, "test content"); - + // Builder should be created successfully // (We can't test the internal config field as it's private, which is correct) } - + #[test] fn test_audit_tags_automatically_added() { let config = AuditConfig::ci(); @@ -368,21 +393,28 @@ mod tests { config: config.clone(), keys: keys.clone(), }; - + // Create an event with a custom tag - let event = client.event_builder(Kind::TextNote, "test content") + let event = client + .event_builder(Kind::TextNote, "test content") .tag(Tag::custom(TagKind::custom("custom"), vec!["value"])) .build(&keys) .unwrap(); - + // Should have custom tag (1) + 3 audit tags = at least 4 tags - assert!(event.tags.len() >= 4, "Expected at least 4 tags, got {}", event.tags.len()); - + assert!( + event.tags.len() >= 4, + "Expected at least 4 tags, got {}", + event.tags.len() + ); + // Verify audit tags are present by checking tag content - let tag_contents: Vec = event.tags.iter() + let tag_contents: Vec = event + .tags + .iter() .filter_map(|t| t.content().map(|s| s.to_string())) .collect(); - + // Check for the three required audit tags assert!( tag_contents.contains(&"grasp-audit-test-event".to_string()), @@ -393,10 +425,12 @@ mod tests { "Missing 'audit-ci-*' tag" ); assert!( - tag_contents.iter().any(|t| t.starts_with("audit-cleanup-after-")), + tag_contents + .iter() + .any(|t| t.starts_with("audit-cleanup-after-")), "Missing 'audit-cleanup-after-*' tag" ); - + // Verify the custom tag is also present assert!( tag_contents.contains(&"value".to_string()), diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index 71d64d3..8eee81f 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs @@ -33,13 +33,13 @@ use std::sync::{Arc, Mutex}; pub enum FixtureKind { /// Basic repository announcement (kind 30617) ValidRepo, - + /// Repository with one issue (kind 1621) RepoWithIssue, - + /// Repository with issue and comment (kind 1111) RepoWithComment, - + /// Repository state announcement (kind 30618) RepoState, } @@ -49,7 +49,7 @@ pub enum FixtureKind { pub enum ContextMode { /// Create fresh fixtures for each request (test isolation) Isolated, - + /// Reuse shared fixtures across requests (minimal events) Shared, } @@ -104,7 +104,7 @@ impl<'a> TestContext<'a> { cache: Arc::new(Mutex::new(HashMap::new())), } } - + /// Create a test context with explicit mode override /// /// This is useful for testing the context itself or for advanced use cases @@ -116,7 +116,7 @@ impl<'a> TestContext<'a> { cache: Arc::new(Mutex::new(HashMap::new())), } } - + /// Get a fixture, creating it if needed based on mode /// /// # Behavior @@ -139,7 +139,7 @@ impl<'a> TestContext<'a> { ContextMode::Shared => self.get_or_create_shared(kind).await, } } - + /// Get the underlying client for direct access /// /// This allows tests to use the client directly when needed while still @@ -147,23 +147,27 @@ impl<'a> TestContext<'a> { pub fn client(&self) -> &'a AuditClient { self.client } - + /// Get the current context mode pub fn mode(&self) -> ContextMode { self.mode } - + /// Create a fresh fixture (always creates new) async fn create_fresh(&self, kind: FixtureKind) -> Result { - let event = self.build_fixture(kind).await + let event = self + .build_fixture(kind) + .await .with_context(|| format!("Failed to build {:?} fixture", kind))?; - - self.client.send_event(event.clone()).await + + self.client + .send_event(event.clone()) + .await .with_context(|| format!("Failed to send {:?} fixture event to relay", kind))?; - + Ok(event) } - + /// Get or create a shared fixture (caches for reuse) async fn get_or_create_shared(&self, kind: FixtureKind) -> Result { // Check cache first @@ -173,39 +177,54 @@ impl<'a> TestContext<'a> { return Ok(event.clone()); } } - + // Not in cache, create it - let event = self.build_fixture(kind).await + let event = self + .build_fixture(kind) + .await .with_context(|| format!("Failed to build {:?} fixture for shared cache", kind))?; - - self.client.send_event(event.clone()).await - .with_context(|| format!("Failed to send {:?} fixture event to relay (shared cache)", kind))?; - + + self.client + .send_event(event.clone()) + .await + .with_context(|| { + format!( + "Failed to send {:?} fixture event to relay (shared cache)", + kind + ) + })?; + // Store in cache { let mut cache = self.cache.lock().unwrap(); cache.insert(kind, event.clone()); } - + Ok(event) } - + /// Build a fixture event (doesn't send it) async fn build_fixture(&self, kind: FixtureKind) -> Result { match kind { FixtureKind::ValidRepo => { - let test_name = format!("fixture-{:?}-{}", kind, &uuid::Uuid::new_v4().to_string()[..8]); + let test_name = format!( + "fixture-{:?}-{}", + kind, + &uuid::Uuid::new_v4().to_string()[..8] + ); self.client.create_repo_announcement(&test_name).await } - + FixtureKind::RepoWithIssue => { - use nostr_sdk::prelude::*; - // First create and send repo - let test_name = format!("fixture-{:?}-{}", FixtureKind::ValidRepo, &uuid::Uuid::new_v4().to_string()[..8]); + let test_name = format!( + "fixture-{:?}-{}", + FixtureKind::ValidRepo, + &uuid::Uuid::new_v4().to_string()[..8] + ); let repo = self.client.create_repo_announcement(&test_name).await?; self.client.send_event(repo.clone()).await?; - + // Then create issue referencing it - this will have 'a' tag to repo // Note: We build the issue but DON'T send it here - the caller will send it let issue = self.client.create_issue( @@ -214,64 +233,70 @@ impl<'a> TestContext<'a> { "Issue content for testing", vec![], )?; - + // Return the issue - tests can extract repo reference from its 'a' tag // The caller (create_fresh/get_or_create_shared) will send this event Ok(issue) } - + FixtureKind::RepoWithComment => { // First create repo with issue - let test_name = format!("fixture-{:?}-{}", FixtureKind::ValidRepo, &uuid::Uuid::new_v4().to_string()[..8]); + let test_name = format!( + "fixture-{:?}-{}", + FixtureKind::ValidRepo, + &uuid::Uuid::new_v4().to_string()[..8] + ); let repo = self.client.create_repo_announcement(&test_name).await?; self.client.send_event(repo.clone()).await?; - - let issue = self.client.create_issue( - &repo, - "Test Issue", - "Issue content", - vec![], - )?; + + let issue = + self.client + .create_issue(&repo, "Test Issue", "Issue content", vec![])?; self.client.send_event(issue.clone()).await?; - + // Then create comment on issue - self.client.create_comment( - &issue, - "Test comment", - vec![], - ) + self.client.create_comment(&issue, "Test comment", vec![]) } - + FixtureKind::RepoState => { use nostr_sdk::prelude::*; - + // First create repo announcement - let test_name = format!("fixture-{:?}-{}", FixtureKind::ValidRepo, &uuid::Uuid::new_v4().to_string()[..8]); + let test_name = format!( + "fixture-{:?}-{}", + FixtureKind::ValidRepo, + &uuid::Uuid::new_v4().to_string()[..8] + ); let repo = self.client.create_repo_announcement(&test_name).await?; self.client.send_event(repo.clone()).await?; - + // Extract repo_id from repo announcement - let repo_id = repo.tags.iter() + let repo_id = repo + .tags + .iter() .find(|t| t.kind() == TagKind::d()) .and_then(|t| t.content()) .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement"))? .to_string(); - + // Create state announcement - self.client.event_builder(Kind::Custom(30618), "") + self.client + .event_builder(Kind::Custom(30618), "") .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom(TagKind::custom("refs/heads/main"), vec![ - "abc123def456789012345678901234567890abcd" - ])) - .tag(Tag::custom(TagKind::custom("HEAD"), vec![ - "ref: refs/heads/main" - ])) + .tag(Tag::custom( + TagKind::custom("refs/heads/main"), + vec!["abc123def456789012345678901234567890abcd"], + )) + .tag(Tag::custom( + TagKind::custom("HEAD"), + vec!["ref: refs/heads/main"], + )) .build(self.client.keys()) .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e)) } } } - + /// Clear the fixture cache /// /// This is useful for tests that want to ensure fresh fixtures @@ -286,34 +311,37 @@ impl<'a> TestContext<'a> { mod tests { use super::*; use crate::AuditConfig; - + #[test] fn test_context_mode_from_audit_mode() { assert_eq!(ContextMode::from(AuditMode::CI), ContextMode::Isolated); - assert_eq!(ContextMode::from(AuditMode::Production), ContextMode::Shared); + assert_eq!( + ContextMode::from(AuditMode::Production), + ContextMode::Shared + ); } - + #[test] fn test_fixture_kind_hash() { use std::collections::HashSet; - + let mut set = HashSet::new(); set.insert(FixtureKind::ValidRepo); set.insert(FixtureKind::RepoWithIssue); - + assert!(set.contains(&FixtureKind::ValidRepo)); assert!(!set.contains(&FixtureKind::RepoWithComment)); } - + #[tokio::test] async fn test_context_creation() { let config = AuditConfig::ci(); let client = crate::AuditClient::new_test(config); - + let ctx = TestContext::new(&client); assert_eq!(ctx.mode(), ContextMode::Isolated); - + let ctx = TestContext::with_mode(&client, ContextMode::Shared); assert_eq!(ctx.mode(), ContextMode::Shared); } -} \ No newline at end of file +} diff --git a/grasp-audit/src/isolation.rs b/grasp-audit/src/isolation.rs index 540da34..d0a2645 100644 --- a/grasp-audit/src/isolation.rs +++ b/grasp-audit/src/isolation.rs @@ -11,7 +11,7 @@ pub fn generate_test_id() -> String { .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); - + format!("test-{}-{}", timestamp, counter) } @@ -26,29 +26,29 @@ pub fn generate_prod_run_id() -> String { .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); - + format!("prod-audit-{}", timestamp) } #[cfg(test)] mod tests { use super::*; - + #[test] fn test_generate_test_id() { let id1 = generate_test_id(); let id2 = generate_test_id(); - + assert_ne!(id1, id2); assert!(id1.starts_with("test-")); } - + #[test] fn test_generate_ci_run_id() { let id = generate_ci_run_id(); assert!(id.starts_with("ci-")); } - + #[test] fn test_generate_prod_run_id() { let id = generate_prod_run_id(); diff --git a/grasp-audit/src/lib.rs b/grasp-audit/src/lib.rs index 6eac73c..dd9ac32 100644 --- a/grasp-audit/src/lib.rs +++ b/grasp-audit/src/lib.rs @@ -29,15 +29,15 @@ //! ``` pub mod audit; -pub mod fixtures; pub mod client; +pub mod fixtures; pub mod isolation; pub mod result; pub mod specs; pub use audit::{AuditConfig, AuditMode}; -pub use fixtures::{ContextMode, FixtureKind, TestContext}; pub use client::AuditClient; +pub use fixtures::{ContextMode, FixtureKind, TestContext}; pub use result::{AuditResult, TestResult}; // Re-export commonly used types diff --git a/grasp-audit/src/result.rs b/grasp-audit/src/result.rs index f591304..de377e5 100644 --- a/grasp-audit/src/result.rs +++ b/grasp-audit/src/result.rs @@ -25,7 +25,7 @@ impl TestResult { duration: Duration::default(), } } - + /// Run a test function and capture the result pub async fn run(mut self, test_fn: F) -> Self where @@ -33,7 +33,7 @@ impl TestResult { Fut: std::future::Future>, { let start = Instant::now(); - + match test_fn().await { Ok(()) => { self.passed = true; @@ -43,17 +43,17 @@ impl TestResult { self.error = Some(e); } } - + self.duration = start.elapsed(); self } - + /// Mark test as passed pub fn pass(mut self) -> Self { self.passed = true; self } - + /// Mark test as failed with error pub fn fail(mut self, error: impl Into) -> Self { self.passed = false; @@ -77,68 +77,69 @@ impl AuditResult { results: Vec::new(), } } - + /// Add a test result pub fn add(&mut self, result: TestResult) { self.results.push(result); } - + /// Merge another audit result pub fn merge(&mut self, other: AuditResult) { self.results.extend(other.results); } - + /// Check if all tests passed pub fn all_passed(&self) -> bool { self.results.iter().all(|r| r.passed) } - + /// Get count of passed tests pub fn passed_count(&self) -> usize { self.results.iter().filter(|r| r.passed).count() } - + /// Get count of failed tests pub fn failed_count(&self) -> usize { self.results.iter().filter(|r| !r.passed).count() } - + /// Get total count of tests pub fn total_count(&self) -> usize { self.results.len() } - + /// Print a detailed report pub fn print_report(&self) { println!("\n{}", self.spec); println!("{}", "═".repeat(60)); println!(); - + let passed = self.passed_count(); let total = self.total_count(); - + for result in &self.results { let status = if result.passed { "✓" } else { "✗" }; - + println!("{} {} ({})", status, result.name, result.spec_ref); println!(" Requirement: {}", result.requirement); - + if let Some(error) = &result.error { println!(" Error: {}", error); } - + println!(" Duration: {:?}", result.duration); println!(); } - - println!("Results: {}/{} passed ({:.1}%)", - passed, + + println!( + "Results: {}/{} passed ({:.1}%)", + passed, total, (passed as f64 / total as f64) * 100.0 ); println!(); } - + /// Get a summary string pub fn summary(&self) -> String { format!( @@ -153,34 +154,34 @@ impl AuditResult { #[cfg(test)] mod tests { use super::*; - + #[tokio::test] async fn test_result_pass() { let result = TestResult::new("test", "SPEC:1", "Must work") .run(|| async { Ok(()) }) .await; - + assert!(result.passed); assert!(result.error.is_none()); } - + #[tokio::test] async fn test_result_fail() { let result = TestResult::new("test", "SPEC:1", "Must work") .run(|| async { Err("Failed".to_string()) }) .await; - + assert!(!result.passed); assert_eq!(result.error, Some("Failed".to_string())); } - + #[test] fn test_audit_result() { let mut audit = AuditResult::new("Test Spec"); - + audit.add(TestResult::new("test1", "SPEC:1", "Req1").pass()); audit.add(TestResult::new("test2", "SPEC:2", "Req2").fail("Error")); - + assert_eq!(audit.total_count(), 2); assert_eq!(audit.passed_count(), 1); assert_eq!(audit.failed_count(), 1); diff --git a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs index 176b19a..c257155 100644 --- a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs +++ b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs @@ -88,8 +88,8 @@ //! - Forward reference tests verify out-of-order event acceptance //! - Transitive tests verify multi-hop acceptance chains +use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp}; -use crate::{AuditClient, AuditResult, TestResult, TestContext, FixtureKind}; use std::time::Duration; /// Test suite for GRASP-01 event acceptance policy @@ -99,42 +99,42 @@ impl EventAcceptancePolicyTests { /// Run all event acceptance policy tests pub async fn run_all(client: &AuditClient) -> AuditResult { let mut results = AuditResult::new("GRASP-01 Nostr Event Acceptance Policy Tests"); - + // Repository Announcement Acceptance Tests results.add(Self::test_accept_valid_repo_announcement(client).await); results.add(Self::test_reject_repo_announcement_missing_clone_tag(client).await); results.add(Self::test_reject_repo_announcement_missing_relays_tag(client).await); - + // Repository State Announcement Tests results.add(Self::test_accept_valid_repo_state_announcement(client).await); - + // Group 1: Accept Events Tagging Accepted Repositories results.add(Self::test_accept_issue_via_a_tag(client).await); - results.add(Self::test_accept_comment_via_A_tag(client).await); + results.add(Self::test_accept_comment_via_capital_a_tag(client).await); results.add(Self::test_accept_kind1_via_q_tag(client).await); - + // Group 2: Accept Events Tagging Accepted Events (Transitive) results.add(Self::test_accept_issue_quoting_issue_via_q(client).await); - results.add(Self::test_accept_comment_via_E_tag(client).await); + results.add(Self::test_accept_comment_via_capital_e_tag(client).await); results.add(Self::test_accept_kind1_via_e_tag(client).await); - + // Group 3: Accept Events Tagged BY Accepted Events (Forward Refs) results.add(Self::test_accept_kind1_referenced_in_issue(client).await); results.add(Self::test_accept_comment_referenced_in_comment(client).await); results.add(Self::test_accept_kind1_referenced_in_kind1(client).await); - + // Group 4: Reject Unrelated Events results.add(Self::test_reject_orphan_issue(client).await); results.add(Self::test_reject_orphan_kind1(client).await); results.add(Self::test_reject_comment_quoting_other_repo(client).await); - + results } - + // ============================================================ // Repository Announcement Acceptance Tests // ============================================================ - + /// Test: Accept valid repository announcements /// /// Spec: Lines 3-5 of ../grasp/01.md @@ -152,41 +152,52 @@ impl EventAcceptancePolicyTests { .run(|| async { // Create TestContext for mode-aware fixture management let ctx = TestContext::new(client); - + // Request repository fixture - behavior depends on mode - let event = ctx.get_fixture(FixtureKind::ValidRepo).await - .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; - + let event = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; + // Get relay URL for validation - let relay_url = client.client().relays().await + let relay_url = client + .client() + .relays() + .await .keys() .next() .ok_or("No relay connected")? .to_string(); - + // Convert WebSocket URL to HTTP URL for validation let http_url = relay_url .replace("ws://", "http://") .replace("wss://", "https://"); - + // Extract repo_id from the event's d tag - let repo_id = event.tags.iter() + let repo_id = event + .tags + .iter() .find(|t| t.kind() == TagKind::d()) .and_then(|t| t.content()) .ok_or("Missing d tag in announcement")? .to_string(); - + let event_id = event.id; - + // Query back to verify it was accepted and stored let filter = Filter::new() .kind(Kind::GitRepoAnnouncement) .author(client.public_key()) .identifier(&repo_id); - - let events = client.query(filter).await + + let events = client + .query(filter) + .await .map_err(|e| format!("Failed to query events from relay: {}", e))?; - + // Verify we got the event back if events.is_empty() { return Err(format!( @@ -194,41 +205,43 @@ impl EventAcceptancePolicyTests { event_id, repo_id )); } - + // Verify it's the same event - let stored_event = events.iter() - .find(|e| e.id == event_id) - .ok_or(format!( - "Stored event ID doesn't match sent event. Expected: {}, Got {} events", - event_id, events.len() - ))?; - + let stored_event = events.iter().find(|e| e.id == event_id).ok_or(format!( + "Stored event ID doesn't match sent event. Expected: {}, Got {} events", + event_id, + events.len() + ))?; + // Verify key tags are present - let has_clone_tag = stored_event.tags.iter() - .any(|t| { - t.kind() == TagKind::Custom("clone".into()) && - t.content().map(|c| c.contains(&http_url)).unwrap_or(false) - }); - - let has_relays_tag = stored_event.tags.iter() - .any(|t| { - t.kind() == TagKind::Custom("relays".into()) && - t.content() == Some(&relay_url) - }); - + let has_clone_tag = stored_event.tags.iter().any(|t| { + t.kind() == TagKind::Custom("clone".into()) + && t.content().map(|c| c.contains(&http_url)).unwrap_or(false) + }); + + let has_relays_tag = stored_event.tags.iter().any(|t| { + t.kind() == TagKind::Custom("relays".into()) && t.content() == Some(&relay_url) + }); + if !has_clone_tag { - return Err(format!("Stored event missing clone tag with service URL ({})", http_url)); + return Err(format!( + "Stored event missing clone tag with service URL ({})", + http_url + )); } - + if !has_relays_tag { - return Err(format!("Stored event missing relays tag with service URL ({})", relay_url)); + return Err(format!( + "Stored event missing relays tag with service URL ({})", + relay_url + )); } - + Ok(()) }) .await } - + /// Test: Reject repo announcements not listing service in clone tag /// /// Spec: Line 5 of ../grasp/01.md @@ -241,39 +254,54 @@ impl EventAcceptancePolicyTests { ) .run(|| async { // Get relay URL from client - let relay_url = client.client().relays().await + let relay_url = client + .client() + .relays() + .await .keys() .next() .ok_or("No relay connected - client has no active relay connections")? .to_string(); - + // Create unique repository identifier let timestamp = Timestamp::now().as_u64(); let repo_id = format!("test-repo-no-clone-{}", timestamp); - + // Create repo announcement WITHOUT service in clone tag - let event = client.event_builder(Kind::GitRepoAnnouncement, "") + let event = client + .event_builder(Kind::GitRepoAnnouncement, "") .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom(TagKind::Custom("name".into()), vec!["Test Repo No Clone"])) - .tag(Tag::custom(TagKind::Custom("clone".into()), vec!["https://github.com/user/repo.git"])) // NOT this service - .tag(Tag::custom(TagKind::Custom("relays".into()), vec![relay_url.clone()])) // Correct relay + .tag(Tag::custom( + TagKind::Custom("name".into()), + vec!["Test Repo No Clone"], + )) + .tag(Tag::custom( + TagKind::Custom("clone".into()), + vec!["https://github.com/user/repo.git"], + )) // NOT this service + .tag(Tag::custom( + TagKind::Custom("relays".into()), + vec![relay_url.clone()], + )) // Correct relay .build(client.keys()) .map_err(|e| format!("Failed to build event: {}", e))?; - + let event_id = event.id; - + // Send event - expect rejection - let send_result = client.send_event(event.clone()).await; - + let _send_result = client.send_event(event.clone()).await; + // Query to verify event is NOT stored let filter = Filter::new() .kind(Kind::GitRepoAnnouncement) .author(client.public_key()) .identifier(&repo_id); - - let events = client.query(filter).await + + let events = client + .query(filter) + .await .map_err(|e| format!("Failed to query events from relay: {}", e))?; - + // Verify event was rejected (not stored) if events.iter().any(|e| e.id == event_id) { return Err(format!( @@ -282,12 +310,12 @@ impl EventAcceptancePolicyTests { event_id, relay_url )); } - + Ok(()) }) .await } - + /// Test: Reject repo announcements not listing service in relays tag /// /// Spec: Line 5 of ../grasp/01.md @@ -300,44 +328,63 @@ impl EventAcceptancePolicyTests { ) .run(|| async { // Get relay URL from client - let relay_url = client.client().relays().await + let relay_url = client + .client() + .relays() + .await .keys() .next() .ok_or("No relay connected - client has no active relay connections")? .to_string(); - + // Convert WebSocket URL to HTTP URL for clone tag let http_url = relay_url .replace("ws://", "http://") .replace("wss://", "https://"); - + // Create unique repository identifier let timestamp = Timestamp::now().as_u64(); let repo_id = format!("test-repo-no-relays-{}", timestamp); - + // Create repo announcement WITHOUT service in relays tag - let event = client.event_builder(Kind::GitRepoAnnouncement, "") + let event = client + .event_builder(Kind::GitRepoAnnouncement, "") .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom(TagKind::custom("name"), vec!["Test Repo No Relays"])) - .tag(Tag::custom(TagKind::custom("clone"), vec![format!("{}/{}/test-repo.git", http_url, client.public_key())])) // Correct clone - .tag(Tag::custom(TagKind::custom("relays"), vec!["wss://relay.damus.io"])) // NOT this service + .tag(Tag::custom( + TagKind::custom("name"), + vec!["Test Repo No Relays"], + )) + .tag(Tag::custom( + TagKind::custom("clone"), + vec![format!( + "{}/{}/test-repo.git", + http_url, + client.public_key() + )], + )) // Correct clone + .tag(Tag::custom( + TagKind::custom("relays"), + vec!["wss://relay.damus.io"], + )) // NOT this service .build(client.keys()) .map_err(|e| format!("Failed to build event: {}", e))?; - + let event_id = event.id; - + // Send event - expect rejection let _send_result = client.send_event(event.clone()).await; - + // Query to verify event is NOT stored let filter = Filter::new() .kind(Kind::GitRepoAnnouncement) .author(client.public_key()) .identifier(&repo_id); - - let events = client.query(filter).await + + let events = client + .query(filter) + .await .map_err(|e| format!("Failed to query events from relay: {}", e))?; - + // Verify event was rejected (not stored) if events.iter().any(|e| e.id == event_id) { return Err(format!( @@ -346,16 +393,16 @@ impl EventAcceptancePolicyTests { event_id, relay_url )); } - + Ok(()) }) .await } - + // ============================================================ // Repository State Announcement Tests // ============================================================ - + /// Test: Accept valid repository state announcements /// /// Spec: Lines 6-7 of ../grasp/01.md @@ -374,31 +421,39 @@ impl EventAcceptancePolicyTests { .run(|| async { // NEW: Create TestContext for mode-aware fixture management let ctx = TestContext::new(client); - + // NEW: Request repository fixture - behavior depends on mode // CI mode: Creates fresh repo for this test // Production mode: Returns cached repo if available - let repo_event = ctx.get_fixture(FixtureKind::RepoState).await - .map_err(|e| format!("Test setup failed: could not get repository state fixture: {}", e))?; - + let repo_event = ctx.get_fixture(FixtureKind::RepoState).await.map_err(|e| { + format!( + "Test setup failed: could not get repository state fixture: {}", + e + ) + })?; + // Extract repo_id from the repository announcement - let repo_id = repo_event.tags.iter() + let repo_id = repo_event + .tags + .iter() .find(|t| t.kind() == TagKind::d()) .and_then(|t| t.content()) .ok_or("Missing d tag in repository announcement")? .to_string(); - + let event_id = repo_event.id; - + // Query back to verify it was accepted and stored let filter = Filter::new() .kind(Kind::Custom(30618)) .author(client.public_key()) .identifier(&repo_id); - - let events = client.query(filter).await + + let events = client + .query(filter) + .await .map_err(|e| format!("Failed to query events from relay: {}", e))?; - + // Verify we got the event back if events.is_empty() { return Err(format!( @@ -406,16 +461,16 @@ impl EventAcceptancePolicyTests { event_id, repo_id )); } - + Ok(()) }) .await } - + // ============================================================ // Helper Functions (6 total) // ============================================================ - + /// Extract the `d` tag value from an event fn extract_d_tag(event: &Event) -> Option { for tag in event.tags.iter() { @@ -426,15 +481,16 @@ impl EventAcceptancePolicyTests { } None } - + /// Create a basic repository announcement (kind 30617) /// Uses the client's create_repo_announcement helper which includes required clone and relays tags async fn create_test_repo(client: &AuditClient, repo_id: &str) -> Result { - client.create_repo_announcement(repo_id) + client + .create_repo_announcement(repo_id) .await .map_err(|e| format!("Test setup failed: could not create test repository: {}", e)) } - + /// Create an issue (kind 1621) that references a repository /// Uses AuditClient::create_issue helper method fn create_issue_for_repo( @@ -442,10 +498,11 @@ impl EventAcceptancePolicyTests { repo_event: &Event, issue_title: &str, ) -> Result { - client.create_issue(repo_event, issue_title, "issue content", vec![]) + client + .create_issue(repo_event, issue_title, "issue content", vec![]) .map_err(|e| format!("Test setup failed: could not create test issue: {}", e)) } - + /// Create a NIP-22 comment (kind 1111) for an event /// Uses AuditClient::create_comment helper method fn create_comment_for_event( @@ -453,10 +510,11 @@ impl EventAcceptancePolicyTests { event: &Event, content: &str, ) -> Result { - client.create_comment(event, content, vec![]) + client + .create_comment(event, content, vec![]) .map_err(|e| format!("Test setup failed: could not create test comment: {}", e)) } - + /// Send event and verify it was accepted (stored by relay) async fn send_and_verify_accepted( client: &AuditClient, @@ -464,23 +522,27 @@ impl EventAcceptancePolicyTests { description: &str, ) -> Result<(), String> { let event_id = event.id; - - client.send_event(event).await + + client + .send_event(event) + .await .map_err(|e| format!("Failed to send event to relay: {}", e))?; - + tokio::time::sleep(Duration::from_millis(100)).await; - + let filter = Filter::new().id(event_id); - let events = client.query(filter).await + let events = client + .query(filter) + .await .map_err(|e| format!("Failed to query relay for verification: {}", e))?; - + if events.is_empty() { return Err(format!("Event should be accepted: {}", description)); } - + Ok(()) } - + /// Send event and verify it was rejected (NOT stored by relay) async fn send_and_verify_rejected( client: &AuditClient, @@ -488,27 +550,31 @@ impl EventAcceptancePolicyTests { description: &str, ) -> Result<(), String> { let event_id = event.id; - - client.send_event(event).await + + client + .send_event(event) + .await .map_err(|e| format!("Failed to send event to relay: {}", e))?; - + tokio::time::sleep(Duration::from_millis(100)).await; - + let filter = Filter::new().id(event_id); - let events = client.query(filter).await + let events = client + .query(filter) + .await .map_err(|e| format!("Failed to query relay for verification: {}", e))?; - + if !events.is_empty() { return Err(format!("Event should be rejected: {}", description)); } - + Ok(()) } - + // ============================================================ // Group 1: Accept Events Tagging Accepted Repositories (3 tests) // ============================================================ - + /// Test 1.1: Issue referencing repo via `a` tag should be accepted /// /// **EXAMPLE: Using TestContext for prerequisite events** @@ -522,28 +588,33 @@ impl EventAcceptancePolicyTests { .run(|| async { // NEW: Create TestContext let ctx = TestContext::new(client); - + // NEW: Get repository fixture (mode-aware) - let repo = ctx.get_fixture(FixtureKind::ValidRepo).await - .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; - + let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; + // 2. Create issue that references the repo let issue = Self::create_issue_for_repo(client, &repo, "Test Issue 1")?; - + // 3. Send issue and verify it's accepted - Self::send_and_verify_accepted(client, issue, "issue referencing repo via 'a' tag").await?; - + Self::send_and_verify_accepted(client, issue, "issue referencing repo via 'a' tag") + .await?; + Ok(()) }) .await } - + /// Test 1.2: NIP-22 comment with root `A` tag referencing repo should be accepted /// /// **Using TestContext pattern:** /// - In CI mode: Creates fresh repo for full isolation /// - In Production mode: Reuses cached repo to minimize events - async fn test_accept_comment_via_A_tag(client: &AuditClient) -> TestResult { + async fn test_accept_comment_via_capital_a_tag(client: &AuditClient) -> TestResult { TestResult::new( "accept_comment_via_A_tag", "GRASP-01:event-acceptance:1.2", @@ -552,37 +623,44 @@ impl EventAcceptancePolicyTests { .run(|| async { // Create TestContext let ctx = TestContext::new(client); - + // Get repository fixture (mode-aware) - let repo = ctx.get_fixture(FixtureKind::ValidRepo).await - .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; - + let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; + // Extract repo_id and create `A` tag manually - let repo_id = Self::extract_d_tag(&repo) - .ok_or("Failed to extract repo_id from repo event")?; + let repo_id = + Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id from repo event")?; let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); - + // Create comment with `A` tag (root reference to repo) let tags = vec![ - Tag::custom(TagKind::custom("A"), vec![a_tag_value.clone(), "".to_string(), "root".to_string()]), + Tag::custom( + TagKind::custom("A"), + vec![a_tag_value.clone(), "".to_string(), "root".to_string()], + ), Tag::custom(TagKind::custom("K"), vec!["30617".to_string()]), Tag::public_key(repo.pubkey), ]; - + let comment = client .event_builder(Kind::Custom(1111), "Comment on repo") .tags(tags) .build(client.keys()) .map_err(|e| format!("Failed to build comment: {}", e))?; - + // Send comment and verify it's accepted Self::send_and_verify_accepted(client, comment, "comment with 'A' tag to repo").await?; - + Ok(()) }) .await } - + /// Test 1.3: Kind 1 text note quoting repo via `q` tag should be accepted /// /// **Using TestContext pattern:** @@ -597,39 +675,41 @@ impl EventAcceptancePolicyTests { .run(|| async { // Create TestContext let ctx = TestContext::new(client); - + // Get repository fixture (mode-aware) - let repo = ctx.get_fixture(FixtureKind::ValidRepo).await - .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; - + let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; + // Extract repo_id and create `q` tag - let repo_id = Self::extract_d_tag(&repo) - .ok_or("Failed to extract repo_id from repo event")?; + let repo_id = + Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id from repo event")?; let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); - + // Create kind 1 note with `q` tag (quote reference to repo) - let tags = vec![ - Tag::custom(TagKind::custom("q"), vec![a_tag_value]), - ]; - + let tags = vec![Tag::custom(TagKind::custom("q"), vec![a_tag_value])]; + let note = client .event_builder(Kind::TextNote, "Mentioning this repo") .tags(tags) .build(client.keys()) .map_err(|e| format!("Failed to build note: {}", e))?; - + // Send note and verify it's accepted Self::send_and_verify_accepted(client, note, "kind 1 with 'q' tag to repo").await?; - + Ok(()) }) .await } - + // ============================================================ // Group 2: Accept Events Tagging Accepted Events (3 tests) // ============================================================ - + /// Test 2.1: Issue quoting another accepted issue should be accepted (transitive) /// /// **Using TestContext pattern:** @@ -644,37 +724,44 @@ impl EventAcceptancePolicyTests { .run(|| async { // Create TestContext let ctx = TestContext::new(client); - + // Get repo with issue fixture (mode-aware) - returns the issue event - let issue_a = ctx.get_fixture(FixtureKind::RepoWithIssue).await - .map_err(|e| format!("Test setup failed: could not get repo with issue fixture: {}", e))?; - + let issue_a = ctx + .get_fixture(FixtureKind::RepoWithIssue) + .await + .map_err(|e| { + format!( + "Test setup failed: could not get repo with issue fixture: {}", + e + ) + })?; + // Create Repo B but DON'T send it (unaccepted) - just for creating Issue B let repo_b = Self::create_test_repo(client, "repo-b").await?; - + // Create Issue B that quotes accepted Issue A via 'q' tag (should make it accepted) - let additional_tags = vec![ - Tag::custom(TagKind::custom("q"), vec![issue_a.id.to_hex()]), - ]; - + let additional_tags = + vec![Tag::custom(TagKind::custom("q"), vec![issue_a.id.to_hex()])]; + let issue_b = client .create_issue(&repo_b, "Issue B", "issue content", additional_tags) .map_err(|e| format!("Failed to build issue B: {}", e))?; - + // Send Issue B and verify it's ACCEPTED (via transitive quote to Issue A) - Self::send_and_verify_accepted(client, issue_b, "issue B quoting accepted issue A").await?; - + Self::send_and_verify_accepted(client, issue_b, "issue B quoting accepted issue A") + .await?; + Ok(()) }) .await } - + /// Test 2.2: NIP-22 comment with root 'E' tag to accepted issue should be accepted /// /// **Using TestContext pattern:** /// - In CI mode: Creates fresh repo+issue for full isolation /// - In Production mode: Reuses cached repo+issue to minimize events - async fn test_accept_comment_via_E_tag(client: &AuditClient) -> TestResult { + async fn test_accept_comment_via_capital_e_tag(client: &AuditClient) -> TestResult { TestResult::new( "accept_comment_via_E_tag", "GRASP-01:event-acceptance:2.2", @@ -683,22 +770,30 @@ impl EventAcceptancePolicyTests { .run(|| async { // Create TestContext let ctx = TestContext::new(client); - + // Get repo with issue fixture (mode-aware) - returns the issue event - let issue = ctx.get_fixture(FixtureKind::RepoWithIssue).await - .map_err(|e| format!("Test setup failed: could not get repo with issue fixture: {}", e))?; - + let issue = ctx + .get_fixture(FixtureKind::RepoWithIssue) + .await + .map_err(|e| { + format!( + "Test setup failed: could not get repo with issue fixture: {}", + e + ) + })?; + // Create comment using the helper (which adds NIP-22 tags including 'E') let comment = Self::create_comment_for_event(client, &issue, "Comment content")?; - + // Send comment and verify it's accepted (via E tag to accepted issue) - Self::send_and_verify_accepted(client, comment, "comment with E tag to accepted issue").await?; - + Self::send_and_verify_accepted(client, comment, "comment with E tag to accepted issue") + .await?; + Ok(()) }) .await } - + /// Test 2.3: Kind 1 note with 'e' tag reply to accepted kind 1 should be accepted /// /// **Using TestContext pattern:** @@ -713,43 +808,52 @@ impl EventAcceptancePolicyTests { .run(|| async { // Create TestContext let ctx = TestContext::new(client); - + // Get repository fixture (mode-aware) - let repo = ctx.get_fixture(FixtureKind::ValidRepo).await - .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; - + let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; + // Create Kind 1 A that quotes the repo (makes it accepted) - let repo_id = Self::extract_d_tag(&repo) - .ok_or("Failed to extract repo_id")?; + let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?; let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); - + let kind1_a = client .event_builder(Kind::TextNote, "Note A about repo") .tags(vec![Tag::custom(TagKind::custom("q"), vec![a_tag_value])]) .build(client.keys()) .map_err(|e| format!("Failed to build kind1 A: {}", e))?; - - Self::send_and_verify_accepted(client, kind1_a.clone(), "kind 1 A quoting repo").await?; - + + Self::send_and_verify_accepted(client, kind1_a.clone(), "kind 1 A quoting repo") + .await?; + // Create Kind 1 B that replies to Kind 1 A via 'e' tag let kind1_b = client .event_builder(Kind::TextNote, "Reply to Note A") .tags(vec![Tag::event(kind1_a.id)]) .build(client.keys()) .map_err(|e| format!("Failed to build kind1 B: {}", e))?; - + // Send Kind 1 B and verify it's accepted (via 'e' tag to accepted kind 1 A) - Self::send_and_verify_accepted(client, kind1_b, "kind 1 B replying to accepted kind 1 A").await?; - + Self::send_and_verify_accepted( + client, + kind1_b, + "kind 1 B replying to accepted kind 1 A", + ) + .await?; + Ok(()) }) .await } - + // ============================================================ // Group 3: Accept Events Tagged BY Accepted Events (3 tests) // ============================================================ - + /// Test 3.1: Kind 1 note should be accepted when referenced by an accepted issue (forward ref) /// /// **Using TestContext pattern:** @@ -764,44 +868,61 @@ impl EventAcceptancePolicyTests { .run(|| async { // Create TestContext let ctx = TestContext::new(client); - + // Get repository fixture (mode-aware) - let repo = ctx.get_fixture(FixtureKind::ValidRepo).await - .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; - + let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; + // Create Kind 1 note locally but DON'T send it yet let kind1_note = client .event_builder(Kind::TextNote, "Note to be referenced") .build(client.keys()) .map_err(|e| format!("Failed to build kind1: {}", e))?; - + // Create and send issue that QUOTES the unsent Kind 1 note let issue_tags = vec![ // Reference to accepted repo - Tag::custom(TagKind::custom("a"), vec![ - format!("30617:{}:{}", repo.pubkey, Self::extract_d_tag(&repo).unwrap()) - ]), - Tag::custom(TagKind::custom("subject"), vec!["Issue referencing kind1".to_string()]), + Tag::custom( + TagKind::custom("a"), + vec![format!( + "30617:{}:{}", + repo.pubkey, + Self::extract_d_tag(&repo).unwrap() + )], + ), + Tag::custom( + TagKind::custom("subject"), + vec!["Issue referencing kind1".to_string()], + ), // Quote the Kind 1 that hasn't been sent yet Tag::custom(TagKind::custom("q"), vec![kind1_note.id.to_hex()]), ]; - + let issue = client .event_builder(Kind::Custom(1621), "issue content") .tags(issue_tags) .build(client.keys()) .map_err(|e| format!("Failed to build issue: {}", e))?; - + Self::send_and_verify_accepted(client, issue, "issue quoting unsent kind1").await?; - + // NOW send the Kind 1 note - should be accepted because accepted issue quotes it - Self::send_and_verify_accepted(client, kind1_note, "kind1 note referenced by accepted issue").await?; - + Self::send_and_verify_accepted( + client, + kind1_note, + "kind1 note referenced by accepted issue", + ) + .await?; + Ok(()) }) .await } - + /// Test 3.2: Comment should be accepted when referenced by another accepted comment (forward ref) /// /// **Using TestContext pattern:** @@ -816,58 +937,74 @@ impl EventAcceptancePolicyTests { .run(|| async { // Create TestContext let ctx = TestContext::new(client); - + // Get repo with issue fixture (mode-aware) - let repo = ctx.get_fixture(FixtureKind::RepoWithIssue).await - .map_err(|e| format!("Test setup failed: could not get repo with issue fixture: {}", e))?; - + let repo = ctx + .get_fixture(FixtureKind::RepoWithIssue) + .await + .map_err(|e| { + format!( + "Test setup failed: could not get repo with issue fixture: {}", + e + ) + })?; + // Extract the issue from the repo event (it's stored as the first 'e' tag) - let issue_id = repo.tags.iter() + let issue_id = repo + .tags + .iter() .find(|t| t.kind() == TagKind::e()) .and_then(|t| t.content()) .ok_or("Missing issue reference in RepoWithIssue fixture")?; - + // Query to get the actual issue event - let filter = Filter::new().id( - nostr_sdk::EventId::from_hex(issue_id) - .map_err(|e| format!("Invalid issue ID: {}", e))? - ); - let issues = client.query(filter).await + let filter = Filter::new().id(nostr_sdk::EventId::from_hex(issue_id) + .map_err(|e| format!("Invalid issue ID: {}", e))?); + let issues = client + .query(filter) + .await .map_err(|e| format!("Failed to query issue: {}", e))?; - let issue = issues.first() - .ok_or("Issue not found")? - .clone(); - + let issue = issues.first().ok_or("Issue not found")?.clone(); + // Create Comment A locally but DON'T send it yet let comment_a = Self::create_comment_for_event(client, &issue, "Comment A")?; - + // Create and send Comment B that quotes Comment A (which hasn't been sent) let comment_b_tags = vec![ // NIP-22 tags for the original issue - Tag::custom(TagKind::custom("E"), vec![issue.id.to_hex(), "".to_string(), "root".to_string()]), + Tag::custom( + TagKind::custom("E"), + vec![issue.id.to_hex(), "".to_string(), "root".to_string()], + ), Tag::event(issue.id), Tag::custom(TagKind::custom("K"), vec![issue.kind.as_u16().to_string()]), Tag::public_key(issue.pubkey), // Quote Comment A which hasn't been sent yet Tag::custom(TagKind::custom("q"), vec![comment_a.id.to_hex()]), ]; - + let comment_b = client .event_builder(Kind::Custom(1111), "Comment B quoting Comment A") .tags(comment_b_tags) .build(client.keys()) .map_err(|e| format!("Failed to build comment B: {}", e))?; - - Self::send_and_verify_accepted(client, comment_b, "comment B quoting unsent comment A").await?; - + + Self::send_and_verify_accepted(client, comment_b, "comment B quoting unsent comment A") + .await?; + // NOW send Comment A - should be accepted because accepted Comment B quotes it - Self::send_and_verify_accepted(client, comment_a, "comment A referenced by accepted comment B").await?; - + Self::send_and_verify_accepted( + client, + comment_a, + "comment A referenced by accepted comment B", + ) + .await?; + Ok(()) }) .await } - + /// Test 3.3: Kind 1 note should be accepted when referenced by another accepted kind 1 (forward ref) /// /// **Using TestContext pattern:** @@ -882,47 +1019,56 @@ impl EventAcceptancePolicyTests { .run(|| async { // Create TestContext let ctx = TestContext::new(client); - + // Get repository fixture (mode-aware) - let repo = ctx.get_fixture(FixtureKind::ValidRepo).await - .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; - + let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; + // Create Kind 1 A locally but DON'T send it yet let kind1_a = client .event_builder(Kind::TextNote, "Note A to be referenced") .build(client.keys()) .map_err(|e| format!("Failed to build kind1 A: {}", e))?; - + // Create and send Kind 1 B that: // - Quotes the repo (makes it accepted) // - Mentions Kind 1 A via 'e' tag (which hasn't been sent yet) - let repo_id = Self::extract_d_tag(&repo) - .ok_or("Failed to extract repo_id")?; + let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?; let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); - + let kind1_b = client .event_builder(Kind::TextNote, "Note B mentioning Note A") .tags(vec![ - Tag::custom(TagKind::custom("q"), vec![a_tag_value]), // Quote repo (accepted) - Tag::event(kind1_a.id), // Mention unsent Kind 1 A + Tag::custom(TagKind::custom("q"), vec![a_tag_value]), // Quote repo (accepted) + Tag::event(kind1_a.id), // Mention unsent Kind 1 A ]) .build(client.keys()) .map_err(|e| format!("Failed to build kind1 B: {}", e))?; - - Self::send_and_verify_accepted(client, kind1_b, "kind1 B mentioning unsent kind1 A").await?; - + + Self::send_and_verify_accepted(client, kind1_b, "kind1 B mentioning unsent kind1 A") + .await?; + // NOW send Kind 1 A - should be accepted because accepted Kind 1 B mentions it - Self::send_and_verify_accepted(client, kind1_a, "kind1 A referenced by accepted kind1 B").await?; - + Self::send_and_verify_accepted( + client, + kind1_a, + "kind1 A referenced by accepted kind1 B", + ) + .await?; + Ok(()) }) .await } - + // ============================================================ // Group 4: Reject Unrelated Events (3 tests) // ============================================================ - + /// Test 4.1: Issue referencing unaccepted repo should be rejected async fn test_reject_orphan_issue(client: &AuditClient) -> TestResult { TestResult::new( @@ -933,18 +1079,24 @@ impl EventAcceptancePolicyTests { .run(|| async { // 1. Create a repo but DON'T send it (so it's unaccepted) let unaccepted_repo = Self::create_test_repo(client, "unaccepted-repo-1").await?; - + // 2. Create issue that references the unaccepted repo - let orphan_issue = Self::create_issue_for_repo(client, &unaccepted_repo, "Orphan Issue")?; - + let orphan_issue = + Self::create_issue_for_repo(client, &unaccepted_repo, "Orphan Issue")?; + // 3. Send issue and verify it's REJECTED - Self::send_and_verify_rejected(client, orphan_issue, "issue referencing unaccepted repo").await?; - + Self::send_and_verify_rejected( + client, + orphan_issue, + "issue referencing unaccepted repo", + ) + .await?; + Ok(()) }) .await } - + /// Test 4.2: Generic kind 1 note with no repo references should be rejected async fn test_reject_orphan_kind1(client: &AuditClient) -> TestResult { TestResult::new( @@ -958,15 +1110,16 @@ impl EventAcceptancePolicyTests { .event_builder(Kind::TextNote, "Just a random note") .build(client.keys()) .map_err(|e| format!("Failed to build note: {}", e))?; - + // 2. Send note and verify it's REJECTED - Self::send_and_verify_rejected(client, orphan_note, "kind 1 with no repo references").await?; - + Self::send_and_verify_rejected(client, orphan_note, "kind 1 with no repo references") + .await?; + Ok(()) }) .await } - + /// Test 4.3: Comment quoting unaccepted repo should be rejected /// /// **Using TestContext pattern:** @@ -982,35 +1135,42 @@ impl EventAcceptancePolicyTests { .run(|| async { // Create TestContext let ctx = TestContext::new(client); - + // Get accepted repo A fixture (mode-aware) - let _repo_a = ctx.get_fixture(FixtureKind::ValidRepo).await - .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; - + let _repo_a = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; + // Create Repo B but DON'T send it (unaccepted) let repo_b = Self::create_test_repo(client, "unaccepted-repo-b").await?; - + // Extract repo_b info and create comment that quotes repo B (not repo A) - let repo_b_id = Self::extract_d_tag(&repo_b) - .ok_or("Failed to extract repo_b id")?; + let repo_b_id = Self::extract_d_tag(&repo_b).ok_or("Failed to extract repo_b id")?; let repo_b_a_tag = format!("30617:{}:{}", repo_b.pubkey, repo_b_id); - + // Create comment that references ONLY repo B (unaccepted) let tags = vec![ - Tag::custom(TagKind::custom("A"), vec![repo_b_a_tag, "".to_string(), "root".to_string()]), + Tag::custom( + TagKind::custom("A"), + vec![repo_b_a_tag, "".to_string(), "root".to_string()], + ), Tag::custom(TagKind::custom("K"), vec!["30617".to_string()]), Tag::public_key(repo_b.pubkey), ]; - + let comment = client .event_builder(Kind::Custom(1111), "Comment on unaccepted repo") .tags(tags) .build(client.keys()) .map_err(|e| format!("Failed to build comment: {}", e))?; - + // Send comment and verify it's REJECTED (only references unaccepted repo B) - Self::send_and_verify_rejected(client, comment, "comment quoting only unaccepted repo").await?; - + Self::send_and_verify_rejected(client, comment, "comment quoting only unaccepted repo") + .await?; + Ok(()) }) .await @@ -1021,27 +1181,30 @@ impl EventAcceptancePolicyTests { mod tests { use super::*; use crate::AuditConfig; - + #[tokio::test] #[ignore] // Requires running relay async fn test_grasp01_event_acceptance_policy_against_relay() { // Read relay URL from environment variable - must be supplied - let relay_url = std::env::var("RELAY_URL") - .expect("RELAY_URL environment variable must be set. Example: RELAY_URL=ws://localhost:18081"); - + let relay_url = std::env::var("RELAY_URL").expect( + "RELAY_URL environment variable must be set. Example: RELAY_URL=ws://localhost:18081", + ); + let config = AuditConfig::ci(); let client = AuditClient::new(&relay_url, config) .await - .expect(&format!( - "Failed to connect to relay at {}. Ensure relay is running and accessible. \ + .unwrap_or_else(|_| { + panic!( + "Failed to connect to relay at {}. Ensure relay is running and accessible. \ Try: docker run --rm -p 18081:8081 ghcr.io/danconwaydev/ngit-relay:latest", - relay_url - )); - + relay_url + ) + }); + let results = EventAcceptancePolicyTests::run_all(&client).await; results.print_report(); - + // Don't assert all passed yet - some tests may be failing // Future: assert!(results.all_passed(), "Some GRASP-01 event acceptance tests failed"); } -} \ No newline at end of file +} diff --git a/grasp-audit/src/specs/grasp01/mod.rs b/grasp-audit/src/specs/grasp01/mod.rs index 4f4583e..6fd6960 100644 --- a/grasp-audit/src/specs/grasp01/mod.rs +++ b/grasp-audit/src/specs/grasp01/mod.rs @@ -6,4 +6,4 @@ pub mod nip11_document; pub use event_acceptance_policy::EventAcceptancePolicyTests; pub use nip01_smoke::Nip01SmokeTests; -pub use nip11_document::Nip11DocumentTests; \ No newline at end of file +pub use nip11_document::Nip11DocumentTests; diff --git a/grasp-audit/src/specs/grasp01/nip01_smoke.rs b/grasp-audit/src/specs/grasp01/nip01_smoke.rs index 9ed0f56..204ee60 100644 --- a/grasp-audit/src/specs/grasp01/nip01_smoke.rs +++ b/grasp-audit/src/specs/grasp01/nip01_smoke.rs @@ -13,7 +13,7 @@ impl Nip01SmokeTests { /// Run all NIP-01 smoke tests pub async fn run_all(client: &AuditClient) -> AuditResult { let mut results = AuditResult::new("NIP-01 Smoke Tests"); - + // Run tests sequentially to avoid future type issues results.add(Self::test_websocket_connection(client).await); results.add(Self::test_send_receive_event(client).await); @@ -21,10 +21,10 @@ impl Nip01SmokeTests { results.add(Self::test_close_subscription(client).await); results.add(Self::test_reject_invalid_signature(client).await); results.add(Self::test_reject_invalid_event_id(client).await); - + results } - + /// Test 1: Can establish WebSocket connection /// /// Spec: NIP-01 basic requirement @@ -39,17 +39,17 @@ impl Nip01SmokeTests { if !client.is_connected().await { return Err("Failed to connect to relay".to_string()); } - + Ok(()) }) .await } - + /// Test 2: Can send EVENT and receive OK response /// /// Spec: NIP-01 EVENT message /// Requirement: Relay MUST accept valid EVENT messages - /// + /// /// For GRASP servers, we send a NIP-34 repository announcement that lists /// the GRASP server in clone and relays tags (required for acceptance). async fn test_send_receive_event(client: &AuditClient) -> TestResult { @@ -60,15 +60,17 @@ impl Nip01SmokeTests { ) .run(|| async { // Create a NIP-34 announcement event - let event = client.create_repo_announcement("send_receive_event").await + let event = client + .create_repo_announcement("send_receive_event") + .await .map_err(|e| format!("Failed to create announcement: {}", e))?; - + // Send event let event_id = client .send_event(event.clone()) .await .map_err(|e| format!("Failed to send event: {}", e))?; - + // Verify we got an event ID back if event_id != event.id { return Err(format!( @@ -76,43 +78,47 @@ impl Nip01SmokeTests { event.id, event_id )); } - + // Wait a bit for event to be indexed tokio::time::sleep(std::time::Duration::from_millis(100)).await; - + // Try to query it back - let filter = Filter::new() - .kind(Kind::Custom(30617)) - .id(event_id); - + let filter = Filter::new().kind(Kind::Custom(30617)).id(event_id); + let events = client .query(filter) .await .map_err(|e| format!("Failed to query event: {}", e))?; - + if events.is_empty() { // Debug: try querying without audit client filtering eprintln!("Event not found with audit client query, trying direct client query..."); let direct_filter = Filter::new().kind(Kind::Custom(30617)).id(event_id); - let direct_events = client.client().fetch_events(direct_filter, std::time::Duration::from_secs(5)).await + let direct_events = client + .client() + .fetch_events(direct_filter, std::time::Duration::from_secs(5)) + .await .map_err(|e| format!("Direct query failed: {}", e))?; let direct_vec: Vec = direct_events.into_iter().collect(); eprintln!("Direct query found {} events", direct_vec.len()); if !direct_vec.is_empty() { eprintln!("Event tags: {:?}", direct_vec[0].tags); } - return Err(format!("Event not found after sending (direct query found {})", direct_vec.len())); + return Err(format!( + "Event not found after sending (direct query found {})", + direct_vec.len() + )); } - + if events[0].id != event_id { return Err("Retrieved event has different ID".to_string()); } - + Ok(()) }) .await } - + /// Test 3: Can create subscription with REQ /// /// Spec: NIP-01 REQ message @@ -125,34 +131,36 @@ impl Nip01SmokeTests { ) .run(|| async { // Create a NIP-34 announcement event (accepted by GRASP relays) - let event = client.create_repo_announcement("create_subscription").await + let event = client + .create_repo_announcement("create_subscription") + .await .map_err(|e| format!("Failed to create announcement: {}", e))?; - - let event_id = client + + let _event_id = client .send_event(event.clone()) .await .map_err(|e| format!("Failed to send event: {}", e))?; - + // Subscribe to NIP-34 announcements from this author let filter = Filter::new() .kind(Kind::Custom(30617)) .author(client.public_key()); - + let events = client .subscribe(vec![filter], Some(std::time::Duration::from_secs(5))) .await .map_err(|e| format!("Failed to subscribe: {}", e))?; - + // Should have at least our event if events.is_empty() { return Err("No events received from subscription".to_string()); } - + Ok(()) }) .await } - + /// Test 4: Can close subscription with CLOSE /// /// Spec: NIP-01 CLOSE message @@ -167,22 +175,20 @@ impl Nip01SmokeTests { // For now, we just verify we can query events // Full subscription management with CLOSE would require // lower-level WebSocket access - - let filter = Filter::new() - .kind(Kind::TextNote) - .limit(1); - + + let filter = Filter::new().kind(Kind::TextNote).limit(1); + let _events = client .subscribe(vec![filter], Some(std::time::Duration::from_secs(2))) .await .map_err(|e| format!("Failed to subscribe: {}", e))?; - + // If we got here, subscription worked Ok(()) }) .await } - + /// Test 5: Rejects events with invalid signatures /// /// Spec: NIP-01 event validation @@ -199,7 +205,7 @@ impl Nip01SmokeTests { .event_builder(Kind::TextNote, "Invalid signature test") .build(client.keys()) .map_err(|e| format!("Failed to build event: {}", e))?; - + // Corrupt the signature by creating a new event with wrong sig // We'll use a different key to sign, creating an invalid signature let wrong_keys = Keys::generate(); @@ -207,7 +213,7 @@ impl Nip01SmokeTests { .tags(event.tags.clone()) .sign_with_keys(&wrong_keys) .map_err(|e| format!("Failed to build wrong event: {}", e))?; - + // Create event JSON with mismatched pubkey and signature // This should be rejected by the relay let invalid_event_json = serde_json::json!({ @@ -219,24 +225,24 @@ impl Nip01SmokeTests { "content": event.content, "sig": wrong_event.sig.to_string(), // Wrong signature! }); - + // Parse it back to an Event let invalid_event: Event = serde_json::from_value(invalid_event_json) .map_err(|e| format!("Failed to create invalid event: {}", e))?; - + // Try to send the invalid event let result = client.send_event(invalid_event).await; - + // We expect this to fail if result.is_ok() { return Err("Relay accepted event with invalid signature".to_string()); } - + Ok(()) }) .await } - + /// Test 6: Rejects events with invalid event IDs /// /// Spec: NIP-01 event ID validation @@ -253,7 +259,7 @@ impl Nip01SmokeTests { .event_builder(Kind::TextNote, "Invalid ID test") .build(client.keys()) .map_err(|e| format!("Failed to build event: {}", e))?; - + // Create event JSON with corrupted ID let invalid_event_json = serde_json::json!({ "id": EventId::all_zeros().to_hex(), // Wrong ID! @@ -264,19 +270,19 @@ impl Nip01SmokeTests { "content": event.content, "sig": event.sig.to_string(), }); - + // Parse it back to an Event let invalid_event: Event = serde_json::from_value(invalid_event_json) .map_err(|e| format!("Failed to create invalid event: {}", e))?; - + // Try to send the invalid event let result = client.send_event(invalid_event).await; - + // We expect this to fail if result.is_ok() { return Err("Relay accepted event with invalid ID".to_string()); } - + Ok(()) }) .await @@ -287,25 +293,25 @@ impl Nip01SmokeTests { mod tests { use super::*; use crate::AuditConfig; - + // Note: These tests require a running relay // They are integration tests, not unit tests - + #[tokio::test] #[ignore] // Ignore by default since it needs a running relay async fn test_smoke_tests_against_relay() { // RELAY_URL env var must be set - no default fallback let relay_url = std::env::var("RELAY_URL") .expect("RELAY_URL environment variable must be set for integration tests"); - + let config = AuditConfig::ci(); let client = AuditClient::new(&relay_url, config) .await .expect("Failed to connect to relay"); - + let results = Nip01SmokeTests::run_all(&client).await; results.print_report(); - + assert!(results.all_passed(), "Some smoke tests failed"); } } diff --git a/grasp-audit/src/specs/grasp01/nip11_document.rs b/grasp-audit/src/specs/grasp01/nip11_document.rs index 3f9c04a..be04777 100644 --- a/grasp-audit/src/specs/grasp01/nip11_document.rs +++ b/grasp-audit/src/specs/grasp01/nip11_document.rs @@ -9,7 +9,6 @@ //! - Handles curation field correctly (present if curated, absent otherwise) use crate::{AuditClient, AuditResult, TestResult}; -use nostr_sdk::prelude::*; pub struct Nip11DocumentTests; @@ -17,25 +16,25 @@ impl Nip11DocumentTests { /// Run all NIP-11 document tests pub async fn run_all(client: &AuditClient) -> AuditResult { let mut results = AuditResult::new("GRASP-01 NIP-11 Document Tests"); - + // NIP-11 relay information tests results.add(Self::test_nip11_document_exists(client).await); results.add(Self::test_nip11_supported_grasps_field(client).await); results.add(Self::test_nip11_repo_acceptance_criteria_field(client).await); results.add(Self::test_nip11_curation_field(client).await); - + results } - + // ========================================================================= // NIP-11 Relay Information Tests // ========================================================================= - + /// Test: Serve NIP-11 document /// /// Spec: Line 11 of ../grasp/01.md /// Requirement: MUST serve NIP-11 document - async fn test_nip11_document_exists(client: &AuditClient) -> TestResult { + async fn test_nip11_document_exists(_client: &AuditClient) -> TestResult { TestResult::new( "nip11_document_exists", "GRASP-01:nostr-relay:11", @@ -52,17 +51,17 @@ impl Nip11DocumentTests { // 4. Verify response is valid JSON // 5. Parse as NIP-11 document // 6. Verify has required fields (name, description, etc.) - + Err("Not implemented yet".to_string()) }) .await } - + /// Test: NIP-11 includes supported_grasps field /// /// Spec: Line 12 of ../grasp/01.md /// Requirement: MUST list supported GRASPs as string array - async fn test_nip11_supported_grasps_field(client: &AuditClient) -> TestResult { + async fn test_nip11_supported_grasps_field(_client: &AuditClient) -> TestResult { TestResult::new( "nip11_supported_grasps_field", "GRASP-01:nostr-relay:12", @@ -76,17 +75,17 @@ impl Nip11DocumentTests { // 4. Verify array includes "GRASP-01" // 5. Verify format: each entry matches pattern "GRASP-\d{2}" // 6. Document other GRASPs found (for info) - + Err("Not implemented yet".to_string()) }) .await } - + /// Test: NIP-11 includes repo_acceptance_criteria field /// /// Spec: Line 13 of ../grasp/01.md /// Requirement: MUST list repository acceptance criteria - async fn test_nip11_repo_acceptance_criteria_field(client: &AuditClient) -> TestResult { + async fn test_nip11_repo_acceptance_criteria_field(_client: &AuditClient) -> TestResult { TestResult::new( "nip11_repo_acceptance_criteria_field", "GRASP-01:nostr-relay:13", @@ -101,17 +100,17 @@ impl Nip11DocumentTests { // 5. Document the criteria (for info) // Examples: "Must list this relay in clone and relays tags" // "Pre-payment required via Lightning invoice" - + Err("Not implemented yet".to_string()) }) .await } - + /// Test: NIP-11 curation field handling /// /// Spec: Line 14 of ../grasp/01.md /// Requirement: MUST include curation if curated, omit otherwise - async fn test_nip11_curation_field(client: &AuditClient) -> TestResult { + async fn test_nip11_curation_field(_client: &AuditClient) -> TestResult { TestResult::new( "nip11_curation_field", "GRASP-01:nostr-relay:14", @@ -127,39 +126,41 @@ impl Nip11DocumentTests { // 4. If absent: // - Document that no curation beyond SPAM prevention // 5. Both cases are valid per spec - + Err("Not implemented yet".to_string()) }) .await } - } #[cfg(test)] mod tests { use super::*; use crate::AuditConfig; - -#[tokio::test] -#[ignore] // Requires running relay -async fn test_grasp01_nip11_document_against_relay() { - // Read relay URL from environment variable - must be supplied - let relay_url = std::env::var("RELAY_URL") - .expect("RELAY_URL environment variable must be set. Example: RELAY_URL=ws://localhost:18081"); - - let config = AuditConfig::ci(); - let client = AuditClient::new(&relay_url, config) - .await - .expect(&format!( - "Failed to connect to relay at {}. Ensure relay is running and accessible. \ + + #[tokio::test] + #[ignore] // Requires running relay + async fn test_grasp01_nip11_document_against_relay() { + // Read relay URL from environment variable - must be supplied + let relay_url = std::env::var("RELAY_URL").expect( + "RELAY_URL environment variable must be set. Example: RELAY_URL=ws://localhost:18081", + ); + + let config = AuditConfig::ci(); + let client = AuditClient::new(&relay_url, config) + .await + .unwrap_or_else(|_| { + panic!( + "Failed to connect to relay at {}. Ensure relay is running and accessible. \ Try: docker run --rm -p 18081:8081 ghcr.io/danconwaydev/ngit-relay:latest", - relay_url - )); - + relay_url + ) + }); + let results = Nip11DocumentTests::run_all(&client).await; results.print_report(); - + // Don't assert all passed yet - tests not implemented // assert!(results.all_passed(), "Some GRASP-01 NIP-11 document tests failed"); } -} \ No newline at end of file +} diff --git a/grasp-audit/src/specs/mod.rs b/grasp-audit/src/specs/mod.rs index c1c277c..a502866 100644 --- a/grasp-audit/src/specs/mod.rs +++ b/grasp-audit/src/specs/mod.rs @@ -3,8 +3,4 @@ pub mod grasp01; // Re-export all test structs from grasp01 module -pub use grasp01::{ - EventAcceptancePolicyTests, - Nip01SmokeTests, - Nip11DocumentTests, -}; +pub use grasp01::{EventAcceptancePolicyTests, Nip01SmokeTests, Nip11DocumentTests}; -- cgit v1.2.3