upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/specs/grasp01
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/specs/grasp01
parentfa065ad128882755f2a988d6203b59a2ab5e38ff (diff)
fix some clippy fmt warnings
Diffstat (limited to 'grasp-audit/src/specs/grasp01')
-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
4 files changed, 542 insertions, 372 deletions
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}