upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/grasp-audit/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-19 17:01:36 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-19 17:01:36 +0000
commitbf7f4d5381203d5c27b2811d62c5b1781533aa2b (patch)
tree26903bbf535d83abd7242370d8b6932eb80e3389 /grasp-audit/src
parentfa065ad128882755f2a988d6203b59a2ab5e38ff (diff)
fix some clippy fmt warnings
Diffstat (limited to 'grasp-audit/src')
-rw-r--r--grasp-audit/src/audit.rs84
-rw-r--r--grasp-audit/src/bin/grasp-audit.rs36
-rw-r--r--grasp-audit/src/client.rs174
-rw-r--r--grasp-audit/src/fixtures.rs162
-rw-r--r--grasp-audit/src/isolation.rs12
-rw-r--r--grasp-audit/src/lib.rs4
-rw-r--r--grasp-audit/src/result.rs57
-rw-r--r--grasp-audit/src/specs/grasp01/event_acceptance_policy.rs729
-rw-r--r--grasp-audit/src/specs/grasp01/mod.rs2
-rw-r--r--grasp-audit/src/specs/grasp01/nip01_smoke.rs112
-rw-r--r--grasp-audit/src/specs/grasp01/nip11_document.rs71
-rw-r--r--grasp-audit/src/specs/mod.rs6
12 files changed, 844 insertions, 605 deletions
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::*;
7pub struct AuditConfig { 7pub struct AuditConfig {
8 /// Unique ID for this audit run 8 /// Unique ID for this audit run
9 pub run_id: String, 9 pub run_id: String,
10 10
11 /// Mode: CI (isolated) or Production (live) 11 /// Mode: CI (isolated) or Production (live)
12 pub mode: AuditMode, 12 pub mode: AuditMode,
13 13
14 /// Cleanup timestamp (events can be cleaned after this) 14 /// Cleanup timestamp (events can be cleaned after this)
15 pub cleanup_after: Timestamp, 15 pub cleanup_after: Timestamp,
16 16
17 /// Whether to actually create events or just query 17 /// Whether to actually create events or just query
18 pub read_only: bool, 18 pub read_only: bool,
19} 19}
@@ -23,7 +23,7 @@ pub struct AuditConfig {
23pub enum AuditMode { 23pub enum AuditMode {
24 /// Isolated CI/CD tests - only see own events 24 /// Isolated CI/CD tests - only see own events
25 CI, 25 CI,
26 26
27 /// Production audit - see all events, minimal writes 27 /// Production audit - see all events, minimal writes
28 Production, 28 Production,
29} 29}
@@ -39,7 +39,7 @@ impl AuditConfig {
39 read_only: false, 39 read_only: false,
40 } 40 }
41 } 41 }
42 42
43 /// Create config for production audit 43 /// Create config for production audit
44 pub fn production() -> Self { 44 pub fn production() -> Self {
45 let run_id = format!("prod-audit-{}", Timestamp::now().as_u64()); 45 let run_id = format!("prod-audit-{}", Timestamp::now().as_u64());
@@ -47,10 +47,10 @@ impl AuditConfig {
47 run_id, 47 run_id,
48 mode: AuditMode::Production, 48 mode: AuditMode::Production,
49 cleanup_after: Timestamp::now() + 300, // 5 minutes from now 49 cleanup_after: Timestamp::now() + 300, // 5 minutes from now
50 read_only: true, // Default to read-only for production 50 read_only: true, // Default to read-only for production
51 } 51 }
52 } 52 }
53 53
54 /// Create config with custom run ID 54 /// Create config with custom run ID
55 pub fn with_run_id(run_id: String, mode: AuditMode) -> Self { 55 pub fn with_run_id(run_id: String, mode: AuditMode) -> Self {
56 Self { 56 Self {
@@ -60,7 +60,7 @@ impl AuditConfig {
60 read_only: mode == AuditMode::Production, 60 read_only: mode == AuditMode::Production,
61 } 61 }
62 } 62 }
63 63
64 /// Get audit tags that are automatically added to all events 64 /// Get audit tags that are automatically added to all events
65 /// 65 ///
66 /// These tags are automatically added to all events created via [`AuditEventBuilder`]. 66 /// These tags are automatically added to all events created via [`AuditEventBuilder`].
@@ -102,22 +102,22 @@ impl AuditConfig {
102 /// ``` 102 /// ```
103 pub fn audit_tags(&self) -> Vec<Tag> { 103 pub fn audit_tags(&self) -> Vec<Tag> {
104 use nostr_sdk::prelude::{Alphabet, SingleLetterTag}; 104 use nostr_sdk::prelude::{Alphabet, SingleLetterTag};
105 105
106 // Use "t" tags for categorization (standard NIP-01 hashtag type) 106 // Use "t" tags for categorization (standard NIP-01 hashtag type)
107 let t_tag = SingleLetterTag::lowercase(Alphabet::T); 107 let t_tag = SingleLetterTag::lowercase(Alphabet::T);
108 108
109 vec![ 109 vec![
110 Tag::custom(TagKind::SingleLetter(t_tag), vec!["grasp-audit-test-event"]),
110 Tag::custom( 111 Tag::custom(
111 TagKind::SingleLetter(t_tag), 112 TagKind::SingleLetter(t_tag),
112 vec!["grasp-audit-test-event"] 113 vec![format!("audit-{}", self.run_id)],
113 ), 114 ),
114 Tag::custom( 115 Tag::custom(
115 TagKind::SingleLetter(t_tag), 116 TagKind::SingleLetter(t_tag),
116 vec![format!("audit-{}", self.run_id)] 117 vec![format!(
117 ), 118 "audit-cleanup-after-{}",
118 Tag::custom( 119 self.cleanup_after.as_u64()
119 TagKind::SingleLetter(t_tag), 120 )],
120 vec![format!("audit-cleanup-after-{}", self.cleanup_after.as_u64())]
121 ), 121 ),
122 ] 122 ]
123 } 123 }
@@ -141,28 +141,28 @@ impl AuditEventBuilder {
141 config, 141 config,
142 } 142 }
143 } 143 }
144 144
145 /// Add a tag 145 /// Add a tag
146 pub fn tag(mut self, tag: Tag) -> Self { 146 pub fn tag(mut self, tag: Tag) -> Self {
147 self.tags.push(tag); 147 self.tags.push(tag);
148 self 148 self
149 } 149 }
150 150
151 /// Add multiple tags 151 /// Add multiple tags
152 pub fn tags(mut self, tags: Vec<Tag>) -> Self { 152 pub fn tags(mut self, tags: Vec<Tag>) -> Self {
153 self.tags.extend(tags); 153 self.tags.extend(tags);
154 self 154 self
155 } 155 }
156 156
157 /// Build the event with audit tags 157 /// Build the event with audit tags
158 pub fn build(self, keys: &Keys) -> anyhow::Result<Event> { 158 pub fn build(self, keys: &Keys) -> anyhow::Result<Event> {
159 let mut all_tags = self.tags; 159 let mut all_tags = self.tags;
160 all_tags.extend(self.config.audit_tags()); 160 all_tags.extend(self.config.audit_tags());
161 161
162 let event = EventBuilder::new(self.kind, self.content) 162 let event = EventBuilder::new(self.kind, self.content)
163 .tags(all_tags) 163 .tags(all_tags)
164 .sign_with_keys(keys)?; 164 .sign_with_keys(keys)?;
165 165
166 Ok(event) 166 Ok(event)
167 } 167 }
168} 168}
@@ -170,7 +170,7 @@ impl AuditEventBuilder {
170#[cfg(test)] 170#[cfg(test)]
171mod tests { 171mod tests {
172 use super::*; 172 use super::*;
173 173
174 #[test] 174 #[test]
175 fn test_ci_config() { 175 fn test_ci_config() {
176 let config = AuditConfig::ci(); 176 let config = AuditConfig::ci();
@@ -178,7 +178,7 @@ mod tests {
178 assert!(!config.read_only); 178 assert!(!config.read_only);
179 assert!(config.run_id.starts_with("ci-")); 179 assert!(config.run_id.starts_with("ci-"));
180 } 180 }
181 181
182 #[test] 182 #[test]
183 fn test_production_config() { 183 fn test_production_config() {
184 let config = AuditConfig::production(); 184 let config = AuditConfig::production();
@@ -186,18 +186,18 @@ mod tests {
186 assert!(config.read_only); 186 assert!(config.read_only);
187 assert!(config.run_id.starts_with("prod-audit-")); 187 assert!(config.run_id.starts_with("prod-audit-"));
188 } 188 }
189 189
190 #[test] 190 #[test]
191 fn test_audit_tags() { 191 fn test_audit_tags() {
192 use nostr_sdk::prelude::{Alphabet, SingleLetterTag}; 192 use nostr_sdk::prelude::{Alphabet, SingleLetterTag};
193 193
194 let config = AuditConfig::ci(); 194 let config = AuditConfig::ci();
195 let tags = config.audit_tags(); 195 let tags = config.audit_tags();
196 196
197 assert_eq!(tags.len(), 3); 197 assert_eq!(tags.len(), 3);
198 198
199 let t_tag = SingleLetterTag::lowercase(Alphabet::T); 199 let t_tag = SingleLetterTag::lowercase(Alphabet::T);
200 200
201 // All tags should be "t" tags (hashtags) 201 // All tags should be "t" tags (hashtags)
202 for tag in &tags { 202 for tag in &tags {
203 if let TagKind::SingleLetter(letter) = tag.kind() { 203 if let TagKind::SingleLetter(letter) = tag.kind() {
@@ -206,36 +206,40 @@ mod tests {
206 panic!("Expected SingleLetter tag"); 206 panic!("Expected SingleLetter tag");
207 } 207 }
208 } 208 }
209 209
210 // Check for "t" tag with "grasp-audit-test-event" 210 // Check for "t" tag with "grasp-audit-test-event"
211 assert!(tags.iter().any(|t| { 211 assert!(tags
212 t.content() == Some("grasp-audit-test-event") 212 .iter()
213 })); 213 .any(|t| { t.content() == Some("grasp-audit-test-event") }));
214 214
215 // Check for "t" tag with "audit-{run_id}" 215 // Check for "t" tag with "audit-{run_id}"
216 assert!(tags.iter().any(|t| { 216 assert!(tags.iter().any(|t| {
217 t.content().map(|c| c.starts_with("audit-ci-")).unwrap_or(false) 217 t.content()
218 .map(|c| c.starts_with("audit-ci-"))
219 .unwrap_or(false)
218 })); 220 }));
219 221
220 // Check for "t" tag with "audit-cleanup-after-{timestamp}" 222 // Check for "t" tag with "audit-cleanup-after-{timestamp}"
221 assert!(tags.iter().any(|t| { 223 assert!(tags.iter().any(|t| {
222 t.content().map(|c| c.starts_with("audit-cleanup-after-")).unwrap_or(false) 224 t.content()
225 .map(|c| c.starts_with("audit-cleanup-after-"))
226 .unwrap_or(false)
223 })); 227 }));
224 } 228 }
225 229
226 #[test] 230 #[test]
227 fn test_audit_event_builder() { 231 fn test_audit_event_builder() {
228 let config = AuditConfig::ci(); 232 let config = AuditConfig::ci();
229 let keys = Keys::generate(); 233 let keys = Keys::generate();
230 234
231 let event = AuditEventBuilder::new(Kind::TextNote, "test", config.clone()) 235 let event = AuditEventBuilder::new(Kind::TextNote, "test", config.clone())
232 .tag(Tag::custom(TagKind::Custom("test".into()), vec!["value"])) 236 .tag(Tag::custom(TagKind::Custom("test".into()), vec!["value"]))
233 .build(&keys) 237 .build(&keys)
234 .unwrap(); 238 .unwrap();
235 239
236 // Should have our custom tag + 3 audit tags 240 // Should have our custom tag + 3 audit tags
237 assert!(event.tags.len() >= 4); 241 assert!(event.tags.len() >= 4);
238 242
239 // Verify event is valid 243 // Verify event is valid
240 assert!(event.verify().is_ok()); 244 assert!(event.verify().is_ok());
241 } 245 }
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 {
18 /// Relay URL (e.g., ws://localhost:7000) 18 /// Relay URL (e.g., ws://localhost:7000)
19 #[arg(short, long)] 19 #[arg(short, long)]
20 relay: String, 20 relay: String,
21 21
22 /// Mode: ci or production 22 /// Mode: ci or production
23 #[arg(short, long, default_value = "ci")] 23 #[arg(short, long, default_value = "ci")]
24 mode: String, 24 mode: String,
25 25
26 /// Spec to test (nip01-smoke, all) 26 /// Spec to test (nip01-smoke, all)
27 #[arg(short, long, default_value = "nip01-smoke")] 27 #[arg(short, long, default_value = "nip01-smoke")]
28 spec: String, 28 spec: String,
@@ -35,12 +35,12 @@ async fn main() -> Result<()> {
35 tracing_subscriber::fmt() 35 tracing_subscriber::fmt()
36 .with_env_filter( 36 .with_env_filter(
37 tracing_subscriber::EnvFilter::from_default_env() 37 tracing_subscriber::EnvFilter::from_default_env()
38 .add_directive(tracing::Level::INFO.into()) 38 .add_directive(tracing::Level::INFO.into()),
39 ) 39 )
40 .init(); 40 .init();
41 41
42 let cli = Cli::parse(); 42 let cli = Cli::parse();
43 43
44 match cli.command { 44 match cli.command {
45 Commands::Audit { relay, mode, spec } => { 45 Commands::Audit { relay, mode, spec } => {
46 let config = match mode.as_str() { 46 let config = match mode.as_str() {
@@ -48,7 +48,7 @@ async fn main() -> Result<()> {
48 "production" => AuditConfig::production(), 48 "production" => AuditConfig::production(),
49 _ => return Err(anyhow!("Invalid mode: {}. Use 'ci' or 'production'", mode)), 49 _ => return Err(anyhow!("Invalid mode: {}. Use 'ci' or 'production'", mode)),
50 }; 50 };
51 51
52 println!("🔍 GRASP Audit Tool"); 52 println!("🔍 GRASP Audit Tool");
53 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 53 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
54 println!("Relay: {}", relay); 54 println!("Relay: {}", relay);
@@ -57,17 +57,18 @@ async fn main() -> Result<()> {
57 println!("Run ID: {}", config.run_id); 57 println!("Run ID: {}", config.run_id);
58 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); 58 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
59 println!(); 59 println!();
60 60
61 println!("Connecting to relay..."); 61 println!("Connecting to relay...");
62 let client = AuditClient::new(&relay, config).await 62 let client = AuditClient::new(&relay, config)
63 .await
63 .map_err(|e| anyhow!("Failed to connect to relay: {}", e))?; 64 .map_err(|e| anyhow!("Failed to connect to relay: {}", e))?;
64 65
65 if !client.is_connected().await { 66 if !client.is_connected().await {
66 return Err(anyhow!("Could not establish connection to relay")); 67 return Err(anyhow!("Could not establish connection to relay"));
67 } 68 }
68 69
69 println!("✓ Connected\n"); 70 println!("✓ Connected\n");
70 71
71 let results = match spec.as_str() { 72 let results = match spec.as_str() {
72 "nip01-smoke" => { 73 "nip01-smoke" => {
73 println!("Running NIP-01 smoke tests...\n"); 74 println!("Running NIP-01 smoke tests...\n");
@@ -77,11 +78,16 @@ async fn main() -> Result<()> {
77 println!("Running all tests...\n"); 78 println!("Running all tests...\n");
78 specs::Nip01SmokeTests::run_all(&client).await 79 specs::Nip01SmokeTests::run_all(&client).await
79 } 80 }
80 _ => return Err(anyhow!("Unknown spec: {}. Use 'nip01-smoke' or 'all'", spec)), 81 _ => {
82 return Err(anyhow!(
83 "Unknown spec: {}. Use 'nip01-smoke' or 'all'",
84 spec
85 ))
86 }
81 }; 87 };
82 88
83 results.print_report(); 89 results.print_report();
84 90
85 if !results.all_passed() { 91 if !results.all_passed() {
86 println!("❌ Some tests failed"); 92 println!("❌ Some tests failed");
87 std::process::exit(1); 93 std::process::exit(1);
@@ -90,6 +96,6 @@ async fn main() -> Result<()> {
90 } 96 }
91 } 97 }
92 } 98 }
93 99
94 Ok(()) 100 Ok(())
95} 101}
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 {
24 keys, 24 keys,
25 } 25 }
26 } 26 }
27 27
28 /// Create a new audit client 28 /// Create a new audit client
29 pub async fn new(relay_url: &str, config: AuditConfig) -> Result<Self> { 29 pub async fn new(relay_url: &str, config: AuditConfig) -> Result<Self> {
30 let keys = Keys::generate(); 30 let keys = Keys::generate();
31 let client = Client::new(keys.clone()); 31 let client = Client::new(keys.clone());
32 32
33 // Add relay and connect 33 // Add relay and connect
34 client.add_relay(relay_url).await?; 34 client.add_relay(relay_url).await?;
35 client.connect().await; 35 client.connect().await;
36 36
37 // Wait for connection to establish (with retries) 37 // Wait for connection to establish (with retries)
38 let mut attempts = 0; 38 let mut attempts = 0;
39 let mut connected = false; 39 let mut connected = false;
40 while attempts < 20 { 40 while attempts < 20 {
41 tokio::time::sleep(Duration::from_millis(100)).await; 41 tokio::time::sleep(Duration::from_millis(100)).await;
42 42
43 let relays = client.relays().await; 43 let relays = client.relays().await;
44 connected = relays.values().any(|r| r.is_connected()); 44 connected = relays.values().any(|r| r.is_connected());
45 45
46 if connected { 46 if connected {
47 break; 47 break;
48 } 48 }
49 49
50 attempts += 1; 50 attempts += 1;
51 } 51 }
52 52
53 // Verify we actually connected 53 // Verify we actually connected
54 if !connected { 54 if !connected {
55 return Err(anyhow!( 55 return Err(anyhow!(
@@ -68,22 +68,22 @@ impl AuditClient {
68 relay_url 68 relay_url
69 )); 69 ));
70 } 70 }
71 71
72 // Give it a bit more time to stabilize 72 // Give it a bit more time to stabilize
73 tokio::time::sleep(Duration::from_millis(200)).await; 73 tokio::time::sleep(Duration::from_millis(200)).await;
74 74
75 Ok(Self { 75 Ok(Self {
76 client, 76 client,
77 config, 77 config,
78 keys, 78 keys,
79 }) 79 })
80 } 80 }
81 81
82 /// Get the public key for this audit client 82 /// Get the public key for this audit client
83 pub fn public_key(&self) -> PublicKey { 83 pub fn public_key(&self) -> PublicKey {
84 self.keys.public_key() 84 self.keys.public_key()
85 } 85 }
86 86
87 /// Check if connected to relay 87 /// Check if connected to relay
88 pub async fn is_connected(&self) -> bool { 88 pub async fn is_connected(&self) -> bool {
89 // Check if we have any connected relays 89 // Check if we have any connected relays
@@ -95,29 +95,29 @@ impl AuditClient {
95 } 95 }
96 false 96 false
97 } 97 }
98 98
99 /// Send an event (with audit tags automatically added) 99 /// Send an event (with audit tags automatically added)
100 pub async fn send_event(&self, event: Event) -> Result<EventId> { 100 pub async fn send_event(&self, event: Event) -> Result<EventId> {
101 if self.config.read_only { 101 if self.config.read_only {
102 return Err(anyhow!("Client is in read-only mode")); 102 return Err(anyhow!("Client is in read-only mode"));
103 } 103 }
104 104
105 let output = self.client.send_event(&event).await?; 105 let output = self.client.send_event(&event).await?;
106 let event_id = *output.id(); 106 let event_id = *output.id();
107 107
108 // Check if any relay rejected the event and return the error message 108 // Check if any relay rejected the event and return the error message
109 if !output.failed.is_empty() { 109 if !output.failed.is_empty() {
110 // Get the first failed relay error message 110 // Get the first failed relay error message
111 let (relay_url, error) = output.failed.iter().next().unwrap(); 111 let (relay_url, error) = output.failed.iter().next().unwrap();
112 return Err(anyhow!("Relay {} rejected event: {}", relay_url, error)); 112 return Err(anyhow!("Relay {} rejected event: {}", relay_url, error));
113 } 113 }
114 114
115 // Wait a bit for event to propagate 115 // Wait a bit for event to propagate
116 tokio::time::sleep(Duration::from_millis(100)).await; 116 tokio::time::sleep(Duration::from_millis(100)).await;
117 117
118 Ok(event_id) 118 Ok(event_id)
119 } 119 }
120 120
121 /// Create an event builder that automatically includes audit tags 121 /// Create an event builder that automatically includes audit tags
122 /// 122 ///
123 /// All events built through this method will automatically have audit tags appended 123 /// All events built through this method will automatically have audit tags appended
@@ -153,11 +153,11 @@ impl AuditClient {
153 pub fn event_builder(&self, kind: Kind, content: impl Into<String>) -> AuditEventBuilder { 153 pub fn event_builder(&self, kind: Kind, content: impl Into<String>) -> AuditEventBuilder {
154 AuditEventBuilder::new(kind, content, self.config.clone()) 154 AuditEventBuilder::new(kind, content, self.config.clone())
155 } 155 }
156 156
157 /// Query events, optionally filtered to this audit run 157 /// Query events, optionally filtered to this audit run
158 pub async fn query(&self, mut filter: Filter) -> Result<Vec<Event>> { 158 pub async fn query(&self, mut filter: Filter) -> Result<Vec<Event>> {
159 use nostr_sdk::prelude::{Alphabet, SingleLetterTag}; 159 use nostr_sdk::prelude::{Alphabet, SingleLetterTag};
160 160
161 if self.config.mode == AuditMode::CI { 161 if self.config.mode == AuditMode::CI {
162 // In CI mode, only see our own audit events 162 // In CI mode, only see our own audit events
163 // Filter by "t" tags (hashtags) 163 // Filter by "t" tags (hashtags)
@@ -167,14 +167,15 @@ impl AuditClient {
167 .custom_tag(t_tag, format!("audit-{}", self.config.run_id)); 167 .custom_tag(t_tag, format!("audit-{}", self.config.run_id));
168 } 168 }
169 // In Production mode, see all events (no filter modification) 169 // In Production mode, see all events (no filter modification)
170 170
171 let events = self.client 171 let events = self
172 .client
172 .fetch_events(filter, Duration::from_secs(5)) 173 .fetch_events(filter, Duration::from_secs(5))
173 .await?; 174 .await?;
174 175
175 Ok(events.into_iter().collect()) 176 Ok(events.into_iter().collect())
176 } 177 }
177 178
178 /// Subscribe to events with a callback 179 /// Subscribe to events with a callback
179 pub async fn subscribe( 180 pub async fn subscribe(
180 &self, 181 &self,
@@ -183,27 +184,25 @@ impl AuditClient {
183 ) -> Result<Vec<Event>> { 184 ) -> Result<Vec<Event>> {
184 let timeout = timeout.unwrap_or(Duration::from_secs(5)); 185 let timeout = timeout.unwrap_or(Duration::from_secs(5));
185 let mut all_events = Vec::new(); 186 let mut all_events = Vec::new();
186 187
187 for filter in filters { 188 for filter in filters {
188 let events = self.client 189 let events = self.client.fetch_events(filter, timeout).await?;
189 .fetch_events(filter, timeout)
190 .await?;
191 all_events.extend(events.into_iter()); 190 all_events.extend(events.into_iter());
192 } 191 }
193 192
194 Ok(all_events) 193 Ok(all_events)
195 } 194 }
196 195
197 /// Get the underlying nostr client (for advanced usage) 196 /// Get the underlying nostr client (for advanced usage)
198 pub fn client(&self) -> &Client { 197 pub fn client(&self) -> &Client {
199 &self.client 198 &self.client
200 } 199 }
201 200
202 /// Get the keys (for signing custom events) 201 /// Get the keys (for signing custom events)
203 pub fn keys(&self) -> &Keys { 202 pub fn keys(&self) -> &Keys {
204 &self.keys 203 &self.keys
205 } 204 }
206 205
207 /// Create a NIP-34 repository announcement event 206 /// Create a NIP-34 repository announcement event
208 /// 207 ///
209 /// This helper creates a properly formatted NIP-34 announcement that will be 208 /// This helper creates a properly formatted NIP-34 announcement that will be
@@ -216,37 +215,58 @@ impl AuditClient {
216 /// A built and signed Event ready to be sent to the relay 215 /// A built and signed Event ready to be sent to the relay
217 pub async fn create_repo_announcement(&self, test_name: &str) -> Result<Event> { 216 pub async fn create_repo_announcement(&self, test_name: &str) -> Result<Event> {
218 // Get relay URL from client 217 // Get relay URL from client
219 let relay_url = self.client.relays().await 218 let relay_url = self
219 .client
220 .relays()
221 .await
220 .keys() 222 .keys()
221 .next() 223 .next()
222 .ok_or_else(|| anyhow!("No relay connected"))? 224 .ok_or_else(|| anyhow!("No relay connected"))?
223 .to_string(); 225 .to_string();
224 226
225 // Convert WebSocket URL to HTTP URL for clone tag 227 // Convert WebSocket URL to HTTP URL for clone tag
226 let http_url = relay_url 228 let http_url = relay_url
227 .replace("ws://", "http://") 229 .replace("ws://", "http://")
228 .replace("wss://", "https://"); 230 .replace("wss://", "https://");
229 231
230 // Create unique repository identifier using UUID for consistency 232 // Create unique repository identifier using UUID for consistency
231 let repo_id = format!("{}-{}", test_name, &uuid::Uuid::new_v4().to_string()[..8]); 233 let repo_id = format!("{}-{}", test_name, &uuid::Uuid::new_v4().to_string()[..8]);
232 234
233 // Get npub for clone URL 235 // Get npub for clone URL
234 let npub = self.public_key().to_bech32() 236 let npub = self
237 .public_key()
238 .to_bech32()
235 .map_err(|e| anyhow!("Failed to convert public key to bech32 npub format: {}", e))?; 239 .map_err(|e| anyhow!("Failed to convert public key to bech32 npub format: {}", e))?;
236 240
237 // Build kind 30617 repository announcement 241 // Build kind 30617 repository announcement
238 let event = self.event_builder(Kind::GitRepoAnnouncement, format!("Test repository for {}", test_name)) 242 let event = self
243 .event_builder(
244 Kind::GitRepoAnnouncement,
245 format!("Test repository for {}", test_name),
246 )
239 .tag(Tag::identifier(&repo_id)) 247 .tag(Tag::identifier(&repo_id))
240 .tag(Tag::custom(TagKind::custom("name"), vec![format!("{} Test Repository", test_name)])) 248 .tag(Tag::custom(
241 .tag(Tag::custom(TagKind::custom("description"), vec![format!("Repository for {} testing", test_name)])) 249 TagKind::custom("name"),
242 .tag(Tag::custom(TagKind::custom("clone"), vec![format!("{}/{}/{}.git", http_url, npub, repo_id)])) 250 vec![format!("{} Test Repository", test_name)],
243 .tag(Tag::custom(TagKind::custom("relays"), vec![relay_url.clone()])) 251 ))
252 .tag(Tag::custom(
253 TagKind::custom("description"),
254 vec![format!("Repository for {} testing", test_name)],
255 ))
256 .tag(Tag::custom(
257 TagKind::custom("clone"),
258 vec![format!("{}/{}/{}.git", http_url, npub, repo_id)],
259 ))
260 .tag(Tag::custom(
261 TagKind::custom("relays"),
262 vec![relay_url.clone()],
263 ))
244 .build(self.keys()) 264 .build(self.keys())
245 .map_err(|e| anyhow!("Failed to build repository announcement event: {}", e))?; 265 .map_err(|e| anyhow!("Failed to build repository announcement event: {}", e))?;
246 266
247 Ok(event) 267 Ok(event)
248 } 268 }
249 269
250 /// Create an issue (kind 1621) that references a repository 270 /// Create an issue (kind 1621) that references a repository
251 /// 271 ///
252 /// # Arguments 272 /// # Arguments
@@ -265,29 +285,31 @@ impl AuditClient {
265 additional_tags: Vec<Tag>, 285 additional_tags: Vec<Tag>,
266 ) -> Result<Event> { 286 ) -> Result<Event> {
267 // Extract repo_id from the d tag 287 // Extract repo_id from the d tag
268 let repo_id = repo_event.tags.iter() 288 let repo_id = repo_event
289 .tags
290 .iter()
269 .find(|t| t.kind() == TagKind::d()) 291 .find(|t| t.kind() == TagKind::d())
270 .and_then(|t| t.content()) 292 .and_then(|t| t.content())
271 .ok_or_else(|| anyhow!("Repository event must have a 'd' tag"))? 293 .ok_or_else(|| anyhow!("Repository event must have a 'd' tag"))?
272 .to_string(); 294 .to_string();
273 295
274 let repo_pubkey = repo_event.pubkey; 296 let repo_pubkey = repo_event.pubkey;
275 let a_tag_value = format!("30617:{}:{}", repo_pubkey, repo_id); 297 let a_tag_value = format!("30617:{}:{}", repo_pubkey, repo_id);
276 298
277 let mut tags = vec![ 299 let mut tags = vec![
278 Tag::custom(TagKind::custom("a"), vec![a_tag_value]), 300 Tag::custom(TagKind::custom("a"), vec![a_tag_value]),
279 Tag::custom(TagKind::custom("subject"), vec![issue_title]), 301 Tag::custom(TagKind::custom("subject"), vec![issue_title]),
280 ]; 302 ];
281 303
282 // Add any additional tags 304 // Add any additional tags
283 tags.extend(additional_tags); 305 tags.extend(additional_tags);
284 306
285 self.event_builder(Kind::Custom(1621), content) 307 self.event_builder(Kind::Custom(1621), content)
286 .tags(tags) 308 .tags(tags)
287 .build(self.keys()) 309 .build(self.keys())
288 .map_err(|e| anyhow!("Failed to build issue event: {}", e)) 310 .map_err(|e| anyhow!("Failed to build issue event: {}", e))
289 } 311 }
290 312
291 /// Create a NIP-22 comment (kind 1111) for an event 313 /// Create a NIP-22 comment (kind 1111) for an event
292 /// 314 ///
293 /// # Arguments 315 /// # Arguments
@@ -306,17 +328,20 @@ impl AuditClient {
306 let event_kind = event.kind; 328 let event_kind = event.kind;
307 let event_pubkey = event.pubkey; 329 let event_pubkey = event.pubkey;
308 let event_id = event.id; 330 let event_id = event.id;
309 331
310 let mut tags = vec![ 332 let mut tags = vec![
311 Tag::custom(TagKind::custom("E"), vec![event_id.to_hex(), "".to_string(), "root".to_string()]), 333 Tag::custom(
334 TagKind::custom("E"),
335 vec![event_id.to_hex(), "".to_string(), "root".to_string()],
336 ),
312 Tag::event(event_id), 337 Tag::event(event_id),
313 Tag::custom(TagKind::custom("K"), vec![event_kind.as_u16().to_string()]), 338 Tag::custom(TagKind::custom("K"), vec![event_kind.as_u16().to_string()]),
314 Tag::public_key(event_pubkey), 339 Tag::public_key(event_pubkey),
315 ]; 340 ];
316 341
317 // Add any additional tags 342 // Add any additional tags
318 tags.extend(additional_tags); 343 tags.extend(additional_tags);
319 344
320 self.event_builder(Kind::Custom(1111), content) 345 self.event_builder(Kind::Custom(1111), content)
321 .tags(tags) 346 .tags(tags)
322 .build(self.keys()) 347 .build(self.keys())
@@ -327,22 +352,22 @@ impl AuditClient {
327#[cfg(test)] 352#[cfg(test)]
328mod tests { 353mod tests {
329 use super::*; 354 use super::*;
330 355
331 #[tokio::test] 356 #[tokio::test]
332 async fn test_client_creation() { 357 async fn test_client_creation() {
333 let config = AuditConfig::ci(); 358 let config = AuditConfig::ci();
334 359
335 // This will fail if no relay is running, which is expected in tests 360 // This will fail if no relay is running, which is expected in tests
336 // In real usage, there should be a relay at the URL 361 // In real usage, there should be a relay at the URL
337 let result = AuditClient::new("ws://localhost:7000", config).await; 362 let result = AuditClient::new("ws://localhost:7000", config).await;
338 363
339 // We can't test connection without a running relay 364 // We can't test connection without a running relay
340 // But we can test that the client is created 365 // But we can test that the client is created
341 if let Ok(client) = result { 366 if let Ok(client) = result {
342 assert_eq!(client.config.mode, AuditMode::CI); 367 assert_eq!(client.config.mode, AuditMode::CI);
343 } 368 }
344 } 369 }
345 370
346 #[test] 371 #[test]
347 fn test_event_builder() { 372 fn test_event_builder() {
348 let config = AuditConfig::ci(); 373 let config = AuditConfig::ci();
@@ -352,13 +377,13 @@ mod tests {
352 config: config.clone(), 377 config: config.clone(),
353 keys: keys.clone(), 378 keys: keys.clone(),
354 }; 379 };
355 380
356 let _builder = client.event_builder(Kind::TextNote, "test content"); 381 let _builder = client.event_builder(Kind::TextNote, "test content");
357 382
358 // Builder should be created successfully 383 // Builder should be created successfully
359 // (We can't test the internal config field as it's private, which is correct) 384 // (We can't test the internal config field as it's private, which is correct)
360 } 385 }
361 386
362 #[test] 387 #[test]
363 fn test_audit_tags_automatically_added() { 388 fn test_audit_tags_automatically_added() {
364 let config = AuditConfig::ci(); 389 let config = AuditConfig::ci();
@@ -368,21 +393,28 @@ mod tests {
368 config: config.clone(), 393 config: config.clone(),
369 keys: keys.clone(), 394 keys: keys.clone(),
370 }; 395 };
371 396
372 // Create an event with a custom tag 397 // Create an event with a custom tag
373 let event = client.event_builder(Kind::TextNote, "test content") 398 let event = client
399 .event_builder(Kind::TextNote, "test content")
374 .tag(Tag::custom(TagKind::custom("custom"), vec!["value"])) 400 .tag(Tag::custom(TagKind::custom("custom"), vec!["value"]))
375 .build(&keys) 401 .build(&keys)
376 .unwrap(); 402 .unwrap();
377 403
378 // Should have custom tag (1) + 3 audit tags = at least 4 tags 404 // Should have custom tag (1) + 3 audit tags = at least 4 tags
379 assert!(event.tags.len() >= 4, "Expected at least 4 tags, got {}", event.tags.len()); 405 assert!(
380 406 event.tags.len() >= 4,
407 "Expected at least 4 tags, got {}",
408 event.tags.len()
409 );
410
381 // Verify audit tags are present by checking tag content 411 // Verify audit tags are present by checking tag content
382 let tag_contents: Vec<String> = event.tags.iter() 412 let tag_contents: Vec<String> = event
413 .tags
414 .iter()
383 .filter_map(|t| t.content().map(|s| s.to_string())) 415 .filter_map(|t| t.content().map(|s| s.to_string()))
384 .collect(); 416 .collect();
385 417
386 // Check for the three required audit tags 418 // Check for the three required audit tags
387 assert!( 419 assert!(
388 tag_contents.contains(&"grasp-audit-test-event".to_string()), 420 tag_contents.contains(&"grasp-audit-test-event".to_string()),
@@ -393,10 +425,12 @@ mod tests {
393 "Missing 'audit-ci-*' tag" 425 "Missing 'audit-ci-*' tag"
394 ); 426 );
395 assert!( 427 assert!(
396 tag_contents.iter().any(|t| t.starts_with("audit-cleanup-after-")), 428 tag_contents
429 .iter()
430 .any(|t| t.starts_with("audit-cleanup-after-")),
397 "Missing 'audit-cleanup-after-*' tag" 431 "Missing 'audit-cleanup-after-*' tag"
398 ); 432 );
399 433
400 // Verify the custom tag is also present 434 // Verify the custom tag is also present
401 assert!( 435 assert!(
402 tag_contents.contains(&"value".to_string()), 436 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};
33pub enum FixtureKind { 33pub enum FixtureKind {
34 /// Basic repository announcement (kind 30617) 34 /// Basic repository announcement (kind 30617)
35 ValidRepo, 35 ValidRepo,
36 36
37 /// Repository with one issue (kind 1621) 37 /// Repository with one issue (kind 1621)
38 RepoWithIssue, 38 RepoWithIssue,
39 39
40 /// Repository with issue and comment (kind 1111) 40 /// Repository with issue and comment (kind 1111)
41 RepoWithComment, 41 RepoWithComment,
42 42
43 /// Repository state announcement (kind 30618) 43 /// Repository state announcement (kind 30618)
44 RepoState, 44 RepoState,
45} 45}
@@ -49,7 +49,7 @@ pub enum FixtureKind {
49pub enum ContextMode { 49pub enum ContextMode {
50 /// Create fresh fixtures for each request (test isolation) 50 /// Create fresh fixtures for each request (test isolation)
51 Isolated, 51 Isolated,
52 52
53 /// Reuse shared fixtures across requests (minimal events) 53 /// Reuse shared fixtures across requests (minimal events)
54 Shared, 54 Shared,
55} 55}
@@ -104,7 +104,7 @@ impl<'a> TestContext<'a> {
104 cache: Arc::new(Mutex::new(HashMap::new())), 104 cache: Arc::new(Mutex::new(HashMap::new())),
105 } 105 }
106 } 106 }
107 107
108 /// Create a test context with explicit mode override 108 /// Create a test context with explicit mode override
109 /// 109 ///
110 /// This is useful for testing the context itself or for advanced use cases 110 /// This is useful for testing the context itself or for advanced use cases
@@ -116,7 +116,7 @@ impl<'a> TestContext<'a> {
116 cache: Arc::new(Mutex::new(HashMap::new())), 116 cache: Arc::new(Mutex::new(HashMap::new())),
117 } 117 }
118 } 118 }
119 119
120 /// Get a fixture, creating it if needed based on mode 120 /// Get a fixture, creating it if needed based on mode
121 /// 121 ///
122 /// # Behavior 122 /// # Behavior
@@ -139,7 +139,7 @@ impl<'a> TestContext<'a> {
139 ContextMode::Shared => self.get_or_create_shared(kind).await, 139 ContextMode::Shared => self.get_or_create_shared(kind).await,
140 } 140 }
141 } 141 }
142 142
143 /// Get the underlying client for direct access 143 /// Get the underlying client for direct access
144 /// 144 ///
145 /// This allows tests to use the client directly when needed while still 145 /// This allows tests to use the client directly when needed while still
@@ -147,23 +147,27 @@ impl<'a> TestContext<'a> {
147 pub fn client(&self) -> &'a AuditClient { 147 pub fn client(&self) -> &'a AuditClient {
148 self.client 148 self.client
149 } 149 }
150 150
151 /// Get the current context mode 151 /// Get the current context mode
152 pub fn mode(&self) -> ContextMode { 152 pub fn mode(&self) -> ContextMode {
153 self.mode 153 self.mode
154 } 154 }
155 155
156 /// Create a fresh fixture (always creates new) 156 /// Create a fresh fixture (always creates new)
157 async fn create_fresh(&self, kind: FixtureKind) -> Result<Event> { 157 async fn create_fresh(&self, kind: FixtureKind) -> Result<Event> {
158 let event = self.build_fixture(kind).await 158 let event = self
159 .build_fixture(kind)
160 .await
159 .with_context(|| format!("Failed to build {:?} fixture", kind))?; 161 .with_context(|| format!("Failed to build {:?} fixture", kind))?;
160 162
161 self.client.send_event(event.clone()).await 163 self.client
164 .send_event(event.clone())
165 .await
162 .with_context(|| format!("Failed to send {:?} fixture event to relay", kind))?; 166 .with_context(|| format!("Failed to send {:?} fixture event to relay", kind))?;
163 167
164 Ok(event) 168 Ok(event)
165 } 169 }
166 170
167 /// Get or create a shared fixture (caches for reuse) 171 /// Get or create a shared fixture (caches for reuse)
168 async fn get_or_create_shared(&self, kind: FixtureKind) -> Result<Event> { 172 async fn get_or_create_shared(&self, kind: FixtureKind) -> Result<Event> {
169 // Check cache first 173 // Check cache first
@@ -173,39 +177,54 @@ impl<'a> TestContext<'a> {
173 return Ok(event.clone()); 177 return Ok(event.clone());
174 } 178 }
175 } 179 }
176 180
177 // Not in cache, create it 181 // Not in cache, create it
178 let event = self.build_fixture(kind).await 182 let event = self
183 .build_fixture(kind)
184 .await
179 .with_context(|| format!("Failed to build {:?} fixture for shared cache", kind))?; 185 .with_context(|| format!("Failed to build {:?} fixture for shared cache", kind))?;
180 186
181 self.client.send_event(event.clone()).await 187 self.client
182 .with_context(|| format!("Failed to send {:?} fixture event to relay (shared cache)", kind))?; 188 .send_event(event.clone())
183 189 .await
190 .with_context(|| {
191 format!(
192 "Failed to send {:?} fixture event to relay (shared cache)",
193 kind
194 )
195 })?;
196
184 // Store in cache 197 // Store in cache
185 { 198 {
186 let mut cache = self.cache.lock().unwrap(); 199 let mut cache = self.cache.lock().unwrap();
187 cache.insert(kind, event.clone()); 200 cache.insert(kind, event.clone());
188 } 201 }
189 202
190 Ok(event) 203 Ok(event)
191 } 204 }
192 205
193 /// Build a fixture event (doesn't send it) 206 /// Build a fixture event (doesn't send it)
194 async fn build_fixture(&self, kind: FixtureKind) -> Result<Event> { 207 async fn build_fixture(&self, kind: FixtureKind) -> Result<Event> {
195 match kind { 208 match kind {
196 FixtureKind::ValidRepo => { 209 FixtureKind::ValidRepo => {
197 let test_name = format!("fixture-{:?}-{}", kind, &uuid::Uuid::new_v4().to_string()[..8]); 210 let test_name = format!(
211 "fixture-{:?}-{}",
212 kind,
213 &uuid::Uuid::new_v4().to_string()[..8]
214 );
198 self.client.create_repo_announcement(&test_name).await 215 self.client.create_repo_announcement(&test_name).await
199 } 216 }
200 217
201 FixtureKind::RepoWithIssue => { 218 FixtureKind::RepoWithIssue => {
202 use nostr_sdk::prelude::*;
203
204 // First create and send repo 219 // First create and send repo
205 let test_name = format!("fixture-{:?}-{}", FixtureKind::ValidRepo, &uuid::Uuid::new_v4().to_string()[..8]); 220 let test_name = format!(
221 "fixture-{:?}-{}",
222 FixtureKind::ValidRepo,
223 &uuid::Uuid::new_v4().to_string()[..8]
224 );
206 let repo = self.client.create_repo_announcement(&test_name).await?; 225 let repo = self.client.create_repo_announcement(&test_name).await?;
207 self.client.send_event(repo.clone()).await?; 226 self.client.send_event(repo.clone()).await?;
208 227
209 // Then create issue referencing it - this will have 'a' tag to repo 228 // Then create issue referencing it - this will have 'a' tag to repo
210 // Note: We build the issue but DON'T send it here - the caller will send it 229 // Note: We build the issue but DON'T send it here - the caller will send it
211 let issue = self.client.create_issue( 230 let issue = self.client.create_issue(
@@ -214,64 +233,70 @@ impl<'a> TestContext<'a> {
214 "Issue content for testing", 233 "Issue content for testing",
215 vec![], 234 vec![],
216 )?; 235 )?;
217 236
218 // Return the issue - tests can extract repo reference from its 'a' tag 237 // Return the issue - tests can extract repo reference from its 'a' tag
219 // The caller (create_fresh/get_or_create_shared) will send this event 238 // The caller (create_fresh/get_or_create_shared) will send this event
220 Ok(issue) 239 Ok(issue)
221 } 240 }
222 241
223 FixtureKind::RepoWithComment => { 242 FixtureKind::RepoWithComment => {
224 // First create repo with issue 243 // First create repo with issue
225 let test_name = format!("fixture-{:?}-{}", FixtureKind::ValidRepo, &uuid::Uuid::new_v4().to_string()[..8]); 244 let test_name = format!(
245 "fixture-{:?}-{}",
246 FixtureKind::ValidRepo,
247 &uuid::Uuid::new_v4().to_string()[..8]
248 );
226 let repo = self.client.create_repo_announcement(&test_name).await?; 249 let repo = self.client.create_repo_announcement(&test_name).await?;
227 self.client.send_event(repo.clone()).await?; 250 self.client.send_event(repo.clone()).await?;
228 251
229 let issue = self.client.create_issue( 252 let issue =
230 &repo, 253 self.client
231 "Test Issue", 254 .create_issue(&repo, "Test Issue", "Issue content", vec![])?;
232 "Issue content",
233 vec![],
234 )?;
235 self.client.send_event(issue.clone()).await?; 255 self.client.send_event(issue.clone()).await?;
236 256
237 // Then create comment on issue 257 // Then create comment on issue
238 self.client.create_comment( 258 self.client.create_comment(&issue, "Test comment", vec![])
239 &issue,
240 "Test comment",
241 vec![],
242 )
243 } 259 }
244 260
245 FixtureKind::RepoState => { 261 FixtureKind::RepoState => {
246 use nostr_sdk::prelude::*; 262 use nostr_sdk::prelude::*;
247 263
248 // First create repo announcement 264 // First create repo announcement
249 let test_name = format!("fixture-{:?}-{}", FixtureKind::ValidRepo, &uuid::Uuid::new_v4().to_string()[..8]); 265 let test_name = format!(
266 "fixture-{:?}-{}",
267 FixtureKind::ValidRepo,
268 &uuid::Uuid::new_v4().to_string()[..8]
269 );
250 let repo = self.client.create_repo_announcement(&test_name).await?; 270 let repo = self.client.create_repo_announcement(&test_name).await?;
251 self.client.send_event(repo.clone()).await?; 271 self.client.send_event(repo.clone()).await?;
252 272
253 // Extract repo_id from repo announcement 273 // Extract repo_id from repo announcement
254 let repo_id = repo.tags.iter() 274 let repo_id = repo
275 .tags
276 .iter()
255 .find(|t| t.kind() == TagKind::d()) 277 .find(|t| t.kind() == TagKind::d())
256 .and_then(|t| t.content()) 278 .and_then(|t| t.content())
257 .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement"))? 279 .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement"))?
258 .to_string(); 280 .to_string();
259 281
260 // Create state announcement 282 // Create state announcement
261 self.client.event_builder(Kind::Custom(30618), "") 283 self.client
284 .event_builder(Kind::Custom(30618), "")
262 .tag(Tag::identifier(&repo_id)) 285 .tag(Tag::identifier(&repo_id))
263 .tag(Tag::custom(TagKind::custom("refs/heads/main"), vec![ 286 .tag(Tag::custom(
264 "abc123def456789012345678901234567890abcd" 287 TagKind::custom("refs/heads/main"),
265 ])) 288 vec!["abc123def456789012345678901234567890abcd"],
266 .tag(Tag::custom(TagKind::custom("HEAD"), vec![ 289 ))
267 "ref: refs/heads/main" 290 .tag(Tag::custom(
268 ])) 291 TagKind::custom("HEAD"),
292 vec!["ref: refs/heads/main"],
293 ))
269 .build(self.client.keys()) 294 .build(self.client.keys())
270 .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e)) 295 .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e))
271 } 296 }
272 } 297 }
273 } 298 }
274 299
275 /// Clear the fixture cache 300 /// Clear the fixture cache
276 /// 301 ///
277 /// This is useful for tests that want to ensure fresh fixtures 302 /// This is useful for tests that want to ensure fresh fixtures
@@ -286,34 +311,37 @@ impl<'a> TestContext<'a> {
286mod tests { 311mod tests {
287 use super::*; 312 use super::*;
288 use crate::AuditConfig; 313 use crate::AuditConfig;
289 314
290 #[test] 315 #[test]
291 fn test_context_mode_from_audit_mode() { 316 fn test_context_mode_from_audit_mode() {
292 assert_eq!(ContextMode::from(AuditMode::CI), ContextMode::Isolated); 317 assert_eq!(ContextMode::from(AuditMode::CI), ContextMode::Isolated);
293 assert_eq!(ContextMode::from(AuditMode::Production), ContextMode::Shared); 318 assert_eq!(
319 ContextMode::from(AuditMode::Production),
320 ContextMode::Shared
321 );
294 } 322 }
295 323
296 #[test] 324 #[test]
297 fn test_fixture_kind_hash() { 325 fn test_fixture_kind_hash() {
298 use std::collections::HashSet; 326 use std::collections::HashSet;
299 327
300 let mut set = HashSet::new(); 328 let mut set = HashSet::new();
301 set.insert(FixtureKind::ValidRepo); 329 set.insert(FixtureKind::ValidRepo);
302 set.insert(FixtureKind::RepoWithIssue); 330 set.insert(FixtureKind::RepoWithIssue);
303 331
304 assert!(set.contains(&FixtureKind::ValidRepo)); 332 assert!(set.contains(&FixtureKind::ValidRepo));
305 assert!(!set.contains(&FixtureKind::RepoWithComment)); 333 assert!(!set.contains(&FixtureKind::RepoWithComment));
306 } 334 }
307 335
308 #[tokio::test] 336 #[tokio::test]
309 async fn test_context_creation() { 337 async fn test_context_creation() {
310 let config = AuditConfig::ci(); 338 let config = AuditConfig::ci();
311 let client = crate::AuditClient::new_test(config); 339 let client = crate::AuditClient::new_test(config);
312 340
313 let ctx = TestContext::new(&client); 341 let ctx = TestContext::new(&client);
314 assert_eq!(ctx.mode(), ContextMode::Isolated); 342 assert_eq!(ctx.mode(), ContextMode::Isolated);
315 343
316 let ctx = TestContext::with_mode(&client, ContextMode::Shared); 344 let ctx = TestContext::with_mode(&client, ContextMode::Shared);
317 assert_eq!(ctx.mode(), ContextMode::Shared); 345 assert_eq!(ctx.mode(), ContextMode::Shared);
318 } 346 }
319} \ No newline at end of file 347}
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 {
11 .duration_since(std::time::UNIX_EPOCH) 11 .duration_since(std::time::UNIX_EPOCH)
12 .unwrap() 12 .unwrap()
13 .as_secs(); 13 .as_secs();
14 14
15 format!("test-{}-{}", timestamp, counter) 15 format!("test-{}-{}", timestamp, counter)
16} 16}
17 17
@@ -26,29 +26,29 @@ pub fn generate_prod_run_id() -> String {
26 .duration_since(std::time::UNIX_EPOCH) 26 .duration_since(std::time::UNIX_EPOCH)
27 .unwrap() 27 .unwrap()
28 .as_secs(); 28 .as_secs();
29 29
30 format!("prod-audit-{}", timestamp) 30 format!("prod-audit-{}", timestamp)
31} 31}
32 32
33#[cfg(test)] 33#[cfg(test)]
34mod tests { 34mod tests {
35 use super::*; 35 use super::*;
36 36
37 #[test] 37 #[test]
38 fn test_generate_test_id() { 38 fn test_generate_test_id() {
39 let id1 = generate_test_id(); 39 let id1 = generate_test_id();
40 let id2 = generate_test_id(); 40 let id2 = generate_test_id();
41 41
42 assert_ne!(id1, id2); 42 assert_ne!(id1, id2);
43 assert!(id1.starts_with("test-")); 43 assert!(id1.starts_with("test-"));
44 } 44 }
45 45
46 #[test] 46 #[test]
47 fn test_generate_ci_run_id() { 47 fn test_generate_ci_run_id() {
48 let id = generate_ci_run_id(); 48 let id = generate_ci_run_id();
49 assert!(id.starts_with("ci-")); 49 assert!(id.starts_with("ci-"));
50 } 50 }
51 51
52 #[test] 52 #[test]
53 fn test_generate_prod_run_id() { 53 fn test_generate_prod_run_id() {
54 let id = generate_prod_run_id(); 54 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 @@
29//! ``` 29//! ```
30 30
31pub mod audit; 31pub mod audit;
32pub mod fixtures;
33pub mod client; 32pub mod client;
33pub mod fixtures;
34pub mod isolation; 34pub mod isolation;
35pub mod result; 35pub mod result;
36pub mod specs; 36pub mod specs;
37 37
38pub use audit::{AuditConfig, AuditMode}; 38pub use audit::{AuditConfig, AuditMode};
39pub use fixtures::{ContextMode, FixtureKind, TestContext};
40pub use client::AuditClient; 39pub use client::AuditClient;
40pub use fixtures::{ContextMode, FixtureKind, TestContext};
41pub use result::{AuditResult, TestResult}; 41pub use result::{AuditResult, TestResult};
42 42
43// Re-export commonly used types 43// 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 {
25 duration: Duration::default(), 25 duration: Duration::default(),
26 } 26 }
27 } 27 }
28 28
29 /// Run a test function and capture the result 29 /// Run a test function and capture the result
30 pub async fn run<F, Fut>(mut self, test_fn: F) -> Self 30 pub async fn run<F, Fut>(mut self, test_fn: F) -> Self
31 where 31 where
@@ -33,7 +33,7 @@ impl TestResult {
33 Fut: std::future::Future<Output = Result<(), String>>, 33 Fut: std::future::Future<Output = Result<(), String>>,
34 { 34 {
35 let start = Instant::now(); 35 let start = Instant::now();
36 36
37 match test_fn().await { 37 match test_fn().await {
38 Ok(()) => { 38 Ok(()) => {
39 self.passed = true; 39 self.passed = true;
@@ -43,17 +43,17 @@ impl TestResult {
43 self.error = Some(e); 43 self.error = Some(e);
44 } 44 }
45 } 45 }
46 46
47 self.duration = start.elapsed(); 47 self.duration = start.elapsed();
48 self 48 self
49 } 49 }
50 50
51 /// Mark test as passed 51 /// Mark test as passed
52 pub fn pass(mut self) -> Self { 52 pub fn pass(mut self) -> Self {
53 self.passed = true; 53 self.passed = true;
54 self 54 self
55 } 55 }
56 56
57 /// Mark test as failed with error 57 /// Mark test as failed with error
58 pub fn fail(mut self, error: impl Into<String>) -> Self { 58 pub fn fail(mut self, error: impl Into<String>) -> Self {
59 self.passed = false; 59 self.passed = false;
@@ -77,68 +77,69 @@ impl AuditResult {
77 results: Vec::new(), 77 results: Vec::new(),
78 } 78 }
79 } 79 }
80 80
81 /// Add a test result 81 /// Add a test result
82 pub fn add(&mut self, result: TestResult) { 82 pub fn add(&mut self, result: TestResult) {
83 self.results.push(result); 83 self.results.push(result);
84 } 84 }
85 85
86 /// Merge another audit result 86 /// Merge another audit result
87 pub fn merge(&mut self, other: AuditResult) { 87 pub fn merge(&mut self, other: AuditResult) {
88 self.results.extend(other.results); 88 self.results.extend(other.results);
89 } 89 }
90 90
91 /// Check if all tests passed 91 /// Check if all tests passed
92 pub fn all_passed(&self) -> bool { 92 pub fn all_passed(&self) -> bool {
93 self.results.iter().all(|r| r.passed) 93 self.results.iter().all(|r| r.passed)
94 } 94 }
95 95
96 /// Get count of passed tests 96 /// Get count of passed tests
97 pub fn passed_count(&self) -> usize { 97 pub fn passed_count(&self) -> usize {
98 self.results.iter().filter(|r| r.passed).count() 98 self.results.iter().filter(|r| r.passed).count()
99 } 99 }
100 100
101 /// Get count of failed tests 101 /// Get count of failed tests
102 pub fn failed_count(&self) -> usize { 102 pub fn failed_count(&self) -> usize {
103 self.results.iter().filter(|r| !r.passed).count() 103 self.results.iter().filter(|r| !r.passed).count()
104 } 104 }
105 105
106 /// Get total count of tests 106 /// Get total count of tests
107 pub fn total_count(&self) -> usize { 107 pub fn total_count(&self) -> usize {
108 self.results.len() 108 self.results.len()
109 } 109 }
110 110
111 /// Print a detailed report 111 /// Print a detailed report
112 pub fn print_report(&self) { 112 pub fn print_report(&self) {
113 println!("\n{}", self.spec); 113 println!("\n{}", self.spec);
114 println!("{}", "═".repeat(60)); 114 println!("{}", "═".repeat(60));
115 println!(); 115 println!();
116 116
117 let passed = self.passed_count(); 117 let passed = self.passed_count();
118 let total = self.total_count(); 118 let total = self.total_count();
119 119
120 for result in &self.results { 120 for result in &self.results {
121 let status = if result.passed { "✓" } else { "✗" }; 121 let status = if result.passed { "✓" } else { "✗" };
122 122
123 println!("{} {} ({})", status, result.name, result.spec_ref); 123 println!("{} {} ({})", status, result.name, result.spec_ref);
124 println!(" Requirement: {}", result.requirement); 124 println!(" Requirement: {}", result.requirement);
125 125
126 if let Some(error) = &result.error { 126 if let Some(error) = &result.error {
127 println!(" Error: {}", error); 127 println!(" Error: {}", error);
128 } 128 }
129 129
130 println!(" Duration: {:?}", result.duration); 130 println!(" Duration: {:?}", result.duration);
131 println!(); 131 println!();
132 } 132 }
133 133
134 println!("Results: {}/{} passed ({:.1}%)", 134 println!(
135 passed, 135 "Results: {}/{} passed ({:.1}%)",
136 passed,
136 total, 137 total,
137 (passed as f64 / total as f64) * 100.0 138 (passed as f64 / total as f64) * 100.0
138 ); 139 );
139 println!(); 140 println!();
140 } 141 }
141 142
142 /// Get a summary string 143 /// Get a summary string
143 pub fn summary(&self) -> String { 144 pub fn summary(&self) -> String {
144 format!( 145 format!(
@@ -153,34 +154,34 @@ impl AuditResult {
153#[cfg(test)] 154#[cfg(test)]
154mod tests { 155mod tests {
155 use super::*; 156 use super::*;
156 157
157 #[tokio::test] 158 #[tokio::test]
158 async fn test_result_pass() { 159 async fn test_result_pass() {
159 let result = TestResult::new("test", "SPEC:1", "Must work") 160 let result = TestResult::new("test", "SPEC:1", "Must work")
160 .run(|| async { Ok(()) }) 161 .run(|| async { Ok(()) })
161 .await; 162 .await;
162 163
163 assert!(result.passed); 164 assert!(result.passed);
164 assert!(result.error.is_none()); 165 assert!(result.error.is_none());
165 } 166 }
166 167
167 #[tokio::test] 168 #[tokio::test]
168 async fn test_result_fail() { 169 async fn test_result_fail() {
169 let result = TestResult::new("test", "SPEC:1", "Must work") 170 let result = TestResult::new("test", "SPEC:1", "Must work")
170 .run(|| async { Err("Failed".to_string()) }) 171 .run(|| async { Err("Failed".to_string()) })
171 .await; 172 .await;
172 173
173 assert!(!result.passed); 174 assert!(!result.passed);
174 assert_eq!(result.error, Some("Failed".to_string())); 175 assert_eq!(result.error, Some("Failed".to_string()));
175 } 176 }
176 177
177 #[test] 178 #[test]
178 fn test_audit_result() { 179 fn test_audit_result() {
179 let mut audit = AuditResult::new("Test Spec"); 180 let mut audit = AuditResult::new("Test Spec");
180 181
181 audit.add(TestResult::new("test1", "SPEC:1", "Req1").pass()); 182 audit.add(TestResult::new("test1", "SPEC:1", "Req1").pass());
182 audit.add(TestResult::new("test2", "SPEC:2", "Req2").fail("Error")); 183 audit.add(TestResult::new("test2", "SPEC:2", "Req2").fail("Error"));
183 184
184 assert_eq!(audit.total_count(), 2); 185 assert_eq!(audit.total_count(), 2);
185 assert_eq!(audit.passed_count(), 1); 186 assert_eq!(audit.passed_count(), 1);
186 assert_eq!(audit.failed_count(), 1); 187 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 @@
88//! - Forward reference tests verify out-of-order event acceptance 88//! - Forward reference tests verify out-of-order event acceptance
89//! - Transitive tests verify multi-hop acceptance chains 89//! - Transitive tests verify multi-hop acceptance chains
90 90
91use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult};
91use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp}; 92use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp};
92use crate::{AuditClient, AuditResult, TestResult, TestContext, FixtureKind};
93use std::time::Duration; 93use std::time::Duration;
94 94
95/// Test suite for GRASP-01 event acceptance policy 95/// Test suite for GRASP-01 event acceptance policy
@@ -99,42 +99,42 @@ impl EventAcceptancePolicyTests {
99 /// Run all event acceptance policy tests 99 /// Run all event acceptance policy tests
100 pub async fn run_all(client: &AuditClient) -> AuditResult { 100 pub async fn run_all(client: &AuditClient) -> AuditResult {
101 let mut results = AuditResult::new("GRASP-01 Nostr Event Acceptance Policy Tests"); 101 let mut results = AuditResult::new("GRASP-01 Nostr Event Acceptance Policy Tests");
102 102
103 // Repository Announcement Acceptance Tests 103 // Repository Announcement Acceptance Tests
104 results.add(Self::test_accept_valid_repo_announcement(client).await); 104 results.add(Self::test_accept_valid_repo_announcement(client).await);
105 results.add(Self::test_reject_repo_announcement_missing_clone_tag(client).await); 105 results.add(Self::test_reject_repo_announcement_missing_clone_tag(client).await);
106 results.add(Self::test_reject_repo_announcement_missing_relays_tag(client).await); 106 results.add(Self::test_reject_repo_announcement_missing_relays_tag(client).await);
107 107
108 // Repository State Announcement Tests 108 // Repository State Announcement Tests
109 results.add(Self::test_accept_valid_repo_state_announcement(client).await); 109 results.add(Self::test_accept_valid_repo_state_announcement(client).await);
110 110
111 // Group 1: Accept Events Tagging Accepted Repositories 111 // Group 1: Accept Events Tagging Accepted Repositories
112 results.add(Self::test_accept_issue_via_a_tag(client).await); 112 results.add(Self::test_accept_issue_via_a_tag(client).await);
113 results.add(Self::test_accept_comment_via_A_tag(client).await); 113 results.add(Self::test_accept_comment_via_capital_a_tag(client).await);
114 results.add(Self::test_accept_kind1_via_q_tag(client).await); 114 results.add(Self::test_accept_kind1_via_q_tag(client).await);
115 115
116 // Group 2: Accept Events Tagging Accepted Events (Transitive) 116 // Group 2: Accept Events Tagging Accepted Events (Transitive)
117 results.add(Self::test_accept_issue_quoting_issue_via_q(client).await); 117 results.add(Self::test_accept_issue_quoting_issue_via_q(client).await);
118 results.add(Self::test_accept_comment_via_E_tag(client).await); 118 results.add(Self::test_accept_comment_via_capital_e_tag(client).await);
119 results.add(Self::test_accept_kind1_via_e_tag(client).await); 119 results.add(Self::test_accept_kind1_via_e_tag(client).await);
120 120
121 // Group 3: Accept Events Tagged BY Accepted Events (Forward Refs) 121 // Group 3: Accept Events Tagged BY Accepted Events (Forward Refs)
122 results.add(Self::test_accept_kind1_referenced_in_issue(client).await); 122 results.add(Self::test_accept_kind1_referenced_in_issue(client).await);
123 results.add(Self::test_accept_comment_referenced_in_comment(client).await); 123 results.add(Self::test_accept_comment_referenced_in_comment(client).await);
124 results.add(Self::test_accept_kind1_referenced_in_kind1(client).await); 124 results.add(Self::test_accept_kind1_referenced_in_kind1(client).await);
125 125
126 // Group 4: Reject Unrelated Events 126 // Group 4: Reject Unrelated Events
127 results.add(Self::test_reject_orphan_issue(client).await); 127 results.add(Self::test_reject_orphan_issue(client).await);
128 results.add(Self::test_reject_orphan_kind1(client).await); 128 results.add(Self::test_reject_orphan_kind1(client).await);
129 results.add(Self::test_reject_comment_quoting_other_repo(client).await); 129 results.add(Self::test_reject_comment_quoting_other_repo(client).await);
130 130
131 results 131 results
132 } 132 }
133 133
134 // ============================================================ 134 // ============================================================
135 // Repository Announcement Acceptance Tests 135 // Repository Announcement Acceptance Tests
136 // ============================================================ 136 // ============================================================
137 137
138 /// Test: Accept valid repository announcements 138 /// Test: Accept valid repository announcements
139 /// 139 ///
140 /// Spec: Lines 3-5 of ../grasp/01.md 140 /// Spec: Lines 3-5 of ../grasp/01.md
@@ -152,41 +152,52 @@ impl EventAcceptancePolicyTests {
152 .run(|| async { 152 .run(|| async {
153 // Create TestContext for mode-aware fixture management 153 // Create TestContext for mode-aware fixture management
154 let ctx = TestContext::new(client); 154 let ctx = TestContext::new(client);
155 155
156 // Request repository fixture - behavior depends on mode 156 // Request repository fixture - behavior depends on mode
157 let event = ctx.get_fixture(FixtureKind::ValidRepo).await 157 let event = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| {
158 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; 158 format!(
159 159 "Test setup failed: could not get valid repository fixture: {}",
160 e
161 )
162 })?;
163
160 // Get relay URL for validation 164 // Get relay URL for validation
161 let relay_url = client.client().relays().await 165 let relay_url = client
166 .client()
167 .relays()
168 .await
162 .keys() 169 .keys()
163 .next() 170 .next()
164 .ok_or("No relay connected")? 171 .ok_or("No relay connected")?
165 .to_string(); 172 .to_string();
166 173
167 // Convert WebSocket URL to HTTP URL for validation 174 // Convert WebSocket URL to HTTP URL for validation
168 let http_url = relay_url 175 let http_url = relay_url
169 .replace("ws://", "http://") 176 .replace("ws://", "http://")
170 .replace("wss://", "https://"); 177 .replace("wss://", "https://");
171 178
172 // Extract repo_id from the event's d tag 179 // Extract repo_id from the event's d tag
173 let repo_id = event.tags.iter() 180 let repo_id = event
181 .tags
182 .iter()
174 .find(|t| t.kind() == TagKind::d()) 183 .find(|t| t.kind() == TagKind::d())
175 .and_then(|t| t.content()) 184 .and_then(|t| t.content())
176 .ok_or("Missing d tag in announcement")? 185 .ok_or("Missing d tag in announcement")?
177 .to_string(); 186 .to_string();
178 187
179 let event_id = event.id; 188 let event_id = event.id;
180 189
181 // Query back to verify it was accepted and stored 190 // Query back to verify it was accepted and stored
182 let filter = Filter::new() 191 let filter = Filter::new()
183 .kind(Kind::GitRepoAnnouncement) 192 .kind(Kind::GitRepoAnnouncement)
184 .author(client.public_key()) 193 .author(client.public_key())
185 .identifier(&repo_id); 194 .identifier(&repo_id);
186 195
187 let events = client.query(filter).await 196 let events = client
197 .query(filter)
198 .await
188 .map_err(|e| format!("Failed to query events from relay: {}", e))?; 199 .map_err(|e| format!("Failed to query events from relay: {}", e))?;
189 200
190 // Verify we got the event back 201 // Verify we got the event back
191 if events.is_empty() { 202 if events.is_empty() {
192 return Err(format!( 203 return Err(format!(
@@ -194,41 +205,43 @@ impl EventAcceptancePolicyTests {
194 event_id, repo_id 205 event_id, repo_id
195 )); 206 ));
196 } 207 }
197 208
198 // Verify it's the same event 209 // Verify it's the same event
199 let stored_event = events.iter() 210 let stored_event = events.iter().find(|e| e.id == event_id).ok_or(format!(
200 .find(|e| e.id == event_id) 211 "Stored event ID doesn't match sent event. Expected: {}, Got {} events",
201 .ok_or(format!( 212 event_id,
202 "Stored event ID doesn't match sent event. Expected: {}, Got {} events", 213 events.len()
203 event_id, events.len() 214 ))?;
204 ))?; 215
205
206 // Verify key tags are present 216 // Verify key tags are present
207 let has_clone_tag = stored_event.tags.iter() 217 let has_clone_tag = stored_event.tags.iter().any(|t| {
208 .any(|t| { 218 t.kind() == TagKind::Custom("clone".into())
209 t.kind() == TagKind::Custom("clone".into()) && 219 && t.content().map(|c| c.contains(&http_url)).unwrap_or(false)
210 t.content().map(|c| c.contains(&http_url)).unwrap_or(false) 220 });
211 }); 221
212 222 let has_relays_tag = stored_event.tags.iter().any(|t| {
213 let has_relays_tag = stored_event.tags.iter() 223 t.kind() == TagKind::Custom("relays".into()) && t.content() == Some(&relay_url)
214 .any(|t| { 224 });
215 t.kind() == TagKind::Custom("relays".into()) && 225
216 t.content() == Some(&relay_url)
217 });
218
219 if !has_clone_tag { 226 if !has_clone_tag {
220 return Err(format!("Stored event missing clone tag with service URL ({})", http_url)); 227 return Err(format!(
228 "Stored event missing clone tag with service URL ({})",
229 http_url
230 ));
221 } 231 }
222 232
223 if !has_relays_tag { 233 if !has_relays_tag {
224 return Err(format!("Stored event missing relays tag with service URL ({})", relay_url)); 234 return Err(format!(
235 "Stored event missing relays tag with service URL ({})",
236 relay_url
237 ));
225 } 238 }
226 239
227 Ok(()) 240 Ok(())
228 }) 241 })
229 .await 242 .await
230 } 243 }
231 244
232 /// Test: Reject repo announcements not listing service in clone tag 245 /// Test: Reject repo announcements not listing service in clone tag
233 /// 246 ///
234 /// Spec: Line 5 of ../grasp/01.md 247 /// Spec: Line 5 of ../grasp/01.md
@@ -241,39 +254,54 @@ impl EventAcceptancePolicyTests {
241 ) 254 )
242 .run(|| async { 255 .run(|| async {
243 // Get relay URL from client 256 // Get relay URL from client
244 let relay_url = client.client().relays().await 257 let relay_url = client
258 .client()
259 .relays()
260 .await
245 .keys() 261 .keys()
246 .next() 262 .next()
247 .ok_or("No relay connected - client has no active relay connections")? 263 .ok_or("No relay connected - client has no active relay connections")?
248 .to_string(); 264 .to_string();
249 265
250 // Create unique repository identifier 266 // Create unique repository identifier
251 let timestamp = Timestamp::now().as_u64(); 267 let timestamp = Timestamp::now().as_u64();
252 let repo_id = format!("test-repo-no-clone-{}", timestamp); 268 let repo_id = format!("test-repo-no-clone-{}", timestamp);
253 269
254 // Create repo announcement WITHOUT service in clone tag 270 // Create repo announcement WITHOUT service in clone tag
255 let event = client.event_builder(Kind::GitRepoAnnouncement, "") 271 let event = client
272 .event_builder(Kind::GitRepoAnnouncement, "")
256 .tag(Tag::identifier(&repo_id)) 273 .tag(Tag::identifier(&repo_id))
257 .tag(Tag::custom(TagKind::Custom("name".into()), vec!["Test Repo No Clone"])) 274 .tag(Tag::custom(
258 .tag(Tag::custom(TagKind::Custom("clone".into()), vec!["https://github.com/user/repo.git"])) // NOT this service 275 TagKind::Custom("name".into()),
259 .tag(Tag::custom(TagKind::Custom("relays".into()), vec![relay_url.clone()])) // Correct relay 276 vec!["Test Repo No Clone"],
277 ))
278 .tag(Tag::custom(
279 TagKind::Custom("clone".into()),
280 vec!["https://github.com/user/repo.git"],
281 )) // NOT this service
282 .tag(Tag::custom(
283 TagKind::Custom("relays".into()),
284 vec![relay_url.clone()],
285 )) // Correct relay
260 .build(client.keys()) 286 .build(client.keys())
261 .map_err(|e| format!("Failed to build event: {}", e))?; 287 .map_err(|e| format!("Failed to build event: {}", e))?;
262 288
263 let event_id = event.id; 289 let event_id = event.id;
264 290
265 // Send event - expect rejection 291 // Send event - expect rejection
266 let send_result = client.send_event(event.clone()).await; 292 let _send_result = client.send_event(event.clone()).await;
267 293
268 // Query to verify event is NOT stored 294 // Query to verify event is NOT stored
269 let filter = Filter::new() 295 let filter = Filter::new()
270 .kind(Kind::GitRepoAnnouncement) 296 .kind(Kind::GitRepoAnnouncement)
271 .author(client.public_key()) 297 .author(client.public_key())
272 .identifier(&repo_id); 298 .identifier(&repo_id);
273 299
274 let events = client.query(filter).await 300 let events = client
301 .query(filter)
302 .await
275 .map_err(|e| format!("Failed to query events from relay: {}", e))?; 303 .map_err(|e| format!("Failed to query events from relay: {}", e))?;
276 304
277 // Verify event was rejected (not stored) 305 // Verify event was rejected (not stored)
278 if events.iter().any(|e| e.id == event_id) { 306 if events.iter().any(|e| e.id == event_id) {
279 return Err(format!( 307 return Err(format!(
@@ -282,12 +310,12 @@ impl EventAcceptancePolicyTests {
282 event_id, relay_url 310 event_id, relay_url
283 )); 311 ));
284 } 312 }
285 313
286 Ok(()) 314 Ok(())
287 }) 315 })
288 .await 316 .await
289 } 317 }
290 318
291 /// Test: Reject repo announcements not listing service in relays tag 319 /// Test: Reject repo announcements not listing service in relays tag
292 /// 320 ///
293 /// Spec: Line 5 of ../grasp/01.md 321 /// Spec: Line 5 of ../grasp/01.md
@@ -300,44 +328,63 @@ impl EventAcceptancePolicyTests {
300 ) 328 )
301 .run(|| async { 329 .run(|| async {
302 // Get relay URL from client 330 // Get relay URL from client
303 let relay_url = client.client().relays().await 331 let relay_url = client
332 .client()
333 .relays()
334 .await
304 .keys() 335 .keys()
305 .next() 336 .next()
306 .ok_or("No relay connected - client has no active relay connections")? 337 .ok_or("No relay connected - client has no active relay connections")?
307 .to_string(); 338 .to_string();
308 339
309 // Convert WebSocket URL to HTTP URL for clone tag 340 // Convert WebSocket URL to HTTP URL for clone tag
310 let http_url = relay_url 341 let http_url = relay_url
311 .replace("ws://", "http://") 342 .replace("ws://", "http://")
312 .replace("wss://", "https://"); 343 .replace("wss://", "https://");
313 344
314 // Create unique repository identifier 345 // Create unique repository identifier
315 let timestamp = Timestamp::now().as_u64(); 346 let timestamp = Timestamp::now().as_u64();
316 let repo_id = format!("test-repo-no-relays-{}", timestamp); 347 let repo_id = format!("test-repo-no-relays-{}", timestamp);
317 348
318 // Create repo announcement WITHOUT service in relays tag 349 // Create repo announcement WITHOUT service in relays tag
319 let event = client.event_builder(Kind::GitRepoAnnouncement, "") 350 let event = client
351 .event_builder(Kind::GitRepoAnnouncement, "")
320 .tag(Tag::identifier(&repo_id)) 352 .tag(Tag::identifier(&repo_id))
321 .tag(Tag::custom(TagKind::custom("name"), vec!["Test Repo No Relays"])) 353 .tag(Tag::custom(
322 .tag(Tag::custom(TagKind::custom("clone"), vec![format!("{}/{}/test-repo.git", http_url, client.public_key())])) // Correct clone 354 TagKind::custom("name"),
323 .tag(Tag::custom(TagKind::custom("relays"), vec!["wss://relay.damus.io"])) // NOT this service 355 vec!["Test Repo No Relays"],
356 ))
357 .tag(Tag::custom(
358 TagKind::custom("clone"),
359 vec![format!(
360 "{}/{}/test-repo.git",
361 http_url,
362 client.public_key()
363 )],
364 )) // Correct clone
365 .tag(Tag::custom(
366 TagKind::custom("relays"),
367 vec!["wss://relay.damus.io"],
368 )) // NOT this service
324 .build(client.keys()) 369 .build(client.keys())
325 .map_err(|e| format!("Failed to build event: {}", e))?; 370 .map_err(|e| format!("Failed to build event: {}", e))?;
326 371
327 let event_id = event.id; 372 let event_id = event.id;
328 373
329 // Send event - expect rejection 374 // Send event - expect rejection
330 let _send_result = client.send_event(event.clone()).await; 375 let _send_result = client.send_event(event.clone()).await;
331 376
332 // Query to verify event is NOT stored 377 // Query to verify event is NOT stored
333 let filter = Filter::new() 378 let filter = Filter::new()
334 .kind(Kind::GitRepoAnnouncement) 379 .kind(Kind::GitRepoAnnouncement)
335 .author(client.public_key()) 380 .author(client.public_key())
336 .identifier(&repo_id); 381 .identifier(&repo_id);
337 382
338 let events = client.query(filter).await 383 let events = client
384 .query(filter)
385 .await
339 .map_err(|e| format!("Failed to query events from relay: {}", e))?; 386 .map_err(|e| format!("Failed to query events from relay: {}", e))?;
340 387
341 // Verify event was rejected (not stored) 388 // Verify event was rejected (not stored)
342 if events.iter().any(|e| e.id == event_id) { 389 if events.iter().any(|e| e.id == event_id) {
343 return Err(format!( 390 return Err(format!(
@@ -346,16 +393,16 @@ impl EventAcceptancePolicyTests {
346 event_id, relay_url 393 event_id, relay_url
347 )); 394 ));
348 } 395 }
349 396
350 Ok(()) 397 Ok(())
351 }) 398 })
352 .await 399 .await
353 } 400 }
354 401
355 // ============================================================ 402 // ============================================================
356 // Repository State Announcement Tests 403 // Repository State Announcement Tests
357 // ============================================================ 404 // ============================================================
358 405
359 /// Test: Accept valid repository state announcements 406 /// Test: Accept valid repository state announcements
360 /// 407 ///
361 /// Spec: Lines 6-7 of ../grasp/01.md 408 /// Spec: Lines 6-7 of ../grasp/01.md
@@ -374,31 +421,39 @@ impl EventAcceptancePolicyTests {
374 .run(|| async { 421 .run(|| async {
375 // NEW: Create TestContext for mode-aware fixture management 422 // NEW: Create TestContext for mode-aware fixture management
376 let ctx = TestContext::new(client); 423 let ctx = TestContext::new(client);
377 424
378 // NEW: Request repository fixture - behavior depends on mode 425 // NEW: Request repository fixture - behavior depends on mode
379 // CI mode: Creates fresh repo for this test 426 // CI mode: Creates fresh repo for this test
380 // Production mode: Returns cached repo if available 427 // Production mode: Returns cached repo if available
381 let repo_event = ctx.get_fixture(FixtureKind::RepoState).await 428 let repo_event = ctx.get_fixture(FixtureKind::RepoState).await.map_err(|e| {
382 .map_err(|e| format!("Test setup failed: could not get repository state fixture: {}", e))?; 429 format!(
383 430 "Test setup failed: could not get repository state fixture: {}",
431 e
432 )
433 })?;
434
384 // Extract repo_id from the repository announcement 435 // Extract repo_id from the repository announcement
385 let repo_id = repo_event.tags.iter() 436 let repo_id = repo_event
437 .tags
438 .iter()
386 .find(|t| t.kind() == TagKind::d()) 439 .find(|t| t.kind() == TagKind::d())
387 .and_then(|t| t.content()) 440 .and_then(|t| t.content())
388 .ok_or("Missing d tag in repository announcement")? 441 .ok_or("Missing d tag in repository announcement")?
389 .to_string(); 442 .to_string();
390 443
391 let event_id = repo_event.id; 444 let event_id = repo_event.id;
392 445
393 // Query back to verify it was accepted and stored 446 // Query back to verify it was accepted and stored
394 let filter = Filter::new() 447 let filter = Filter::new()
395 .kind(Kind::Custom(30618)) 448 .kind(Kind::Custom(30618))
396 .author(client.public_key()) 449 .author(client.public_key())
397 .identifier(&repo_id); 450 .identifier(&repo_id);
398 451
399 let events = client.query(filter).await 452 let events = client
453 .query(filter)
454 .await
400 .map_err(|e| format!("Failed to query events from relay: {}", e))?; 455 .map_err(|e| format!("Failed to query events from relay: {}", e))?;
401 456
402 // Verify we got the event back 457 // Verify we got the event back
403 if events.is_empty() { 458 if events.is_empty() {
404 return Err(format!( 459 return Err(format!(
@@ -406,16 +461,16 @@ impl EventAcceptancePolicyTests {
406 event_id, repo_id 461 event_id, repo_id
407 )); 462 ));
408 } 463 }
409 464
410 Ok(()) 465 Ok(())
411 }) 466 })
412 .await 467 .await
413 } 468 }
414 469
415 // ============================================================ 470 // ============================================================
416 // Helper Functions (6 total) 471 // Helper Functions (6 total)
417 // ============================================================ 472 // ============================================================
418 473
419 /// Extract the `d` tag value from an event 474 /// Extract the `d` tag value from an event
420 fn extract_d_tag(event: &Event) -> Option<String> { 475 fn extract_d_tag(event: &Event) -> Option<String> {
421 for tag in event.tags.iter() { 476 for tag in event.tags.iter() {
@@ -426,15 +481,16 @@ impl EventAcceptancePolicyTests {
426 } 481 }
427 None 482 None
428 } 483 }
429 484
430 /// Create a basic repository announcement (kind 30617) 485 /// Create a basic repository announcement (kind 30617)
431 /// Uses the client's create_repo_announcement helper which includes required clone and relays tags 486 /// Uses the client's create_repo_announcement helper which includes required clone and relays tags
432 async fn create_test_repo(client: &AuditClient, repo_id: &str) -> Result<Event, String> { 487 async fn create_test_repo(client: &AuditClient, repo_id: &str) -> Result<Event, String> {
433 client.create_repo_announcement(repo_id) 488 client
489 .create_repo_announcement(repo_id)
434 .await 490 .await
435 .map_err(|e| format!("Test setup failed: could not create test repository: {}", e)) 491 .map_err(|e| format!("Test setup failed: could not create test repository: {}", e))
436 } 492 }
437 493
438 /// Create an issue (kind 1621) that references a repository 494 /// Create an issue (kind 1621) that references a repository
439 /// Uses AuditClient::create_issue helper method 495 /// Uses AuditClient::create_issue helper method
440 fn create_issue_for_repo( 496 fn create_issue_for_repo(
@@ -442,10 +498,11 @@ impl EventAcceptancePolicyTests {
442 repo_event: &Event, 498 repo_event: &Event,
443 issue_title: &str, 499 issue_title: &str,
444 ) -> Result<Event, String> { 500 ) -> Result<Event, String> {
445 client.create_issue(repo_event, issue_title, "issue content", vec![]) 501 client
502 .create_issue(repo_event, issue_title, "issue content", vec![])
446 .map_err(|e| format!("Test setup failed: could not create test issue: {}", e)) 503 .map_err(|e| format!("Test setup failed: could not create test issue: {}", e))
447 } 504 }
448 505
449 /// Create a NIP-22 comment (kind 1111) for an event 506 /// Create a NIP-22 comment (kind 1111) for an event
450 /// Uses AuditClient::create_comment helper method 507 /// Uses AuditClient::create_comment helper method
451 fn create_comment_for_event( 508 fn create_comment_for_event(
@@ -453,10 +510,11 @@ impl EventAcceptancePolicyTests {
453 event: &Event, 510 event: &Event,
454 content: &str, 511 content: &str,
455 ) -> Result<Event, String> { 512 ) -> Result<Event, String> {
456 client.create_comment(event, content, vec![]) 513 client
514 .create_comment(event, content, vec![])
457 .map_err(|e| format!("Test setup failed: could not create test comment: {}", e)) 515 .map_err(|e| format!("Test setup failed: could not create test comment: {}", e))
458 } 516 }
459 517
460 /// Send event and verify it was accepted (stored by relay) 518 /// Send event and verify it was accepted (stored by relay)
461 async fn send_and_verify_accepted( 519 async fn send_and_verify_accepted(
462 client: &AuditClient, 520 client: &AuditClient,
@@ -464,23 +522,27 @@ impl EventAcceptancePolicyTests {
464 description: &str, 522 description: &str,
465 ) -> Result<(), String> { 523 ) -> Result<(), String> {
466 let event_id = event.id; 524 let event_id = event.id;
467 525
468 client.send_event(event).await 526 client
527 .send_event(event)
528 .await
469 .map_err(|e| format!("Failed to send event to relay: {}", e))?; 529 .map_err(|e| format!("Failed to send event to relay: {}", e))?;
470 530
471 tokio::time::sleep(Duration::from_millis(100)).await; 531 tokio::time::sleep(Duration::from_millis(100)).await;
472 532
473 let filter = Filter::new().id(event_id); 533 let filter = Filter::new().id(event_id);
474 let events = client.query(filter).await 534 let events = client
535 .query(filter)
536 .await
475 .map_err(|e| format!("Failed to query relay for verification: {}", e))?; 537 .map_err(|e| format!("Failed to query relay for verification: {}", e))?;
476 538
477 if events.is_empty() { 539 if events.is_empty() {
478 return Err(format!("Event should be accepted: {}", description)); 540 return Err(format!("Event should be accepted: {}", description));
479 } 541 }
480 542
481 Ok(()) 543 Ok(())
482 } 544 }
483 545
484 /// Send event and verify it was rejected (NOT stored by relay) 546 /// Send event and verify it was rejected (NOT stored by relay)
485 async fn send_and_verify_rejected( 547 async fn send_and_verify_rejected(
486 client: &AuditClient, 548 client: &AuditClient,
@@ -488,27 +550,31 @@ impl EventAcceptancePolicyTests {
488 description: &str, 550 description: &str,
489 ) -> Result<(), String> { 551 ) -> Result<(), String> {
490 let event_id = event.id; 552 let event_id = event.id;
491 553
492 client.send_event(event).await 554 client
555 .send_event(event)
556 .await
493 .map_err(|e| format!("Failed to send event to relay: {}", e))?; 557 .map_err(|e| format!("Failed to send event to relay: {}", e))?;
494 558
495 tokio::time::sleep(Duration::from_millis(100)).await; 559 tokio::time::sleep(Duration::from_millis(100)).await;
496 560
497 let filter = Filter::new().id(event_id); 561 let filter = Filter::new().id(event_id);
498 let events = client.query(filter).await 562 let events = client
563 .query(filter)
564 .await
499 .map_err(|e| format!("Failed to query relay for verification: {}", e))?; 565 .map_err(|e| format!("Failed to query relay for verification: {}", e))?;
500 566
501 if !events.is_empty() { 567 if !events.is_empty() {
502 return Err(format!("Event should be rejected: {}", description)); 568 return Err(format!("Event should be rejected: {}", description));
503 } 569 }
504 570
505 Ok(()) 571 Ok(())
506 } 572 }
507 573
508 // ============================================================ 574 // ============================================================
509 // Group 1: Accept Events Tagging Accepted Repositories (3 tests) 575 // Group 1: Accept Events Tagging Accepted Repositories (3 tests)
510 // ============================================================ 576 // ============================================================
511 577
512 /// Test 1.1: Issue referencing repo via `a` tag should be accepted 578 /// Test 1.1: Issue referencing repo via `a` tag should be accepted
513 /// 579 ///
514 /// **EXAMPLE: Using TestContext for prerequisite events** 580 /// **EXAMPLE: Using TestContext for prerequisite events**
@@ -522,28 +588,33 @@ impl EventAcceptancePolicyTests {
522 .run(|| async { 588 .run(|| async {
523 // NEW: Create TestContext 589 // NEW: Create TestContext
524 let ctx = TestContext::new(client); 590 let ctx = TestContext::new(client);
525 591
526 // NEW: Get repository fixture (mode-aware) 592 // NEW: Get repository fixture (mode-aware)
527 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await 593 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| {
528 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; 594 format!(
529 595 "Test setup failed: could not get valid repository fixture: {}",
596 e
597 )
598 })?;
599
530 // 2. Create issue that references the repo 600 // 2. Create issue that references the repo
531 let issue = Self::create_issue_for_repo(client, &repo, "Test Issue 1")?; 601 let issue = Self::create_issue_for_repo(client, &repo, "Test Issue 1")?;
532 602
533 // 3. Send issue and verify it's accepted 603 // 3. Send issue and verify it's accepted
534 Self::send_and_verify_accepted(client, issue, "issue referencing repo via 'a' tag").await?; 604 Self::send_and_verify_accepted(client, issue, "issue referencing repo via 'a' tag")
535 605 .await?;
606
536 Ok(()) 607 Ok(())
537 }) 608 })
538 .await 609 .await
539 } 610 }
540 611
541 /// Test 1.2: NIP-22 comment with root `A` tag referencing repo should be accepted 612 /// Test 1.2: NIP-22 comment with root `A` tag referencing repo should be accepted
542 /// 613 ///
543 /// **Using TestContext pattern:** 614 /// **Using TestContext pattern:**
544 /// - In CI mode: Creates fresh repo for full isolation 615 /// - In CI mode: Creates fresh repo for full isolation
545 /// - In Production mode: Reuses cached repo to minimize events 616 /// - In Production mode: Reuses cached repo to minimize events
546 async fn test_accept_comment_via_A_tag(client: &AuditClient) -> TestResult { 617 async fn test_accept_comment_via_capital_a_tag(client: &AuditClient) -> TestResult {
547 TestResult::new( 618 TestResult::new(
548 "accept_comment_via_A_tag", 619 "accept_comment_via_A_tag",
549 "GRASP-01:event-acceptance:1.2", 620 "GRASP-01:event-acceptance:1.2",
@@ -552,37 +623,44 @@ impl EventAcceptancePolicyTests {
552 .run(|| async { 623 .run(|| async {
553 // Create TestContext 624 // Create TestContext
554 let ctx = TestContext::new(client); 625 let ctx = TestContext::new(client);
555 626
556 // Get repository fixture (mode-aware) 627 // Get repository fixture (mode-aware)
557 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await 628 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| {
558 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; 629 format!(
559 630 "Test setup failed: could not get valid repository fixture: {}",
631 e
632 )
633 })?;
634
560 // Extract repo_id and create `A` tag manually 635 // Extract repo_id and create `A` tag manually
561 let repo_id = Self::extract_d_tag(&repo) 636 let repo_id =
562 .ok_or("Failed to extract repo_id from repo event")?; 637 Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id from repo event")?;
563 let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); 638 let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id);
564 639
565 // Create comment with `A` tag (root reference to repo) 640 // Create comment with `A` tag (root reference to repo)
566 let tags = vec![ 641 let tags = vec![
567 Tag::custom(TagKind::custom("A"), vec![a_tag_value.clone(), "".to_string(), "root".to_string()]), 642 Tag::custom(
643 TagKind::custom("A"),
644 vec![a_tag_value.clone(), "".to_string(), "root".to_string()],
645 ),
568 Tag::custom(TagKind::custom("K"), vec!["30617".to_string()]), 646 Tag::custom(TagKind::custom("K"), vec!["30617".to_string()]),
569 Tag::public_key(repo.pubkey), 647 Tag::public_key(repo.pubkey),
570 ]; 648 ];
571 649
572 let comment = client 650 let comment = client
573 .event_builder(Kind::Custom(1111), "Comment on repo") 651 .event_builder(Kind::Custom(1111), "Comment on repo")
574 .tags(tags) 652 .tags(tags)
575 .build(client.keys()) 653 .build(client.keys())
576 .map_err(|e| format!("Failed to build comment: {}", e))?; 654 .map_err(|e| format!("Failed to build comment: {}", e))?;
577 655
578 // Send comment and verify it's accepted 656 // Send comment and verify it's accepted
579 Self::send_and_verify_accepted(client, comment, "comment with 'A' tag to repo").await?; 657 Self::send_and_verify_accepted(client, comment, "comment with 'A' tag to repo").await?;
580 658
581 Ok(()) 659 Ok(())
582 }) 660 })
583 .await 661 .await
584 } 662 }
585 663
586 /// Test 1.3: Kind 1 text note quoting repo via `q` tag should be accepted 664 /// Test 1.3: Kind 1 text note quoting repo via `q` tag should be accepted
587 /// 665 ///
588 /// **Using TestContext pattern:** 666 /// **Using TestContext pattern:**
@@ -597,39 +675,41 @@ impl EventAcceptancePolicyTests {
597 .run(|| async { 675 .run(|| async {
598 // Create TestContext 676 // Create TestContext
599 let ctx = TestContext::new(client); 677 let ctx = TestContext::new(client);
600 678
601 // Get repository fixture (mode-aware) 679 // Get repository fixture (mode-aware)
602 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await 680 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| {
603 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; 681 format!(
604 682 "Test setup failed: could not get valid repository fixture: {}",
683 e
684 )
685 })?;
686
605 // Extract repo_id and create `q` tag 687 // Extract repo_id and create `q` tag
606 let repo_id = Self::extract_d_tag(&repo) 688 let repo_id =
607 .ok_or("Failed to extract repo_id from repo event")?; 689 Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id from repo event")?;
608 let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); 690 let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id);
609 691
610 // Create kind 1 note with `q` tag (quote reference to repo) 692 // Create kind 1 note with `q` tag (quote reference to repo)
611 let tags = vec![ 693 let tags = vec![Tag::custom(TagKind::custom("q"), vec![a_tag_value])];
612 Tag::custom(TagKind::custom("q"), vec![a_tag_value]), 694
613 ];
614
615 let note = client 695 let note = client
616 .event_builder(Kind::TextNote, "Mentioning this repo") 696 .event_builder(Kind::TextNote, "Mentioning this repo")
617 .tags(tags) 697 .tags(tags)
618 .build(client.keys()) 698 .build(client.keys())
619 .map_err(|e| format!("Failed to build note: {}", e))?; 699 .map_err(|e| format!("Failed to build note: {}", e))?;
620 700
621 // Send note and verify it's accepted 701 // Send note and verify it's accepted
622 Self::send_and_verify_accepted(client, note, "kind 1 with 'q' tag to repo").await?; 702 Self::send_and_verify_accepted(client, note, "kind 1 with 'q' tag to repo").await?;
623 703
624 Ok(()) 704 Ok(())
625 }) 705 })
626 .await 706 .await
627 } 707 }
628 708
629 // ============================================================ 709 // ============================================================
630 // Group 2: Accept Events Tagging Accepted Events (3 tests) 710 // Group 2: Accept Events Tagging Accepted Events (3 tests)
631 // ============================================================ 711 // ============================================================
632 712
633 /// Test 2.1: Issue quoting another accepted issue should be accepted (transitive) 713 /// Test 2.1: Issue quoting another accepted issue should be accepted (transitive)
634 /// 714 ///
635 /// **Using TestContext pattern:** 715 /// **Using TestContext pattern:**
@@ -644,37 +724,44 @@ impl EventAcceptancePolicyTests {
644 .run(|| async { 724 .run(|| async {
645 // Create TestContext 725 // Create TestContext
646 let ctx = TestContext::new(client); 726 let ctx = TestContext::new(client);
647 727
648 // Get repo with issue fixture (mode-aware) - returns the issue event 728 // Get repo with issue fixture (mode-aware) - returns the issue event
649 let issue_a = ctx.get_fixture(FixtureKind::RepoWithIssue).await 729 let issue_a = ctx
650 .map_err(|e| format!("Test setup failed: could not get repo with issue fixture: {}", e))?; 730 .get_fixture(FixtureKind::RepoWithIssue)
651 731 .await
732 .map_err(|e| {
733 format!(
734 "Test setup failed: could not get repo with issue fixture: {}",
735 e
736 )
737 })?;
738
652 // Create Repo B but DON'T send it (unaccepted) - just for creating Issue B 739 // Create Repo B but DON'T send it (unaccepted) - just for creating Issue B
653 let repo_b = Self::create_test_repo(client, "repo-b").await?; 740 let repo_b = Self::create_test_repo(client, "repo-b").await?;
654 741
655 // Create Issue B that quotes accepted Issue A via 'q' tag (should make it accepted) 742 // Create Issue B that quotes accepted Issue A via 'q' tag (should make it accepted)
656 let additional_tags = vec![ 743 let additional_tags =
657 Tag::custom(TagKind::custom("q"), vec![issue_a.id.to_hex()]), 744 vec![Tag::custom(TagKind::custom("q"), vec![issue_a.id.to_hex()])];
658 ]; 745
659
660 let issue_b = client 746 let issue_b = client
661 .create_issue(&repo_b, "Issue B", "issue content", additional_tags) 747 .create_issue(&repo_b, "Issue B", "issue content", additional_tags)
662 .map_err(|e| format!("Failed to build issue B: {}", e))?; 748 .map_err(|e| format!("Failed to build issue B: {}", e))?;
663 749
664 // Send Issue B and verify it's ACCEPTED (via transitive quote to Issue A) 750 // Send Issue B and verify it's ACCEPTED (via transitive quote to Issue A)
665 Self::send_and_verify_accepted(client, issue_b, "issue B quoting accepted issue A").await?; 751 Self::send_and_verify_accepted(client, issue_b, "issue B quoting accepted issue A")
666 752 .await?;
753
667 Ok(()) 754 Ok(())
668 }) 755 })
669 .await 756 .await
670 } 757 }
671 758
672 /// Test 2.2: NIP-22 comment with root 'E' tag to accepted issue should be accepted 759 /// Test 2.2: NIP-22 comment with root 'E' tag to accepted issue should be accepted
673 /// 760 ///
674 /// **Using TestContext pattern:** 761 /// **Using TestContext pattern:**
675 /// - In CI mode: Creates fresh repo+issue for full isolation 762 /// - In CI mode: Creates fresh repo+issue for full isolation
676 /// - In Production mode: Reuses cached repo+issue to minimize events 763 /// - In Production mode: Reuses cached repo+issue to minimize events
677 async fn test_accept_comment_via_E_tag(client: &AuditClient) -> TestResult { 764 async fn test_accept_comment_via_capital_e_tag(client: &AuditClient) -> TestResult {
678 TestResult::new( 765 TestResult::new(
679 "accept_comment_via_E_tag", 766 "accept_comment_via_E_tag",
680 "GRASP-01:event-acceptance:2.2", 767 "GRASP-01:event-acceptance:2.2",
@@ -683,22 +770,30 @@ impl EventAcceptancePolicyTests {
683 .run(|| async { 770 .run(|| async {
684 // Create TestContext 771 // Create TestContext
685 let ctx = TestContext::new(client); 772 let ctx = TestContext::new(client);
686 773
687 // Get repo with issue fixture (mode-aware) - returns the issue event 774 // Get repo with issue fixture (mode-aware) - returns the issue event
688 let issue = ctx.get_fixture(FixtureKind::RepoWithIssue).await 775 let issue = ctx
689 .map_err(|e| format!("Test setup failed: could not get repo with issue fixture: {}", e))?; 776 .get_fixture(FixtureKind::RepoWithIssue)
690 777 .await
778 .map_err(|e| {
779 format!(
780 "Test setup failed: could not get repo with issue fixture: {}",
781 e
782 )
783 })?;
784
691 // Create comment using the helper (which adds NIP-22 tags including 'E') 785 // Create comment using the helper (which adds NIP-22 tags including 'E')
692 let comment = Self::create_comment_for_event(client, &issue, "Comment content")?; 786 let comment = Self::create_comment_for_event(client, &issue, "Comment content")?;
693 787
694 // Send comment and verify it's accepted (via E tag to accepted issue) 788 // Send comment and verify it's accepted (via E tag to accepted issue)
695 Self::send_and_verify_accepted(client, comment, "comment with E tag to accepted issue").await?; 789 Self::send_and_verify_accepted(client, comment, "comment with E tag to accepted issue")
696 790 .await?;
791
697 Ok(()) 792 Ok(())
698 }) 793 })
699 .await 794 .await
700 } 795 }
701 796
702 /// Test 2.3: Kind 1 note with 'e' tag reply to accepted kind 1 should be accepted 797 /// Test 2.3: Kind 1 note with 'e' tag reply to accepted kind 1 should be accepted
703 /// 798 ///
704 /// **Using TestContext pattern:** 799 /// **Using TestContext pattern:**
@@ -713,43 +808,52 @@ impl EventAcceptancePolicyTests {
713 .run(|| async { 808 .run(|| async {
714 // Create TestContext 809 // Create TestContext
715 let ctx = TestContext::new(client); 810 let ctx = TestContext::new(client);
716 811
717 // Get repository fixture (mode-aware) 812 // Get repository fixture (mode-aware)
718 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await 813 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| {
719 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; 814 format!(
720 815 "Test setup failed: could not get valid repository fixture: {}",
816 e
817 )
818 })?;
819
721 // Create Kind 1 A that quotes the repo (makes it accepted) 820 // Create Kind 1 A that quotes the repo (makes it accepted)
722 let repo_id = Self::extract_d_tag(&repo) 821 let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?;
723 .ok_or("Failed to extract repo_id")?;
724 let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); 822 let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id);
725 823
726 let kind1_a = client 824 let kind1_a = client
727 .event_builder(Kind::TextNote, "Note A about repo") 825 .event_builder(Kind::TextNote, "Note A about repo")
728 .tags(vec![Tag::custom(TagKind::custom("q"), vec![a_tag_value])]) 826 .tags(vec![Tag::custom(TagKind::custom("q"), vec![a_tag_value])])
729 .build(client.keys()) 827 .build(client.keys())
730 .map_err(|e| format!("Failed to build kind1 A: {}", e))?; 828 .map_err(|e| format!("Failed to build kind1 A: {}", e))?;
731 829
732 Self::send_and_verify_accepted(client, kind1_a.clone(), "kind 1 A quoting repo").await?; 830 Self::send_and_verify_accepted(client, kind1_a.clone(), "kind 1 A quoting repo")
733 831 .await?;
832
734 // Create Kind 1 B that replies to Kind 1 A via 'e' tag 833 // Create Kind 1 B that replies to Kind 1 A via 'e' tag
735 let kind1_b = client 834 let kind1_b = client
736 .event_builder(Kind::TextNote, "Reply to Note A") 835 .event_builder(Kind::TextNote, "Reply to Note A")
737 .tags(vec![Tag::event(kind1_a.id)]) 836 .tags(vec![Tag::event(kind1_a.id)])
738 .build(client.keys()) 837 .build(client.keys())
739 .map_err(|e| format!("Failed to build kind1 B: {}", e))?; 838 .map_err(|e| format!("Failed to build kind1 B: {}", e))?;
740 839
741 // Send Kind 1 B and verify it's accepted (via 'e' tag to accepted kind 1 A) 840 // Send Kind 1 B and verify it's accepted (via 'e' tag to accepted kind 1 A)
742 Self::send_and_verify_accepted(client, kind1_b, "kind 1 B replying to accepted kind 1 A").await?; 841 Self::send_and_verify_accepted(
743 842 client,
843 kind1_b,
844 "kind 1 B replying to accepted kind 1 A",
845 )
846 .await?;
847
744 Ok(()) 848 Ok(())
745 }) 849 })
746 .await 850 .await
747 } 851 }
748 852
749 // ============================================================ 853 // ============================================================
750 // Group 3: Accept Events Tagged BY Accepted Events (3 tests) 854 // Group 3: Accept Events Tagged BY Accepted Events (3 tests)
751 // ============================================================ 855 // ============================================================
752 856
753 /// Test 3.1: Kind 1 note should be accepted when referenced by an accepted issue (forward ref) 857 /// Test 3.1: Kind 1 note should be accepted when referenced by an accepted issue (forward ref)
754 /// 858 ///
755 /// **Using TestContext pattern:** 859 /// **Using TestContext pattern:**
@@ -764,44 +868,61 @@ impl EventAcceptancePolicyTests {
764 .run(|| async { 868 .run(|| async {
765 // Create TestContext 869 // Create TestContext
766 let ctx = TestContext::new(client); 870 let ctx = TestContext::new(client);
767 871
768 // Get repository fixture (mode-aware) 872 // Get repository fixture (mode-aware)
769 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await 873 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| {
770 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; 874 format!(
771 875 "Test setup failed: could not get valid repository fixture: {}",
876 e
877 )
878 })?;
879
772 // Create Kind 1 note locally but DON'T send it yet 880 // Create Kind 1 note locally but DON'T send it yet
773 let kind1_note = client 881 let kind1_note = client
774 .event_builder(Kind::TextNote, "Note to be referenced") 882 .event_builder(Kind::TextNote, "Note to be referenced")
775 .build(client.keys()) 883 .build(client.keys())
776 .map_err(|e| format!("Failed to build kind1: {}", e))?; 884 .map_err(|e| format!("Failed to build kind1: {}", e))?;
777 885
778 // Create and send issue that QUOTES the unsent Kind 1 note 886 // Create and send issue that QUOTES the unsent Kind 1 note
779 let issue_tags = vec![ 887 let issue_tags = vec![
780 // Reference to accepted repo 888 // Reference to accepted repo
781 Tag::custom(TagKind::custom("a"), vec![ 889 Tag::custom(
782 format!("30617:{}:{}", repo.pubkey, Self::extract_d_tag(&repo).unwrap()) 890 TagKind::custom("a"),
783 ]), 891 vec![format!(
784 Tag::custom(TagKind::custom("subject"), vec!["Issue referencing kind1".to_string()]), 892 "30617:{}:{}",
893 repo.pubkey,
894 Self::extract_d_tag(&repo).unwrap()
895 )],
896 ),
897 Tag::custom(
898 TagKind::custom("subject"),
899 vec!["Issue referencing kind1".to_string()],
900 ),
785 // Quote the Kind 1 that hasn't been sent yet 901 // Quote the Kind 1 that hasn't been sent yet
786 Tag::custom(TagKind::custom("q"), vec![kind1_note.id.to_hex()]), 902 Tag::custom(TagKind::custom("q"), vec![kind1_note.id.to_hex()]),
787 ]; 903 ];
788 904
789 let issue = client 905 let issue = client
790 .event_builder(Kind::Custom(1621), "issue content") 906 .event_builder(Kind::Custom(1621), "issue content")
791 .tags(issue_tags) 907 .tags(issue_tags)
792 .build(client.keys()) 908 .build(client.keys())
793 .map_err(|e| format!("Failed to build issue: {}", e))?; 909 .map_err(|e| format!("Failed to build issue: {}", e))?;
794 910
795 Self::send_and_verify_accepted(client, issue, "issue quoting unsent kind1").await?; 911 Self::send_and_verify_accepted(client, issue, "issue quoting unsent kind1").await?;
796 912
797 // NOW send the Kind 1 note - should be accepted because accepted issue quotes it 913 // NOW send the Kind 1 note - should be accepted because accepted issue quotes it
798 Self::send_and_verify_accepted(client, kind1_note, "kind1 note referenced by accepted issue").await?; 914 Self::send_and_verify_accepted(
799 915 client,
916 kind1_note,
917 "kind1 note referenced by accepted issue",
918 )
919 .await?;
920
800 Ok(()) 921 Ok(())
801 }) 922 })
802 .await 923 .await
803 } 924 }
804 925
805 /// Test 3.2: Comment should be accepted when referenced by another accepted comment (forward ref) 926 /// Test 3.2: Comment should be accepted when referenced by another accepted comment (forward ref)
806 /// 927 ///
807 /// **Using TestContext pattern:** 928 /// **Using TestContext pattern:**
@@ -816,58 +937,74 @@ impl EventAcceptancePolicyTests {
816 .run(|| async { 937 .run(|| async {
817 // Create TestContext 938 // Create TestContext
818 let ctx = TestContext::new(client); 939 let ctx = TestContext::new(client);
819 940
820 // Get repo with issue fixture (mode-aware) 941 // Get repo with issue fixture (mode-aware)
821 let repo = ctx.get_fixture(FixtureKind::RepoWithIssue).await 942 let repo = ctx
822 .map_err(|e| format!("Test setup failed: could not get repo with issue fixture: {}", e))?; 943 .get_fixture(FixtureKind::RepoWithIssue)
823 944 .await
945 .map_err(|e| {
946 format!(
947 "Test setup failed: could not get repo with issue fixture: {}",
948 e
949 )
950 })?;
951
824 // Extract the issue from the repo event (it's stored as the first 'e' tag) 952 // Extract the issue from the repo event (it's stored as the first 'e' tag)
825 let issue_id = repo.tags.iter() 953 let issue_id = repo
954 .tags
955 .iter()
826 .find(|t| t.kind() == TagKind::e()) 956 .find(|t| t.kind() == TagKind::e())
827 .and_then(|t| t.content()) 957 .and_then(|t| t.content())
828 .ok_or("Missing issue reference in RepoWithIssue fixture")?; 958 .ok_or("Missing issue reference in RepoWithIssue fixture")?;
829 959
830 // Query to get the actual issue event 960 // Query to get the actual issue event
831 let filter = Filter::new().id( 961 let filter = Filter::new().id(nostr_sdk::EventId::from_hex(issue_id)
832 nostr_sdk::EventId::from_hex(issue_id) 962 .map_err(|e| format!("Invalid issue ID: {}", e))?);
833 .map_err(|e| format!("Invalid issue ID: {}", e))? 963 let issues = client
834 ); 964 .query(filter)
835 let issues = client.query(filter).await 965 .await
836 .map_err(|e| format!("Failed to query issue: {}", e))?; 966 .map_err(|e| format!("Failed to query issue: {}", e))?;
837 let issue = issues.first() 967 let issue = issues.first().ok_or("Issue not found")?.clone();
838 .ok_or("Issue not found")? 968
839 .clone();
840
841 // Create Comment A locally but DON'T send it yet 969 // Create Comment A locally but DON'T send it yet
842 let comment_a = Self::create_comment_for_event(client, &issue, "Comment A")?; 970 let comment_a = Self::create_comment_for_event(client, &issue, "Comment A")?;
843 971
844 // Create and send Comment B that quotes Comment A (which hasn't been sent) 972 // Create and send Comment B that quotes Comment A (which hasn't been sent)
845 let comment_b_tags = vec![ 973 let comment_b_tags = vec![
846 // NIP-22 tags for the original issue 974 // NIP-22 tags for the original issue
847 Tag::custom(TagKind::custom("E"), vec![issue.id.to_hex(), "".to_string(), "root".to_string()]), 975 Tag::custom(
976 TagKind::custom("E"),
977 vec![issue.id.to_hex(), "".to_string(), "root".to_string()],
978 ),
848 Tag::event(issue.id), 979 Tag::event(issue.id),
849 Tag::custom(TagKind::custom("K"), vec![issue.kind.as_u16().to_string()]), 980 Tag::custom(TagKind::custom("K"), vec![issue.kind.as_u16().to_string()]),
850 Tag::public_key(issue.pubkey), 981 Tag::public_key(issue.pubkey),
851 // Quote Comment A which hasn't been sent yet 982 // Quote Comment A which hasn't been sent yet
852 Tag::custom(TagKind::custom("q"), vec![comment_a.id.to_hex()]), 983 Tag::custom(TagKind::custom("q"), vec![comment_a.id.to_hex()]),
853 ]; 984 ];
854 985
855 let comment_b = client 986 let comment_b = client
856 .event_builder(Kind::Custom(1111), "Comment B quoting Comment A") 987 .event_builder(Kind::Custom(1111), "Comment B quoting Comment A")
857 .tags(comment_b_tags) 988 .tags(comment_b_tags)
858 .build(client.keys()) 989 .build(client.keys())
859 .map_err(|e| format!("Failed to build comment B: {}", e))?; 990 .map_err(|e| format!("Failed to build comment B: {}", e))?;
860 991
861 Self::send_and_verify_accepted(client, comment_b, "comment B quoting unsent comment A").await?; 992 Self::send_and_verify_accepted(client, comment_b, "comment B quoting unsent comment A")
862 993 .await?;
994
863 // NOW send Comment A - should be accepted because accepted Comment B quotes it 995 // NOW send Comment A - should be accepted because accepted Comment B quotes it
864 Self::send_and_verify_accepted(client, comment_a, "comment A referenced by accepted comment B").await?; 996 Self::send_and_verify_accepted(
865 997 client,
998 comment_a,
999 "comment A referenced by accepted comment B",
1000 )
1001 .await?;
1002
866 Ok(()) 1003 Ok(())
867 }) 1004 })
868 .await 1005 .await
869 } 1006 }
870 1007
871 /// Test 3.3: Kind 1 note should be accepted when referenced by another accepted kind 1 (forward ref) 1008 /// Test 3.3: Kind 1 note should be accepted when referenced by another accepted kind 1 (forward ref)
872 /// 1009 ///
873 /// **Using TestContext pattern:** 1010 /// **Using TestContext pattern:**
@@ -882,47 +1019,56 @@ impl EventAcceptancePolicyTests {
882 .run(|| async { 1019 .run(|| async {
883 // Create TestContext 1020 // Create TestContext
884 let ctx = TestContext::new(client); 1021 let ctx = TestContext::new(client);
885 1022
886 // Get repository fixture (mode-aware) 1023 // Get repository fixture (mode-aware)
887 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await 1024 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| {
888 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; 1025 format!(
889 1026 "Test setup failed: could not get valid repository fixture: {}",
1027 e
1028 )
1029 })?;
1030
890 // Create Kind 1 A locally but DON'T send it yet 1031 // Create Kind 1 A locally but DON'T send it yet
891 let kind1_a = client 1032 let kind1_a = client
892 .event_builder(Kind::TextNote, "Note A to be referenced") 1033 .event_builder(Kind::TextNote, "Note A to be referenced")
893 .build(client.keys()) 1034 .build(client.keys())
894 .map_err(|e| format!("Failed to build kind1 A: {}", e))?; 1035 .map_err(|e| format!("Failed to build kind1 A: {}", e))?;
895 1036
896 // Create and send Kind 1 B that: 1037 // Create and send Kind 1 B that:
897 // - Quotes the repo (makes it accepted) 1038 // - Quotes the repo (makes it accepted)
898 // - Mentions Kind 1 A via 'e' tag (which hasn't been sent yet) 1039 // - Mentions Kind 1 A via 'e' tag (which hasn't been sent yet)
899 let repo_id = Self::extract_d_tag(&repo) 1040 let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?;
900 .ok_or("Failed to extract repo_id")?;
901 let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); 1041 let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id);
902 1042
903 let kind1_b = client 1043 let kind1_b = client
904 .event_builder(Kind::TextNote, "Note B mentioning Note A") 1044 .event_builder(Kind::TextNote, "Note B mentioning Note A")
905 .tags(vec![ 1045 .tags(vec![
906 Tag::custom(TagKind::custom("q"), vec![a_tag_value]), // Quote repo (accepted) 1046 Tag::custom(TagKind::custom("q"), vec![a_tag_value]), // Quote repo (accepted)
907 Tag::event(kind1_a.id), // Mention unsent Kind 1 A 1047 Tag::event(kind1_a.id), // Mention unsent Kind 1 A
908 ]) 1048 ])
909 .build(client.keys()) 1049 .build(client.keys())
910 .map_err(|e| format!("Failed to build kind1 B: {}", e))?; 1050 .map_err(|e| format!("Failed to build kind1 B: {}", e))?;
911 1051
912 Self::send_and_verify_accepted(client, kind1_b, "kind1 B mentioning unsent kind1 A").await?; 1052 Self::send_and_verify_accepted(client, kind1_b, "kind1 B mentioning unsent kind1 A")
913 1053 .await?;
1054
914 // NOW send Kind 1 A - should be accepted because accepted Kind 1 B mentions it 1055 // NOW send Kind 1 A - should be accepted because accepted Kind 1 B mentions it
915 Self::send_and_verify_accepted(client, kind1_a, "kind1 A referenced by accepted kind1 B").await?; 1056 Self::send_and_verify_accepted(
916 1057 client,
1058 kind1_a,
1059 "kind1 A referenced by accepted kind1 B",
1060 )
1061 .await?;
1062
917 Ok(()) 1063 Ok(())
918 }) 1064 })
919 .await 1065 .await
920 } 1066 }
921 1067
922 // ============================================================ 1068 // ============================================================
923 // Group 4: Reject Unrelated Events (3 tests) 1069 // Group 4: Reject Unrelated Events (3 tests)
924 // ============================================================ 1070 // ============================================================
925 1071
926 /// Test 4.1: Issue referencing unaccepted repo should be rejected 1072 /// Test 4.1: Issue referencing unaccepted repo should be rejected
927 async fn test_reject_orphan_issue(client: &AuditClient) -> TestResult { 1073 async fn test_reject_orphan_issue(client: &AuditClient) -> TestResult {
928 TestResult::new( 1074 TestResult::new(
@@ -933,18 +1079,24 @@ impl EventAcceptancePolicyTests {
933 .run(|| async { 1079 .run(|| async {
934 // 1. Create a repo but DON'T send it (so it's unaccepted) 1080 // 1. Create a repo but DON'T send it (so it's unaccepted)
935 let unaccepted_repo = Self::create_test_repo(client, "unaccepted-repo-1").await?; 1081 let unaccepted_repo = Self::create_test_repo(client, "unaccepted-repo-1").await?;
936 1082
937 // 2. Create issue that references the unaccepted repo 1083 // 2. Create issue that references the unaccepted repo
938 let orphan_issue = Self::create_issue_for_repo(client, &unaccepted_repo, "Orphan Issue")?; 1084 let orphan_issue =
939 1085 Self::create_issue_for_repo(client, &unaccepted_repo, "Orphan Issue")?;
1086
940 // 3. Send issue and verify it's REJECTED 1087 // 3. Send issue and verify it's REJECTED
941 Self::send_and_verify_rejected(client, orphan_issue, "issue referencing unaccepted repo").await?; 1088 Self::send_and_verify_rejected(
942 1089 client,
1090 orphan_issue,
1091 "issue referencing unaccepted repo",
1092 )
1093 .await?;
1094
943 Ok(()) 1095 Ok(())
944 }) 1096 })
945 .await 1097 .await
946 } 1098 }
947 1099
948 /// Test 4.2: Generic kind 1 note with no repo references should be rejected 1100 /// Test 4.2: Generic kind 1 note with no repo references should be rejected
949 async fn test_reject_orphan_kind1(client: &AuditClient) -> TestResult { 1101 async fn test_reject_orphan_kind1(client: &AuditClient) -> TestResult {
950 TestResult::new( 1102 TestResult::new(
@@ -958,15 +1110,16 @@ impl EventAcceptancePolicyTests {
958 .event_builder(Kind::TextNote, "Just a random note") 1110 .event_builder(Kind::TextNote, "Just a random note")
959 .build(client.keys()) 1111 .build(client.keys())
960 .map_err(|e| format!("Failed to build note: {}", e))?; 1112 .map_err(|e| format!("Failed to build note: {}", e))?;
961 1113
962 // 2. Send note and verify it's REJECTED 1114 // 2. Send note and verify it's REJECTED
963 Self::send_and_verify_rejected(client, orphan_note, "kind 1 with no repo references").await?; 1115 Self::send_and_verify_rejected(client, orphan_note, "kind 1 with no repo references")
964 1116 .await?;
1117
965 Ok(()) 1118 Ok(())
966 }) 1119 })
967 .await 1120 .await
968 } 1121 }
969 1122
970 /// Test 4.3: Comment quoting unaccepted repo should be rejected 1123 /// Test 4.3: Comment quoting unaccepted repo should be rejected
971 /// 1124 ///
972 /// **Using TestContext pattern:** 1125 /// **Using TestContext pattern:**
@@ -982,35 +1135,42 @@ impl EventAcceptancePolicyTests {
982 .run(|| async { 1135 .run(|| async {
983 // Create TestContext 1136 // Create TestContext
984 let ctx = TestContext::new(client); 1137 let ctx = TestContext::new(client);
985 1138
986 // Get accepted repo A fixture (mode-aware) 1139 // Get accepted repo A fixture (mode-aware)
987 let _repo_a = ctx.get_fixture(FixtureKind::ValidRepo).await 1140 let _repo_a = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| {
988 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?; 1141 format!(
989 1142 "Test setup failed: could not get valid repository fixture: {}",
1143 e
1144 )
1145 })?;
1146
990 // Create Repo B but DON'T send it (unaccepted) 1147 // Create Repo B but DON'T send it (unaccepted)
991 let repo_b = Self::create_test_repo(client, "unaccepted-repo-b").await?; 1148 let repo_b = Self::create_test_repo(client, "unaccepted-repo-b").await?;
992 1149
993 // Extract repo_b info and create comment that quotes repo B (not repo A) 1150 // Extract repo_b info and create comment that quotes repo B (not repo A)
994 let repo_b_id = Self::extract_d_tag(&repo_b) 1151 let repo_b_id = Self::extract_d_tag(&repo_b).ok_or("Failed to extract repo_b id")?;
995 .ok_or("Failed to extract repo_b id")?;
996 let repo_b_a_tag = format!("30617:{}:{}", repo_b.pubkey, repo_b_id); 1152 let repo_b_a_tag = format!("30617:{}:{}", repo_b.pubkey, repo_b_id);
997 1153
998 // Create comment that references ONLY repo B (unaccepted) 1154 // Create comment that references ONLY repo B (unaccepted)
999 let tags = vec![ 1155 let tags = vec![
1000 Tag::custom(TagKind::custom("A"), vec![repo_b_a_tag, "".to_string(), "root".to_string()]), 1156 Tag::custom(
1157 TagKind::custom("A"),
1158 vec![repo_b_a_tag, "".to_string(), "root".to_string()],
1159 ),
1001 Tag::custom(TagKind::custom("K"), vec!["30617".to_string()]), 1160 Tag::custom(TagKind::custom("K"), vec!["30617".to_string()]),
1002 Tag::public_key(repo_b.pubkey), 1161 Tag::public_key(repo_b.pubkey),
1003 ]; 1162 ];
1004 1163
1005 let comment = client 1164 let comment = client
1006 .event_builder(Kind::Custom(1111), "Comment on unaccepted repo") 1165 .event_builder(Kind::Custom(1111), "Comment on unaccepted repo")
1007 .tags(tags) 1166 .tags(tags)
1008 .build(client.keys()) 1167 .build(client.keys())
1009 .map_err(|e| format!("Failed to build comment: {}", e))?; 1168 .map_err(|e| format!("Failed to build comment: {}", e))?;
1010 1169
1011 // Send comment and verify it's REJECTED (only references unaccepted repo B) 1170 // Send comment and verify it's REJECTED (only references unaccepted repo B)
1012 Self::send_and_verify_rejected(client, comment, "comment quoting only unaccepted repo").await?; 1171 Self::send_and_verify_rejected(client, comment, "comment quoting only unaccepted repo")
1013 1172 .await?;
1173
1014 Ok(()) 1174 Ok(())
1015 }) 1175 })
1016 .await 1176 .await
@@ -1021,27 +1181,30 @@ impl EventAcceptancePolicyTests {
1021mod tests { 1181mod tests {
1022 use super::*; 1182 use super::*;
1023 use crate::AuditConfig; 1183 use crate::AuditConfig;
1024 1184
1025 #[tokio::test] 1185 #[tokio::test]
1026 #[ignore] // Requires running relay 1186 #[ignore] // Requires running relay
1027 async fn test_grasp01_event_acceptance_policy_against_relay() { 1187 async fn test_grasp01_event_acceptance_policy_against_relay() {
1028 // Read relay URL from environment variable - must be supplied 1188 // Read relay URL from environment variable - must be supplied
1029 let relay_url = std::env::var("RELAY_URL") 1189 let relay_url = std::env::var("RELAY_URL").expect(
1030 .expect("RELAY_URL environment variable must be set. Example: RELAY_URL=ws://localhost:18081"); 1190 "RELAY_URL environment variable must be set. Example: RELAY_URL=ws://localhost:18081",
1031 1191 );
1192
1032 let config = AuditConfig::ci(); 1193 let config = AuditConfig::ci();
1033 let client = AuditClient::new(&relay_url, config) 1194 let client = AuditClient::new(&relay_url, config)
1034 .await 1195 .await
1035 .expect(&format!( 1196 .unwrap_or_else(|_| {
1036 "Failed to connect to relay at {}. Ensure relay is running and accessible. \ 1197 panic!(
1198 "Failed to connect to relay at {}. Ensure relay is running and accessible. \
1037 Try: docker run --rm -p 18081:8081 ghcr.io/danconwaydev/ngit-relay:latest", 1199 Try: docker run --rm -p 18081:8081 ghcr.io/danconwaydev/ngit-relay:latest",
1038 relay_url 1200 relay_url
1039 )); 1201 )
1040 1202 });
1203
1041 let results = EventAcceptancePolicyTests::run_all(&client).await; 1204 let results = EventAcceptancePolicyTests::run_all(&client).await;
1042 results.print_report(); 1205 results.print_report();
1043 1206
1044 // Don't assert all passed yet - some tests may be failing 1207 // Don't assert all passed yet - some tests may be failing
1045 // Future: assert!(results.all_passed(), "Some GRASP-01 event acceptance tests failed"); 1208 // Future: assert!(results.all_passed(), "Some GRASP-01 event acceptance tests failed");
1046 } 1209 }
1047} \ No newline at end of file 1210}
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;
6 6
7pub use event_acceptance_policy::EventAcceptancePolicyTests; 7pub use event_acceptance_policy::EventAcceptancePolicyTests;
8pub use nip01_smoke::Nip01SmokeTests; 8pub use nip01_smoke::Nip01SmokeTests;
9pub use nip11_document::Nip11DocumentTests; \ No newline at end of file 9pub 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 {
13 /// Run all NIP-01 smoke tests 13 /// Run all NIP-01 smoke tests
14 pub async fn run_all(client: &AuditClient) -> AuditResult { 14 pub async fn run_all(client: &AuditClient) -> AuditResult {
15 let mut results = AuditResult::new("NIP-01 Smoke Tests"); 15 let mut results = AuditResult::new("NIP-01 Smoke Tests");
16 16
17 // Run tests sequentially to avoid future type issues 17 // Run tests sequentially to avoid future type issues
18 results.add(Self::test_websocket_connection(client).await); 18 results.add(Self::test_websocket_connection(client).await);
19 results.add(Self::test_send_receive_event(client).await); 19 results.add(Self::test_send_receive_event(client).await);
@@ -21,10 +21,10 @@ impl Nip01SmokeTests {
21 results.add(Self::test_close_subscription(client).await); 21 results.add(Self::test_close_subscription(client).await);
22 results.add(Self::test_reject_invalid_signature(client).await); 22 results.add(Self::test_reject_invalid_signature(client).await);
23 results.add(Self::test_reject_invalid_event_id(client).await); 23 results.add(Self::test_reject_invalid_event_id(client).await);
24 24
25 results 25 results
26 } 26 }
27 27
28 /// Test 1: Can establish WebSocket connection 28 /// Test 1: Can establish WebSocket connection
29 /// 29 ///
30 /// Spec: NIP-01 basic requirement 30 /// Spec: NIP-01 basic requirement
@@ -39,17 +39,17 @@ impl Nip01SmokeTests {
39 if !client.is_connected().await { 39 if !client.is_connected().await {
40 return Err("Failed to connect to relay".to_string()); 40 return Err("Failed to connect to relay".to_string());
41 } 41 }
42 42
43 Ok(()) 43 Ok(())
44 }) 44 })
45 .await 45 .await
46 } 46 }
47 47
48 /// Test 2: Can send EVENT and receive OK response 48 /// Test 2: Can send EVENT and receive OK response
49 /// 49 ///
50 /// Spec: NIP-01 EVENT message 50 /// Spec: NIP-01 EVENT message
51 /// Requirement: Relay MUST accept valid EVENT messages 51 /// Requirement: Relay MUST accept valid EVENT messages
52 /// 52 ///
53 /// For GRASP servers, we send a NIP-34 repository announcement that lists 53 /// For GRASP servers, we send a NIP-34 repository announcement that lists
54 /// the GRASP server in clone and relays tags (required for acceptance). 54 /// the GRASP server in clone and relays tags (required for acceptance).
55 async fn test_send_receive_event(client: &AuditClient) -> TestResult { 55 async fn test_send_receive_event(client: &AuditClient) -> TestResult {
@@ -60,15 +60,17 @@ impl Nip01SmokeTests {
60 ) 60 )
61 .run(|| async { 61 .run(|| async {
62 // Create a NIP-34 announcement event 62 // Create a NIP-34 announcement event
63 let event = client.create_repo_announcement("send_receive_event").await 63 let event = client
64 .create_repo_announcement("send_receive_event")
65 .await
64 .map_err(|e| format!("Failed to create announcement: {}", e))?; 66 .map_err(|e| format!("Failed to create announcement: {}", e))?;
65 67
66 // Send event 68 // Send event
67 let event_id = client 69 let event_id = client
68 .send_event(event.clone()) 70 .send_event(event.clone())
69 .await 71 .await
70 .map_err(|e| format!("Failed to send event: {}", e))?; 72 .map_err(|e| format!("Failed to send event: {}", e))?;
71 73
72 // Verify we got an event ID back 74 // Verify we got an event ID back
73 if event_id != event.id { 75 if event_id != event.id {
74 return Err(format!( 76 return Err(format!(
@@ -76,43 +78,47 @@ impl Nip01SmokeTests {
76 event.id, event_id 78 event.id, event_id
77 )); 79 ));
78 } 80 }
79 81
80 // Wait a bit for event to be indexed 82 // Wait a bit for event to be indexed
81 tokio::time::sleep(std::time::Duration::from_millis(100)).await; 83 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
82 84
83 // Try to query it back 85 // Try to query it back
84 let filter = Filter::new() 86 let filter = Filter::new().kind(Kind::Custom(30617)).id(event_id);
85 .kind(Kind::Custom(30617)) 87
86 .id(event_id);
87
88 let events = client 88 let events = client
89 .query(filter) 89 .query(filter)
90 .await 90 .await
91 .map_err(|e| format!("Failed to query event: {}", e))?; 91 .map_err(|e| format!("Failed to query event: {}", e))?;
92 92
93 if events.is_empty() { 93 if events.is_empty() {
94 // Debug: try querying without audit client filtering 94 // Debug: try querying without audit client filtering
95 eprintln!("Event not found with audit client query, trying direct client query..."); 95 eprintln!("Event not found with audit client query, trying direct client query...");
96 let direct_filter = Filter::new().kind(Kind::Custom(30617)).id(event_id); 96 let direct_filter = Filter::new().kind(Kind::Custom(30617)).id(event_id);
97 let direct_events = client.client().fetch_events(direct_filter, std::time::Duration::from_secs(5)).await 97 let direct_events = client
98 .client()
99 .fetch_events(direct_filter, std::time::Duration::from_secs(5))
100 .await
98 .map_err(|e| format!("Direct query failed: {}", e))?; 101 .map_err(|e| format!("Direct query failed: {}", e))?;
99 let direct_vec: Vec<Event> = direct_events.into_iter().collect(); 102 let direct_vec: Vec<Event> = direct_events.into_iter().collect();
100 eprintln!("Direct query found {} events", direct_vec.len()); 103 eprintln!("Direct query found {} events", direct_vec.len());
101 if !direct_vec.is_empty() { 104 if !direct_vec.is_empty() {
102 eprintln!("Event tags: {:?}", direct_vec[0].tags); 105 eprintln!("Event tags: {:?}", direct_vec[0].tags);
103 } 106 }
104 return Err(format!("Event not found after sending (direct query found {})", direct_vec.len())); 107 return Err(format!(
108 "Event not found after sending (direct query found {})",
109 direct_vec.len()
110 ));
105 } 111 }
106 112
107 if events[0].id != event_id { 113 if events[0].id != event_id {
108 return Err("Retrieved event has different ID".to_string()); 114 return Err("Retrieved event has different ID".to_string());
109 } 115 }
110 116
111 Ok(()) 117 Ok(())
112 }) 118 })
113 .await 119 .await
114 } 120 }
115 121
116 /// Test 3: Can create subscription with REQ 122 /// Test 3: Can create subscription with REQ
117 /// 123 ///
118 /// Spec: NIP-01 REQ message 124 /// Spec: NIP-01 REQ message
@@ -125,34 +131,36 @@ impl Nip01SmokeTests {
125 ) 131 )
126 .run(|| async { 132 .run(|| async {
127 // Create a NIP-34 announcement event (accepted by GRASP relays) 133 // Create a NIP-34 announcement event (accepted by GRASP relays)
128 let event = client.create_repo_announcement("create_subscription").await 134 let event = client
135 .create_repo_announcement("create_subscription")
136 .await
129 .map_err(|e| format!("Failed to create announcement: {}", e))?; 137 .map_err(|e| format!("Failed to create announcement: {}", e))?;
130 138
131 let event_id = client 139 let _event_id = client
132 .send_event(event.clone()) 140 .send_event(event.clone())
133 .await 141 .await
134 .map_err(|e| format!("Failed to send event: {}", e))?; 142 .map_err(|e| format!("Failed to send event: {}", e))?;
135 143
136 // Subscribe to NIP-34 announcements from this author 144 // Subscribe to NIP-34 announcements from this author
137 let filter = Filter::new() 145 let filter = Filter::new()
138 .kind(Kind::Custom(30617)) 146 .kind(Kind::Custom(30617))
139 .author(client.public_key()); 147 .author(client.public_key());
140 148
141 let events = client 149 let events = client
142 .subscribe(vec![filter], Some(std::time::Duration::from_secs(5))) 150 .subscribe(vec![filter], Some(std::time::Duration::from_secs(5)))
143 .await 151 .await
144 .map_err(|e| format!("Failed to subscribe: {}", e))?; 152 .map_err(|e| format!("Failed to subscribe: {}", e))?;
145 153
146 // Should have at least our event 154 // Should have at least our event
147 if events.is_empty() { 155 if events.is_empty() {
148 return Err("No events received from subscription".to_string()); 156 return Err("No events received from subscription".to_string());
149 } 157 }
150 158
151 Ok(()) 159 Ok(())
152 }) 160 })
153 .await 161 .await
154 } 162 }
155 163
156 /// Test 4: Can close subscription with CLOSE 164 /// Test 4: Can close subscription with CLOSE
157 /// 165 ///
158 /// Spec: NIP-01 CLOSE message 166 /// Spec: NIP-01 CLOSE message
@@ -167,22 +175,20 @@ impl Nip01SmokeTests {
167 // For now, we just verify we can query events 175 // For now, we just verify we can query events
168 // Full subscription management with CLOSE would require 176 // Full subscription management with CLOSE would require
169 // lower-level WebSocket access 177 // lower-level WebSocket access
170 178
171 let filter = Filter::new() 179 let filter = Filter::new().kind(Kind::TextNote).limit(1);
172 .kind(Kind::TextNote) 180
173 .limit(1);
174
175 let _events = client 181 let _events = client
176 .subscribe(vec![filter], Some(std::time::Duration::from_secs(2))) 182 .subscribe(vec![filter], Some(std::time::Duration::from_secs(2)))
177 .await 183 .await
178 .map_err(|e| format!("Failed to subscribe: {}", e))?; 184 .map_err(|e| format!("Failed to subscribe: {}", e))?;
179 185
180 // If we got here, subscription worked 186 // If we got here, subscription worked
181 Ok(()) 187 Ok(())
182 }) 188 })
183 .await 189 .await
184 } 190 }
185 191
186 /// Test 5: Rejects events with invalid signatures 192 /// Test 5: Rejects events with invalid signatures
187 /// 193 ///
188 /// Spec: NIP-01 event validation 194 /// Spec: NIP-01 event validation
@@ -199,7 +205,7 @@ impl Nip01SmokeTests {
199 .event_builder(Kind::TextNote, "Invalid signature test") 205 .event_builder(Kind::TextNote, "Invalid signature test")
200 .build(client.keys()) 206 .build(client.keys())
201 .map_err(|e| format!("Failed to build event: {}", e))?; 207 .map_err(|e| format!("Failed to build event: {}", e))?;
202 208
203 // Corrupt the signature by creating a new event with wrong sig 209 // Corrupt the signature by creating a new event with wrong sig
204 // We'll use a different key to sign, creating an invalid signature 210 // We'll use a different key to sign, creating an invalid signature
205 let wrong_keys = Keys::generate(); 211 let wrong_keys = Keys::generate();
@@ -207,7 +213,7 @@ impl Nip01SmokeTests {
207 .tags(event.tags.clone()) 213 .tags(event.tags.clone())
208 .sign_with_keys(&wrong_keys) 214 .sign_with_keys(&wrong_keys)
209 .map_err(|e| format!("Failed to build wrong event: {}", e))?; 215 .map_err(|e| format!("Failed to build wrong event: {}", e))?;
210 216
211 // Create event JSON with mismatched pubkey and signature 217 // Create event JSON with mismatched pubkey and signature
212 // This should be rejected by the relay 218 // This should be rejected by the relay
213 let invalid_event_json = serde_json::json!({ 219 let invalid_event_json = serde_json::json!({
@@ -219,24 +225,24 @@ impl Nip01SmokeTests {
219 "content": event.content, 225 "content": event.content,
220 "sig": wrong_event.sig.to_string(), // Wrong signature! 226 "sig": wrong_event.sig.to_string(), // Wrong signature!
221 }); 227 });
222 228
223 // Parse it back to an Event 229 // Parse it back to an Event
224 let invalid_event: Event = serde_json::from_value(invalid_event_json) 230 let invalid_event: Event = serde_json::from_value(invalid_event_json)
225 .map_err(|e| format!("Failed to create invalid event: {}", e))?; 231 .map_err(|e| format!("Failed to create invalid event: {}", e))?;
226 232
227 // Try to send the invalid event 233 // Try to send the invalid event
228 let result = client.send_event(invalid_event).await; 234 let result = client.send_event(invalid_event).await;
229 235
230 // We expect this to fail 236 // We expect this to fail
231 if result.is_ok() { 237 if result.is_ok() {
232 return Err("Relay accepted event with invalid signature".to_string()); 238 return Err("Relay accepted event with invalid signature".to_string());
233 } 239 }
234 240
235 Ok(()) 241 Ok(())
236 }) 242 })
237 .await 243 .await
238 } 244 }
239 245
240 /// Test 6: Rejects events with invalid event IDs 246 /// Test 6: Rejects events with invalid event IDs
241 /// 247 ///
242 /// Spec: NIP-01 event ID validation 248 /// Spec: NIP-01 event ID validation
@@ -253,7 +259,7 @@ impl Nip01SmokeTests {
253 .event_builder(Kind::TextNote, "Invalid ID test") 259 .event_builder(Kind::TextNote, "Invalid ID test")
254 .build(client.keys()) 260 .build(client.keys())
255 .map_err(|e| format!("Failed to build event: {}", e))?; 261 .map_err(|e| format!("Failed to build event: {}", e))?;
256 262
257 // Create event JSON with corrupted ID 263 // Create event JSON with corrupted ID
258 let invalid_event_json = serde_json::json!({ 264 let invalid_event_json = serde_json::json!({
259 "id": EventId::all_zeros().to_hex(), // Wrong ID! 265 "id": EventId::all_zeros().to_hex(), // Wrong ID!
@@ -264,19 +270,19 @@ impl Nip01SmokeTests {
264 "content": event.content, 270 "content": event.content,
265 "sig": event.sig.to_string(), 271 "sig": event.sig.to_string(),
266 }); 272 });
267 273
268 // Parse it back to an Event 274 // Parse it back to an Event
269 let invalid_event: Event = serde_json::from_value(invalid_event_json) 275 let invalid_event: Event = serde_json::from_value(invalid_event_json)
270 .map_err(|e| format!("Failed to create invalid event: {}", e))?; 276 .map_err(|e| format!("Failed to create invalid event: {}", e))?;
271 277
272 // Try to send the invalid event 278 // Try to send the invalid event
273 let result = client.send_event(invalid_event).await; 279 let result = client.send_event(invalid_event).await;
274 280
275 // We expect this to fail 281 // We expect this to fail
276 if result.is_ok() { 282 if result.is_ok() {
277 return Err("Relay accepted event with invalid ID".to_string()); 283 return Err("Relay accepted event with invalid ID".to_string());
278 } 284 }
279 285
280 Ok(()) 286 Ok(())
281 }) 287 })
282 .await 288 .await
@@ -287,25 +293,25 @@ impl Nip01SmokeTests {
287mod tests { 293mod tests {
288 use super::*; 294 use super::*;
289 use crate::AuditConfig; 295 use crate::AuditConfig;
290 296
291 // Note: These tests require a running relay 297 // Note: These tests require a running relay
292 // They are integration tests, not unit tests 298 // They are integration tests, not unit tests
293 299
294 #[tokio::test] 300 #[tokio::test]
295 #[ignore] // Ignore by default since it needs a running relay 301 #[ignore] // Ignore by default since it needs a running relay
296 async fn test_smoke_tests_against_relay() { 302 async fn test_smoke_tests_against_relay() {
297 // RELAY_URL env var must be set - no default fallback 303 // RELAY_URL env var must be set - no default fallback
298 let relay_url = std::env::var("RELAY_URL") 304 let relay_url = std::env::var("RELAY_URL")
299 .expect("RELAY_URL environment variable must be set for integration tests"); 305 .expect("RELAY_URL environment variable must be set for integration tests");
300 306
301 let config = AuditConfig::ci(); 307 let config = AuditConfig::ci();
302 let client = AuditClient::new(&relay_url, config) 308 let client = AuditClient::new(&relay_url, config)
303 .await 309 .await
304 .expect("Failed to connect to relay"); 310 .expect("Failed to connect to relay");
305 311
306 let results = Nip01SmokeTests::run_all(&client).await; 312 let results = Nip01SmokeTests::run_all(&client).await;
307 results.print_report(); 313 results.print_report();
308 314
309 assert!(results.all_passed(), "Some smoke tests failed"); 315 assert!(results.all_passed(), "Some smoke tests failed");
310 } 316 }
311} 317}
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 @@
9//! - Handles curation field correctly (present if curated, absent otherwise) 9//! - Handles curation field correctly (present if curated, absent otherwise)
10 10
11use crate::{AuditClient, AuditResult, TestResult}; 11use crate::{AuditClient, AuditResult, TestResult};
12use nostr_sdk::prelude::*;
13 12
14pub struct Nip11DocumentTests; 13pub struct Nip11DocumentTests;
15 14
@@ -17,25 +16,25 @@ impl Nip11DocumentTests {
17 /// Run all NIP-11 document tests 16 /// Run all NIP-11 document tests
18 pub async fn run_all(client: &AuditClient) -> AuditResult { 17 pub async fn run_all(client: &AuditClient) -> AuditResult {
19 let mut results = AuditResult::new("GRASP-01 NIP-11 Document Tests"); 18 let mut results = AuditResult::new("GRASP-01 NIP-11 Document Tests");
20 19
21 // NIP-11 relay information tests 20 // NIP-11 relay information tests
22 results.add(Self::test_nip11_document_exists(client).await); 21 results.add(Self::test_nip11_document_exists(client).await);
23 results.add(Self::test_nip11_supported_grasps_field(client).await); 22 results.add(Self::test_nip11_supported_grasps_field(client).await);
24 results.add(Self::test_nip11_repo_acceptance_criteria_field(client).await); 23 results.add(Self::test_nip11_repo_acceptance_criteria_field(client).await);
25 results.add(Self::test_nip11_curation_field(client).await); 24 results.add(Self::test_nip11_curation_field(client).await);
26 25
27 results 26 results
28 } 27 }
29 28
30 // ========================================================================= 29 // =========================================================================
31 // NIP-11 Relay Information Tests 30 // NIP-11 Relay Information Tests
32 // ========================================================================= 31 // =========================================================================
33 32
34 /// Test: Serve NIP-11 document 33 /// Test: Serve NIP-11 document
35 /// 34 ///
36 /// Spec: Line 11 of ../grasp/01.md 35 /// Spec: Line 11 of ../grasp/01.md
37 /// Requirement: MUST serve NIP-11 document 36 /// Requirement: MUST serve NIP-11 document
38 async fn test_nip11_document_exists(client: &AuditClient) -> TestResult { 37 async fn test_nip11_document_exists(_client: &AuditClient) -> TestResult {
39 TestResult::new( 38 TestResult::new(
40 "nip11_document_exists", 39 "nip11_document_exists",
41 "GRASP-01:nostr-relay:11", 40 "GRASP-01:nostr-relay:11",
@@ -52,17 +51,17 @@ impl Nip11DocumentTests {
52 // 4. Verify response is valid JSON 51 // 4. Verify response is valid JSON
53 // 5. Parse as NIP-11 document 52 // 5. Parse as NIP-11 document
54 // 6. Verify has required fields (name, description, etc.) 53 // 6. Verify has required fields (name, description, etc.)
55 54
56 Err("Not implemented yet".to_string()) 55 Err("Not implemented yet".to_string())
57 }) 56 })
58 .await 57 .await
59 } 58 }
60 59
61 /// Test: NIP-11 includes supported_grasps field 60 /// Test: NIP-11 includes supported_grasps field
62 /// 61 ///
63 /// Spec: Line 12 of ../grasp/01.md 62 /// Spec: Line 12 of ../grasp/01.md
64 /// Requirement: MUST list supported GRASPs as string array 63 /// Requirement: MUST list supported GRASPs as string array
65 async fn test_nip11_supported_grasps_field(client: &AuditClient) -> TestResult { 64 async fn test_nip11_supported_grasps_field(_client: &AuditClient) -> TestResult {
66 TestResult::new( 65 TestResult::new(
67 "nip11_supported_grasps_field", 66 "nip11_supported_grasps_field",
68 "GRASP-01:nostr-relay:12", 67 "GRASP-01:nostr-relay:12",
@@ -76,17 +75,17 @@ impl Nip11DocumentTests {
76 // 4. Verify array includes "GRASP-01" 75 // 4. Verify array includes "GRASP-01"
77 // 5. Verify format: each entry matches pattern "GRASP-\d{2}" 76 // 5. Verify format: each entry matches pattern "GRASP-\d{2}"
78 // 6. Document other GRASPs found (for info) 77 // 6. Document other GRASPs found (for info)
79 78
80 Err("Not implemented yet".to_string()) 79 Err("Not implemented yet".to_string())
81 }) 80 })
82 .await 81 .await
83 } 82 }
84 83
85 /// Test: NIP-11 includes repo_acceptance_criteria field 84 /// Test: NIP-11 includes repo_acceptance_criteria field
86 /// 85 ///
87 /// Spec: Line 13 of ../grasp/01.md 86 /// Spec: Line 13 of ../grasp/01.md
88 /// Requirement: MUST list repository acceptance criteria 87 /// Requirement: MUST list repository acceptance criteria
89 async fn test_nip11_repo_acceptance_criteria_field(client: &AuditClient) -> TestResult { 88 async fn test_nip11_repo_acceptance_criteria_field(_client: &AuditClient) -> TestResult {
90 TestResult::new( 89 TestResult::new(
91 "nip11_repo_acceptance_criteria_field", 90 "nip11_repo_acceptance_criteria_field",
92 "GRASP-01:nostr-relay:13", 91 "GRASP-01:nostr-relay:13",
@@ -101,17 +100,17 @@ impl Nip11DocumentTests {
101 // 5. Document the criteria (for info) 100 // 5. Document the criteria (for info)
102 // Examples: "Must list this relay in clone and relays tags" 101 // Examples: "Must list this relay in clone and relays tags"
103 // "Pre-payment required via Lightning invoice" 102 // "Pre-payment required via Lightning invoice"
104 103
105 Err("Not implemented yet".to_string()) 104 Err("Not implemented yet".to_string())
106 }) 105 })
107 .await 106 .await
108 } 107 }
109 108
110 /// Test: NIP-11 curation field handling 109 /// Test: NIP-11 curation field handling
111 /// 110 ///
112 /// Spec: Line 14 of ../grasp/01.md 111 /// Spec: Line 14 of ../grasp/01.md
113 /// Requirement: MUST include curation if curated, omit otherwise 112 /// Requirement: MUST include curation if curated, omit otherwise
114 async fn test_nip11_curation_field(client: &AuditClient) -> TestResult { 113 async fn test_nip11_curation_field(_client: &AuditClient) -> TestResult {
115 TestResult::new( 114 TestResult::new(
116 "nip11_curation_field", 115 "nip11_curation_field",
117 "GRASP-01:nostr-relay:14", 116 "GRASP-01:nostr-relay:14",
@@ -127,39 +126,41 @@ impl Nip11DocumentTests {
127 // 4. If absent: 126 // 4. If absent:
128 // - Document that no curation beyond SPAM prevention 127 // - Document that no curation beyond SPAM prevention
129 // 5. Both cases are valid per spec 128 // 5. Both cases are valid per spec
130 129
131 Err("Not implemented yet".to_string()) 130 Err("Not implemented yet".to_string())
132 }) 131 })
133 .await 132 .await
134 } 133 }
135
136} 134}
137 135
138#[cfg(test)] 136#[cfg(test)]
139mod tests { 137mod tests {
140 use super::*; 138 use super::*;
141 use crate::AuditConfig; 139 use crate::AuditConfig;
142 140
143#[tokio::test] 141 #[tokio::test]
144#[ignore] // Requires running relay 142 #[ignore] // Requires running relay
145async fn test_grasp01_nip11_document_against_relay() { 143 async fn test_grasp01_nip11_document_against_relay() {
146 // Read relay URL from environment variable - must be supplied 144 // Read relay URL from environment variable - must be supplied
147 let relay_url = std::env::var("RELAY_URL") 145 let relay_url = std::env::var("RELAY_URL").expect(
148 .expect("RELAY_URL environment variable must be set. Example: RELAY_URL=ws://localhost:18081"); 146 "RELAY_URL environment variable must be set. Example: RELAY_URL=ws://localhost:18081",
149 147 );
150 let config = AuditConfig::ci(); 148
151 let client = AuditClient::new(&relay_url, config) 149 let config = AuditConfig::ci();
152 .await 150 let client = AuditClient::new(&relay_url, config)
153 .expect(&format!( 151 .await
154 "Failed to connect to relay at {}. Ensure relay is running and accessible. \ 152 .unwrap_or_else(|_| {
153 panic!(
154 "Failed to connect to relay at {}. Ensure relay is running and accessible. \
155 Try: docker run --rm -p 18081:8081 ghcr.io/danconwaydev/ngit-relay:latest", 155 Try: docker run --rm -p 18081:8081 ghcr.io/danconwaydev/ngit-relay:latest",
156 relay_url 156 relay_url
157 )); 157 )
158 158 });
159
159 let results = Nip11DocumentTests::run_all(&client).await; 160 let results = Nip11DocumentTests::run_all(&client).await;
160 results.print_report(); 161 results.print_report();
161 162
162 // Don't assert all passed yet - tests not implemented 163 // Don't assert all passed yet - tests not implemented
163 // assert!(results.all_passed(), "Some GRASP-01 NIP-11 document tests failed"); 164 // assert!(results.all_passed(), "Some GRASP-01 NIP-11 document tests failed");
164 } 165 }
165} \ No newline at end of file 166}
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 @@
3pub mod grasp01; 3pub mod grasp01;
4 4
5// Re-export all test structs from grasp01 module 5// Re-export all test structs from grasp01 module
6pub use grasp01::{ 6pub use grasp01::{EventAcceptancePolicyTests, Nip01SmokeTests, Nip11DocumentTests};
7 EventAcceptancePolicyTests,
8 Nip01SmokeTests,
9 Nip11DocumentTests,
10};