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-06 12:59:29 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-06 15:17:32 +0000
commit16d14d07b023614c1da0fbb11693d131327a3532 (patch)
treec6e4b2fe6bae57bd35e8d526f281a84b515641f2 /grasp-audit/src
parentad6b8a825a500896d613fed72c11e7cbce3ddfd9 (diff)
fix cli runs to prevent rate limiting
Diffstat (limited to 'grasp-audit/src')
-rw-r--r--grasp-audit/src/audit.rs2
-rw-r--r--grasp-audit/src/client.rs14
-rw-r--r--grasp-audit/src/fixtures.rs312
-rw-r--r--grasp-audit/src/isolation.rs2
-rw-r--r--grasp-audit/src/lib.rs2
-rw-r--r--grasp-audit/src/specs/grasp01/event_acceptance_policy.rs495
6 files changed, 620 insertions, 207 deletions
diff --git a/grasp-audit/src/audit.rs b/grasp-audit/src/audit.rs
index 105fa00..8afe660 100644
--- a/grasp-audit/src/audit.rs
+++ b/grasp-audit/src/audit.rs
@@ -31,7 +31,7 @@ pub enum AuditMode {
31impl AuditConfig { 31impl AuditConfig {
32 /// Create config for CI/CD testing 32 /// Create config for CI/CD testing
33 pub fn ci() -> Self { 33 pub fn ci() -> Self {
34 let run_id = format!("ci-{}", uuid::Uuid::new_v4()); 34 let run_id = format!("ci-{}", &uuid::Uuid::new_v4().to_string()[..8]);
35 Self { 35 Self {
36 run_id, 36 run_id,
37 mode: AuditMode::CI, 37 mode: AuditMode::CI,
diff --git a/grasp-audit/src/client.rs b/grasp-audit/src/client.rs
index 74a16d8..1f6f0fb 100644
--- a/grasp-audit/src/client.rs
+++ b/grasp-audit/src/client.rs
@@ -13,6 +13,18 @@ pub struct AuditClient {
13} 13}
14 14
15impl AuditClient { 15impl AuditClient {
16 /// Create a new audit client for testing (no relay connection)
17 #[cfg(test)]
18 pub fn new_test(config: AuditConfig) -> Self {
19 let keys = Keys::generate();
20 let client = Client::new(keys.clone());
21 Self {
22 client,
23 config,
24 keys,
25 }
26 }
27
16 /// Create a new audit client 28 /// Create a new audit client
17 pub async fn new(relay_url: &str, config: AuditConfig) -> Result<Self> { 29 pub async fn new(relay_url: &str, config: AuditConfig) -> Result<Self> {
18 let keys = Keys::generate(); 30 let keys = Keys::generate();
@@ -216,7 +228,7 @@ impl AuditClient {
216 .replace("wss://", "https://"); 228 .replace("wss://", "https://");
217 229
218 // Create unique repository identifier using UUID for consistency 230 // Create unique repository identifier using UUID for consistency
219 let repo_id = format!("{}-{}", test_name, uuid::Uuid::new_v4()); 231 let repo_id = format!("{}-{}", test_name, &uuid::Uuid::new_v4().to_string()[..8]);
220 232
221 // Get npub for clone URL 233 // Get npub for clone URL
222 let npub = self.public_key().to_bech32() 234 let npub = self.public_key().to_bech32()
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs
new file mode 100644
index 0000000..e34ee6d
--- /dev/null
+++ b/grasp-audit/src/fixtures.rs
@@ -0,0 +1,312 @@
1//! Test fixture management for dual-mode testing
2//!
3//! This module provides a TestContext abstraction that manages prerequisite events
4//! differently based on the audit mode:
5//!
6//! - **CI Mode (Isolated)**: Creates fresh events for each test, ensuring complete isolation
7//! - **Production Mode (Shared)**: Reuses shared fixtures to minimize event publication
8//!
9//! # Example
10//!
11//! ```no_run
12//! use grasp_audit::*;
13//!
14//! # async fn example() -> anyhow::Result<()> {
15//! let config = AuditConfig::ci();
16//! let client = AuditClient::new("ws://localhost:7000", config).await?;
17//! let ctx = TestContext::new(&client);
18//!
19//! // Request a fixture - behavior depends on mode
20//! let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?;
21//! # Ok(())
22//! # }
23//! ```
24
25use crate::{AuditClient, AuditMode};
26use anyhow::{Context, Result};
27use nostr_sdk::prelude::Event;
28use std::collections::HashMap;
29use std::sync::{Arc, Mutex};
30
31/// Types of test fixtures available
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum FixtureKind {
34 /// Basic repository announcement (kind 30617)
35 ValidRepo,
36
37 /// Repository with one issue (kind 1621)
38 RepoWithIssue,
39
40 /// Repository with issue and comment (kind 1111)
41 RepoWithComment,
42
43 /// Repository state announcement (kind 30618)
44 RepoState,
45}
46
47/// Context mode for fixture management
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum ContextMode {
50 /// Create fresh fixtures for each request (test isolation)
51 Isolated,
52
53 /// Reuse shared fixtures across requests (minimal events)
54 Shared,
55}
56
57impl From<AuditMode> for ContextMode {
58 fn from(mode: AuditMode) -> Self {
59 match mode {
60 AuditMode::CI => ContextMode::Isolated,
61 AuditMode::Production => ContextMode::Shared,
62 }
63 }
64}
65
66/// Test context for managing prerequisite events
67///
68/// The TestContext provides mode-aware fixture management:
69/// - In Isolated mode: Creates fresh events for each test
70/// - In Shared mode: Caches and reuses events across tests
71///
72/// # Example
73///
74/// ```no_run
75/// # use grasp_audit::*;
76/// # async fn example() -> anyhow::Result<()> {
77/// let config = AuditConfig::ci();
78/// let client = AuditClient::new("ws://localhost:7000", config).await?;
79/// let ctx = TestContext::new(&client);
80///
81/// // Get a repository fixture
82/// let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?;
83///
84/// // In CI mode: Creates new repo
85/// // In Production mode: Returns cached repo
86/// # Ok(())
87/// # }
88/// ```
89pub struct TestContext<'a> {
90 client: &'a AuditClient,
91 mode: ContextMode,
92 cache: Arc<Mutex<HashMap<FixtureKind, Event>>>,
93}
94
95impl<'a> TestContext<'a> {
96 /// Create a new test context
97 ///
98 /// The context mode is automatically determined from the client's audit config.
99 pub fn new(client: &'a AuditClient) -> Self {
100 let mode = ContextMode::from(client.config.mode);
101 Self {
102 client,
103 mode,
104 cache: Arc::new(Mutex::new(HashMap::new())),
105 }
106 }
107
108 /// Create a test context with explicit mode override
109 ///
110 /// This is useful for testing the context itself or for advanced use cases
111 /// where you want to override the default mode behavior.
112 pub fn with_mode(client: &'a AuditClient, mode: ContextMode) -> Self {
113 Self {
114 client,
115 mode,
116 cache: Arc::new(Mutex::new(HashMap::new())),
117 }
118 }
119
120 /// Get a fixture, creating it if needed based on mode
121 ///
122 /// # Behavior
123 ///
124 /// - **Isolated mode**: Always creates a fresh fixture
125 /// - **Shared mode**: Returns cached fixture or creates and caches if not present
126 ///
127 /// # Example
128 ///
129 /// ```no_run
130 /// # use grasp_audit::*;
131 /// # async fn example(ctx: &TestContext<'_>) -> anyhow::Result<()> {
132 /// let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?;
133 /// # Ok(())
134 /// # }
135 /// ```
136 pub async fn get_fixture(&self, kind: FixtureKind) -> Result<Event> {
137 match self.mode {
138 ContextMode::Isolated => self.create_fresh(kind).await,
139 ContextMode::Shared => self.get_or_create_shared(kind).await,
140 }
141 }
142
143 /// Get the underlying client for direct access
144 ///
145 /// This allows tests to use the client directly when needed while still
146 /// benefiting from the TestContext for fixture management.
147 pub fn client(&self) -> &'a AuditClient {
148 self.client
149 }
150
151 /// Get the current context mode
152 pub fn mode(&self) -> ContextMode {
153 self.mode
154 }
155
156 /// Create a fresh fixture (always creates new)
157 async fn create_fresh(&self, kind: FixtureKind) -> Result<Event> {
158 let event = self.build_fixture(kind).await
159 .with_context(|| format!("Failed to build {:?} fixture", kind))?;
160
161 self.client.send_event(event.clone()).await
162 .with_context(|| format!("Failed to send {:?} fixture event to relay", kind))?;
163
164 Ok(event)
165 }
166
167 /// Get or create a shared fixture (caches for reuse)
168 async fn get_or_create_shared(&self, kind: FixtureKind) -> Result<Event> {
169 // Check cache first
170 {
171 let cache = self.cache.lock().unwrap();
172 if let Some(event) = cache.get(&kind) {
173 return Ok(event.clone());
174 }
175 }
176
177 // Not in cache, create it
178 let event = self.build_fixture(kind).await
179 .with_context(|| format!("Failed to build {:?} fixture for shared cache", kind))?;
180
181 self.client.send_event(event.clone()).await
182 .with_context(|| format!("Failed to send {:?} fixture event to relay (shared cache)", kind))?;
183
184 // Store in cache
185 {
186 let mut cache = self.cache.lock().unwrap();
187 cache.insert(kind, event.clone());
188 }
189
190 Ok(event)
191 }
192
193 /// Build a fixture event (doesn't send it)
194 async fn build_fixture(&self, kind: FixtureKind) -> Result<Event> {
195 match kind {
196 FixtureKind::ValidRepo => {
197 let test_name = format!("fixture-{:?}-{}", kind, &uuid::Uuid::new_v4().to_string()[..8]);
198 self.client.create_repo_announcement(&test_name).await
199 }
200
201 FixtureKind::RepoWithIssue => {
202 // First create repo
203 let test_name = format!("fixture-{:?}-{}", FixtureKind::ValidRepo, &uuid::Uuid::new_v4().to_string()[..8]);
204 let repo = self.client.create_repo_announcement(&test_name).await?;
205 self.client.send_event(repo.clone()).await?;
206
207 // Then create issue referencing it
208 self.client.create_issue(
209 &repo,
210 "Test Issue",
211 "Issue content for testing",
212 vec![],
213 )
214 }
215
216 FixtureKind::RepoWithComment => {
217 // First create repo with issue
218 let test_name = format!("fixture-{:?}-{}", FixtureKind::ValidRepo, &uuid::Uuid::new_v4().to_string()[..8]);
219 let repo = self.client.create_repo_announcement(&test_name).await?;
220 self.client.send_event(repo.clone()).await?;
221
222 let issue = self.client.create_issue(
223 &repo,
224 "Test Issue",
225 "Issue content",
226 vec![],
227 )?;
228 self.client.send_event(issue.clone()).await?;
229
230 // Then create comment on issue
231 self.client.create_comment(
232 &issue,
233 "Test comment",
234 vec![],
235 )
236 }
237
238 FixtureKind::RepoState => {
239 use nostr_sdk::prelude::*;
240
241 // First create repo announcement
242 let test_name = format!("fixture-{:?}-{}", FixtureKind::ValidRepo, &uuid::Uuid::new_v4().to_string()[..8]);
243 let repo = self.client.create_repo_announcement(&test_name).await?;
244 self.client.send_event(repo.clone()).await?;
245
246 // Extract repo_id from repo announcement
247 let repo_id = repo.tags.iter()
248 .find(|t| t.kind() == TagKind::d())
249 .and_then(|t| t.content())
250 .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement"))?
251 .to_string();
252
253 // Create state announcement
254 self.client.event_builder(Kind::Custom(30618), "")
255 .tag(Tag::identifier(&repo_id))
256 .tag(Tag::custom(TagKind::custom("refs/heads/main"), vec![
257 "abc123def456789012345678901234567890abcd"
258 ]))
259 .tag(Tag::custom(TagKind::custom("HEAD"), vec![
260 "ref: refs/heads/main"
261 ]))
262 .build(self.client.keys())
263 .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e))
264 }
265 }
266 }
267
268 /// Clear the fixture cache
269 ///
270 /// This is useful for tests that want to ensure fresh fixtures
271 /// even in shared mode.
272 pub fn clear_cache(&self) {
273 let mut cache = self.cache.lock().unwrap();
274 cache.clear();
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::AuditConfig;
282
283 #[test]
284 fn test_context_mode_from_audit_mode() {
285 assert_eq!(ContextMode::from(AuditMode::CI), ContextMode::Isolated);
286 assert_eq!(ContextMode::from(AuditMode::Production), ContextMode::Shared);
287 }
288
289 #[test]
290 fn test_fixture_kind_hash() {
291 use std::collections::HashSet;
292
293 let mut set = HashSet::new();
294 set.insert(FixtureKind::ValidRepo);
295 set.insert(FixtureKind::RepoWithIssue);
296
297 assert!(set.contains(&FixtureKind::ValidRepo));
298 assert!(!set.contains(&FixtureKind::RepoWithComment));
299 }
300
301 #[tokio::test]
302 async fn test_context_creation() {
303 let config = AuditConfig::ci();
304 let client = crate::AuditClient::new_test(config);
305
306 let ctx = TestContext::new(&client);
307 assert_eq!(ctx.mode(), ContextMode::Isolated);
308
309 let ctx = TestContext::with_mode(&client, ContextMode::Shared);
310 assert_eq!(ctx.mode(), ContextMode::Shared);
311 }
312} \ No newline at end of file
diff --git a/grasp-audit/src/isolation.rs b/grasp-audit/src/isolation.rs
index 298781a..540da34 100644
--- a/grasp-audit/src/isolation.rs
+++ b/grasp-audit/src/isolation.rs
@@ -17,7 +17,7 @@ pub fn generate_test_id() -> String {
17 17
18/// Generate a unique audit run ID for CI 18/// Generate a unique audit run ID for CI
19pub fn generate_ci_run_id() -> String { 19pub fn generate_ci_run_id() -> String {
20 format!("ci-{}", uuid::Uuid::new_v4()) 20 format!("ci-{}", &uuid::Uuid::new_v4().to_string()[..8])
21} 21}
22 22
23/// Generate a unique audit run ID for production 23/// Generate a unique audit run ID for production
diff --git a/grasp-audit/src/lib.rs b/grasp-audit/src/lib.rs
index 3a6404f..6eac73c 100644
--- a/grasp-audit/src/lib.rs
+++ b/grasp-audit/src/lib.rs
@@ -29,12 +29,14 @@
29//! ``` 29//! ```
30 30
31pub mod audit; 31pub mod audit;
32pub mod fixtures;
32pub mod client; 33pub mod client;
33pub mod isolation; 34pub mod isolation;
34pub mod result; 35pub mod result;
35pub mod specs; 36pub mod specs;
36 37
37pub use audit::{AuditConfig, AuditMode}; 38pub use audit::{AuditConfig, AuditMode};
39pub use fixtures::{ContextMode, FixtureKind, TestContext};
38pub use client::AuditClient; 40pub use client::AuditClient;
39pub use result::{AuditResult, TestResult}; 41pub use result::{AuditResult, TestResult};
40 42
diff --git a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs
index d235eb0..353d2c3 100644
--- a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs
+++ b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs
@@ -89,7 +89,7 @@
89//! - Transitive tests verify multi-hop acceptance chains 89//! - Transitive tests verify multi-hop acceptance chains
90 90
91use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp}; 91use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp};
92use crate::{AuditClient, AuditResult, TestResult}; 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
@@ -139,6 +139,10 @@ impl EventAcceptancePolicyTests {
139 /// 139 ///
140 /// Spec: Lines 3-5 of ../grasp/01.md 140 /// Spec: Lines 3-5 of ../grasp/01.md
141 /// Requirement: MUST accept repo announcements listing service in clone & relays tags 141 /// Requirement: MUST accept repo announcements listing service in clone & relays tags
142 ///
143 /// **Using TestContext pattern:**
144 /// - In CI mode: Creates fresh repo for full isolation
145 /// - In Production mode: Reuses cached repo to minimize events
142 async fn test_accept_valid_repo_announcement(client: &AuditClient) -> TestResult { 146 async fn test_accept_valid_repo_announcement(client: &AuditClient) -> TestResult {
143 TestResult::new( 147 TestResult::new(
144 "accept_valid_repo_announcement", 148 "accept_valid_repo_announcement",
@@ -146,9 +150,12 @@ impl EventAcceptancePolicyTests {
146 "Accept valid repository announcements with service in clone and relays tags", 150 "Accept valid repository announcements with service in clone and relays tags",
147 ) 151 )
148 .run(|| async { 152 .run(|| async {
149 // Create a NIP-34 repository announcement event 153 // Create TestContext for mode-aware fixture management
150 let event = client.create_repo_announcement("accept_valid_repo_announcement").await 154 let ctx = TestContext::new(client);
151 .map_err(|e| format!("Failed to create repository announcement: {}", e))?; 155
156 // Request repository fixture - behavior depends on mode
157 let event = ctx.get_fixture(FixtureKind::ValidRepo).await
158 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?;
152 159
153 // Get relay URL for validation 160 // Get relay URL for validation
154 let relay_url = client.client().relays().await 161 let relay_url = client.client().relays().await
@@ -169,9 +176,7 @@ impl EventAcceptancePolicyTests {
169 .ok_or("Missing d tag in announcement")? 176 .ok_or("Missing d tag in announcement")?
170 .to_string(); 177 .to_string();
171 178
172 // Send the event 179 let event_id = event.id;
173 let event_id = client.send_event(event.clone()).await
174 .map_err(|e| format!("Failed to send repository announcement to relay: {}", e))?;
175 180
176 // Query back to verify it was accepted and stored 181 // Query back to verify it was accepted and stored
177 let filter = Filter::new() 182 let filter = Filter::new()
@@ -355,6 +360,11 @@ impl EventAcceptancePolicyTests {
355 /// 360 ///
356 /// Spec: Lines 6-7 of ../grasp/01.md 361 /// Spec: Lines 6-7 of ../grasp/01.md
357 /// Requirement: MUST accept repo state announcements with d, maintainers, and r tags 362 /// Requirement: MUST accept repo state announcements with d, maintainers, and r tags
363 ///
364 /// **EXAMPLE: Using TestContext pattern for fixture management**
365 /// This test demonstrates the new TestContext pattern:
366 /// - In CI mode: Creates fresh repo for full isolation
367 /// - In Production mode: Reuses cached repo to minimize events
358 async fn test_accept_valid_repo_state_announcement(client: &AuditClient) -> TestResult { 368 async fn test_accept_valid_repo_state_announcement(client: &AuditClient) -> TestResult {
359 TestResult::new( 369 TestResult::new(
360 "accept_valid_repo_state_announcement", 370 "accept_valid_repo_state_announcement",
@@ -362,10 +372,14 @@ impl EventAcceptancePolicyTests {
362 "Accept valid state announcements after repo announcement accepted", 372 "Accept valid state announcements after repo announcement accepted",
363 ) 373 )
364 .run(|| async { 374 .run(|| async {
365 // First, create a repository announcement (kind 30617) by the same author 375 // NEW: Create TestContext for mode-aware fixture management
366 let test_name = format!("test-repo-multi-refs-{}", Timestamp::now().as_u64()); 376 let ctx = TestContext::new(client);
367 let repo_event = client.create_repo_announcement(&test_name).await 377
368 .map_err(|e| format!("Failed to create repository announcement: {}", e))?; 378 // NEW: Request repository fixture - behavior depends on mode
379 // CI mode: Creates fresh repo for this test
380 // Production mode: Returns cached repo if available
381 let repo_event = ctx.get_fixture(FixtureKind::RepoState).await
382 .map_err(|e| format!("Test setup failed: could not get repository state fixture: {}", e))?;
369 383
370 // Extract repo_id from the repository announcement 384 // Extract repo_id from the repository announcement
371 let repo_id = repo_event.tags.iter() 385 let repo_id = repo_event.tags.iter()
@@ -374,36 +388,7 @@ impl EventAcceptancePolicyTests {
374 .ok_or("Missing d tag in repository announcement")? 388 .ok_or("Missing d tag in repository announcement")?
375 .to_string(); 389 .to_string();
376 390
377 // Note: npub not used in this test, removed unused variable 391 let event_id = repo_event.id;
378
379 // Create kind 30618 repository state announcement with multiple refs
380 // Format: ["r", "refs/heads/main", "<commit-id>"]
381 let event = client.event_builder(Kind::Custom(30618), "")
382 .tag(Tag::identifier(&repo_id))
383 .tag(Tag::custom(TagKind::custom("refs/heads/main"), vec![
384 "abc123def456789012345678901234567890abcd"
385 ]))
386 .tag(Tag::custom(TagKind::custom("refs/heads/develop"), vec![
387 "def456789012345678901234567890abcdef123"
388 ]))
389 .tag(Tag::custom(TagKind::custom("refs/tags/v1.0.0"), vec![
390 "123456789012345678901234567890abcdef456"
391 ]))
392 .tag(Tag::custom(TagKind::custom("HEAD"), vec![
393 "ref: refs/heads/main"
394 ]))
395 .build(client.keys())
396 .map_err(|e| format!("Failed to build state announcement: {}", e))?;
397
398 let event_id = event.id;
399
400 // Send the repo announcement event
401 client.send_event(repo_event.clone()).await
402 .map_err(|e| format!("Failed to send state announcement to relay: {}", e))?;
403
404 // Send the state event
405 client.send_event(event.clone()).await
406 .map_err(|e| format!("Failed to send state announcement to relay: {}", e))?;
407 392
408 // Query back to verify it was accepted and stored 393 // Query back to verify it was accepted and stored
409 let filter = Filter::new() 394 let filter = Filter::new()
@@ -447,7 +432,7 @@ impl EventAcceptancePolicyTests {
447 async fn create_test_repo(client: &AuditClient, repo_id: &str) -> Result<Event, String> { 432 async fn create_test_repo(client: &AuditClient, repo_id: &str) -> Result<Event, String> {
448 client.create_repo_announcement(repo_id) 433 client.create_repo_announcement(repo_id)
449 .await 434 .await
450 .map_err(|e| e.to_string()) 435 .map_err(|e| format!("Test setup failed: could not create test repository: {}", e))
451 } 436 }
452 437
453 /// Create an issue (kind 1621) that references a repository 438 /// Create an issue (kind 1621) that references a repository
@@ -458,7 +443,7 @@ impl EventAcceptancePolicyTests {
458 issue_title: &str, 443 issue_title: &str,
459 ) -> Result<Event, String> { 444 ) -> Result<Event, String> {
460 client.create_issue(repo_event, issue_title, "issue content", vec![]) 445 client.create_issue(repo_event, issue_title, "issue content", vec![])
461 .map_err(|e| e.to_string()) 446 .map_err(|e| format!("Test setup failed: could not create test issue: {}", e))
462 } 447 }
463 448
464 /// Create a NIP-22 comment (kind 1111) for an event 449 /// Create a NIP-22 comment (kind 1111) for an event
@@ -469,7 +454,7 @@ impl EventAcceptancePolicyTests {
469 content: &str, 454 content: &str,
470 ) -> Result<Event, String> { 455 ) -> Result<Event, String> {
471 client.create_comment(event, content, vec![]) 456 client.create_comment(event, content, vec![])
472 .map_err(|e| e.to_string()) 457 .map_err(|e| format!("Test setup failed: could not create test comment: {}", e))
473 } 458 }
474 459
475 /// Send event and verify it was accepted (stored by relay) 460 /// Send event and verify it was accepted (stored by relay)
@@ -480,13 +465,14 @@ impl EventAcceptancePolicyTests {
480 ) -> Result<(), String> { 465 ) -> Result<(), String> {
481 let event_id = event.id; 466 let event_id = event.id;
482 467
483 client.send_event(event).await?; 468 client.send_event(event).await
469 .map_err(|e| format!("Failed to send event to relay: {}", e))?;
484 470
485 tokio::time::sleep(Duration::from_millis(100)).await; 471 tokio::time::sleep(Duration::from_millis(100)).await;
486 472
487 let filter = Filter::new().id(event_id); 473 let filter = Filter::new().id(event_id);
488 let events = client.query(filter).await 474 let events = client.query(filter).await
489 .map_err(|e| e.to_string())?; 475 .map_err(|e| format!("Failed to query relay for verification: {}", e))?;
490 476
491 if events.is_empty() { 477 if events.is_empty() {
492 return Err(format!("Event should be accepted: {}", description)); 478 return Err(format!("Event should be accepted: {}", description));
@@ -504,13 +490,13 @@ impl EventAcceptancePolicyTests {
504 let event_id = event.id; 490 let event_id = event.id;
505 491
506 client.send_event(event).await 492 client.send_event(event).await
507 .map_err(|e| e.to_string())?; 493 .map_err(|e| format!("Failed to send event to relay: {}", e))?;
508 494
509 tokio::time::sleep(Duration::from_millis(100)).await; 495 tokio::time::sleep(Duration::from_millis(100)).await;
510 496
511 let filter = Filter::new().id(event_id); 497 let filter = Filter::new().id(event_id);
512 let events = client.query(filter).await 498 let events = client.query(filter).await
513 .map_err(|e| e.to_string())?; 499 .map_err(|e| format!("Failed to query relay for verification: {}", e))?;
514 500
515 if !events.is_empty() { 501 if !events.is_empty() {
516 return Err(format!("Event should be rejected: {}", description)); 502 return Err(format!("Event should be rejected: {}", description));
@@ -524,6 +510,9 @@ impl EventAcceptancePolicyTests {
524 // ============================================================ 510 // ============================================================
525 511
526 /// Test 1.1: Issue referencing repo via `a` tag should be accepted 512 /// Test 1.1: Issue referencing repo via `a` tag should be accepted
513 ///
514 /// **EXAMPLE: Using TestContext for prerequisite events**
515 /// Demonstrates how TestContext simplifies test setup while supporting dual modes
527 async fn test_accept_issue_via_a_tag(client: &AuditClient) -> TestResult { 516 async fn test_accept_issue_via_a_tag(client: &AuditClient) -> TestResult {
528 TestResult::new( 517 TestResult::new(
529 "accept_issue_via_a_tag", 518 "accept_issue_via_a_tag",
@@ -531,11 +520,14 @@ impl EventAcceptancePolicyTests {
531 "Accept issue referencing repo via 'a' tag", 520 "Accept issue referencing repo via 'a' tag",
532 ) 521 )
533 .run(|| async { 522 .run(|| async {
534 // 1. Create and send repo announcement 523 // NEW: Create TestContext
535 let repo = Self::create_test_repo(client, "test-repo-1").await?; 524 let ctx = TestContext::new(client);
536 Self::send_and_verify_accepted(client, repo.clone(), "repository announcement").await?; 525
526 // NEW: Get repository fixture (mode-aware)
527 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await
528 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?;
537 529
538 // 2. Create issue that references the repo (uses create_issue_for_repo helper) 530 // 2. Create issue that references the repo
539 let issue = Self::create_issue_for_repo(client, &repo, "Test Issue 1")?; 531 let issue = Self::create_issue_for_repo(client, &repo, "Test Issue 1")?;
540 532
541 // 3. Send issue and verify it's accepted 533 // 3. Send issue and verify it's accepted
@@ -547,6 +539,10 @@ impl EventAcceptancePolicyTests {
547 } 539 }
548 540
549 /// Test 1.2: NIP-22 comment with root `A` tag referencing repo should be accepted 541 /// Test 1.2: NIP-22 comment with root `A` tag referencing repo should be accepted
542 ///
543 /// **Using TestContext pattern:**
544 /// - In CI mode: Creates fresh repo for full isolation
545 /// - In Production mode: Reuses cached repo to minimize events
550 async fn test_accept_comment_via_A_tag(client: &AuditClient) -> TestResult { 546 async fn test_accept_comment_via_A_tag(client: &AuditClient) -> TestResult {
551 TestResult::new( 547 TestResult::new(
552 "accept_comment_via_A_tag", 548 "accept_comment_via_A_tag",
@@ -554,16 +550,19 @@ impl EventAcceptancePolicyTests {
554 "Accept NIP-22 comment with root 'A' tag referencing repo", 550 "Accept NIP-22 comment with root 'A' tag referencing repo",
555 ) 551 )
556 .run(|| async { 552 .run(|| async {
557 // 1. Create and send repo announcement 553 // Create TestContext
558 let repo = Self::create_test_repo(client, "test-repo-2").await?; 554 let ctx = TestContext::new(client);
559 Self::send_and_verify_accepted(client, repo.clone(), "repository announcement").await?;
560 555
561 // 2. Extract repo_id and create `A` tag manually 556 // Get repository fixture (mode-aware)
557 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await
558 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?;
559
560 // Extract repo_id and create `A` tag manually
562 let repo_id = Self::extract_d_tag(&repo) 561 let repo_id = Self::extract_d_tag(&repo)
563 .ok_or("Failed to extract repo_id from repo event")?; 562 .ok_or("Failed to extract repo_id from repo event")?;
564 let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); 563 let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id);
565 564
566 // 3. Create comment with `A` tag (root reference to repo) 565 // Create comment with `A` tag (root reference to repo)
567 let tags = vec![ 566 let tags = vec![
568 Tag::custom(TagKind::custom("A"), vec![a_tag_value.clone(), "".to_string(), "root".to_string()]), 567 Tag::custom(TagKind::custom("A"), vec![a_tag_value.clone(), "".to_string(), "root".to_string()]),
569 Tag::custom(TagKind::custom("K"), vec!["30617".to_string()]), 568 Tag::custom(TagKind::custom("K"), vec!["30617".to_string()]),
@@ -576,7 +575,7 @@ impl EventAcceptancePolicyTests {
576 .build(client.keys()) 575 .build(client.keys())
577 .map_err(|e| format!("Failed to build comment: {}", e))?; 576 .map_err(|e| format!("Failed to build comment: {}", e))?;
578 577
579 // 4. Send comment and verify it's accepted 578 // Send comment and verify it's accepted
580 Self::send_and_verify_accepted(client, comment, "comment with 'A' tag to repo").await?; 579 Self::send_and_verify_accepted(client, comment, "comment with 'A' tag to repo").await?;
581 580
582 Ok(()) 581 Ok(())
@@ -585,6 +584,10 @@ impl EventAcceptancePolicyTests {
585 } 584 }
586 585
587 /// Test 1.3: Kind 1 text note quoting repo via `q` tag should be accepted 586 /// Test 1.3: Kind 1 text note quoting repo via `q` tag should be accepted
587 ///
588 /// **Using TestContext pattern:**
589 /// - In CI mode: Creates fresh repo for full isolation
590 /// - In Production mode: Reuses cached repo to minimize events
588 async fn test_accept_kind1_via_q_tag(client: &AuditClient) -> TestResult { 591 async fn test_accept_kind1_via_q_tag(client: &AuditClient) -> TestResult {
589 TestResult::new( 592 TestResult::new(
590 "accept_kind1_via_q_tag", 593 "accept_kind1_via_q_tag",
@@ -592,16 +595,19 @@ impl EventAcceptancePolicyTests {
592 "Accept kind 1 note quoting repo via 'q' tag", 595 "Accept kind 1 note quoting repo via 'q' tag",
593 ) 596 )
594 .run(|| async { 597 .run(|| async {
595 // 1. Create and send repo announcement 598 // Create TestContext
596 let repo = Self::create_test_repo(client, "test-repo-3").await?; 599 let ctx = TestContext::new(client);
597 Self::send_and_verify_accepted(client, repo.clone(), "repository announcement").await?; 600
601 // Get repository fixture (mode-aware)
602 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await
603 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?;
598 604
599 // 2. Extract repo_id and create `q` tag 605 // Extract repo_id and create `q` tag
600 let repo_id = Self::extract_d_tag(&repo) 606 let repo_id = Self::extract_d_tag(&repo)
601 .ok_or("Failed to extract repo_id from repo event")?; 607 .ok_or("Failed to extract repo_id from repo event")?;
602 let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); 608 let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id);
603 609
604 // 3. Create kind 1 note with `q` tag (quote reference to repo) 610 // Create kind 1 note with `q` tag (quote reference to repo)
605 let tags = vec![ 611 let tags = vec![
606 Tag::custom(TagKind::custom("q"), vec![a_tag_value]), 612 Tag::custom(TagKind::custom("q"), vec![a_tag_value]),
607 ]; 613 ];
@@ -612,7 +618,7 @@ impl EventAcceptancePolicyTests {
612 .build(client.keys()) 618 .build(client.keys())
613 .map_err(|e| format!("Failed to build note: {}", e))?; 619 .map_err(|e| format!("Failed to build note: {}", e))?;
614 620
615 // 4. Send note and verify it's accepted 621 // Send note and verify it's accepted
616 Self::send_and_verify_accepted(client, note, "kind 1 with 'q' tag to repo").await?; 622 Self::send_and_verify_accepted(client, note, "kind 1 with 'q' tag to repo").await?;
617 623
618 Ok(()) 624 Ok(())
@@ -625,6 +631,10 @@ impl EventAcceptancePolicyTests {
625 // ============================================================ 631 // ============================================================
626 632
627 /// Test 2.1: Issue quoting another accepted issue should be accepted (transitive) 633 /// Test 2.1: Issue quoting another accepted issue should be accepted (transitive)
634 ///
635 /// **Using TestContext pattern:**
636 /// - In CI mode: Creates fresh repo+issue for full isolation
637 /// - In Production mode: Reuses cached repo+issue to minimize events
628 async fn test_accept_issue_quoting_issue_via_q(client: &AuditClient) -> TestResult { 638 async fn test_accept_issue_quoting_issue_via_q(client: &AuditClient) -> TestResult {
629 TestResult::new( 639 TestResult::new(
630 "accept_issue_quoting_issue_via_q", 640 "accept_issue_quoting_issue_via_q",
@@ -632,31 +642,43 @@ impl EventAcceptancePolicyTests {
632 "Accept issue quoting accepted issue (transitive)", 642 "Accept issue quoting accepted issue (transitive)",
633 ) 643 )
634 .run(|| async { 644 .run(|| async {
635 645 // Create TestContext
636 // 1. Create and send Repo A 646 let ctx = TestContext::new(client);
637 let repo_a = Self::create_test_repo(client, "repo-a").await?; 647
638 Self::send_and_verify_accepted(client, repo_a.clone(), "repo A").await?; 648 // Get repo with issue fixture (mode-aware)
639 649 let repo_a = ctx.get_fixture(FixtureKind::RepoWithIssue).await
640 // 2. Create and send Issue A (references repo A, so it's accepted) 650 .map_err(|e| format!("Test setup failed: could not get repo with issue fixture: {}", e))?;
641 let issue_a = Self::create_issue_for_repo(client, &repo_a, "Issue A")?; 651
642 Self::send_and_verify_accepted(client, issue_a.clone(), "issue A").await?; 652 // Extract the issue from the repo_a event (it's stored as the first 'e' tag)
643 653 let issue_a_id = repo_a.tags.iter()
644 // 3. Create Repo B but DON'T send it (unaccepted) - just for creating Issue B 654 .find(|t| t.kind() == TagKind::e())
645 let repo_b = Self::create_test_repo(client, "repo-b").await?; 655 .and_then(|t| t.content())
646 656 .ok_or("Missing issue reference in RepoWithIssue fixture")?;
647 // 4. Create Issue B that: 657
648 // - References unaccepted Repo B (would normally be rejected) 658 // Query to get the actual issue event
649 // - BUT also quotes accepted Issue A via 'q' tag (should make it accepted) 659 let filter = Filter::new().id(
650 let additional_tags = vec![ 660 nostr_sdk::EventId::from_hex(issue_a_id)
651 // Quote to accepted Issue A (this makes it transitive) 661 .map_err(|e| format!("Invalid issue ID: {}", e))?
652 Tag::custom(TagKind::custom("q"), vec![issue_a.id.to_hex()]), 662 );
653 ]; 663 let issues = client.query(filter).await
654 664 .map_err(|e| format!("Failed to query issue: {}", e))?;
655 let issue_b = client 665 let issue_a = issues.first()
656 .create_issue(&repo_b, "Issue B", "issue content", additional_tags) 666 .ok_or("Issue not found")?
657 .map_err(|e| format!("Failed to build issue B: {}", e))?; 667 .clone();
658 668
659 // 5. Send Issue B and verify it's ACCEPTED (via transitive quote to Issue A) 669 // Create Repo B but DON'T send it (unaccepted) - just for creating Issue B
670 let repo_b = Self::create_test_repo(client, "repo-b").await?;
671
672 // Create Issue B that quotes accepted Issue A via 'q' tag (should make it accepted)
673 let additional_tags = vec![
674 Tag::custom(TagKind::custom("q"), vec![issue_a.id.to_hex()]),
675 ];
676
677 let issue_b = client
678 .create_issue(&repo_b, "Issue B", "issue content", additional_tags)
679 .map_err(|e| format!("Failed to build issue B: {}", e))?;
680
681 // Send Issue B and verify it's ACCEPTED (via transitive quote to Issue A)
660 Self::send_and_verify_accepted(client, issue_b, "issue B quoting accepted issue A").await?; 682 Self::send_and_verify_accepted(client, issue_b, "issue B quoting accepted issue A").await?;
661 683
662 Ok(()) 684 Ok(())
@@ -665,6 +687,10 @@ impl EventAcceptancePolicyTests {
665 } 687 }
666 688
667 /// Test 2.2: NIP-22 comment with root 'E' tag to accepted issue should be accepted 689 /// Test 2.2: NIP-22 comment with root 'E' tag to accepted issue should be accepted
690 ///
691 /// **Using TestContext pattern:**
692 /// - In CI mode: Creates fresh repo+issue for full isolation
693 /// - In Production mode: Reuses cached repo+issue to minimize events
668 async fn test_accept_comment_via_E_tag(client: &AuditClient) -> TestResult { 694 async fn test_accept_comment_via_E_tag(client: &AuditClient) -> TestResult {
669 TestResult::new( 695 TestResult::new(
670 "accept_comment_via_E_tag", 696 "accept_comment_via_E_tag",
@@ -672,19 +698,34 @@ impl EventAcceptancePolicyTests {
672 "Accept NIP-22 comment with root 'E' tag to accepted issue", 698 "Accept NIP-22 comment with root 'E' tag to accepted issue",
673 ) 699 )
674 .run(|| async { 700 .run(|| async {
675 701 // Create TestContext
676 // 1. Create and send repo 702 let ctx = TestContext::new(client);
677 let repo = Self::create_test_repo(client, "repo-comment").await?; 703
678 Self::send_and_verify_accepted(client, repo.clone(), "repo").await?; 704 // Get repo with issue fixture (mode-aware)
679 705 let repo = ctx.get_fixture(FixtureKind::RepoWithIssue).await
680 // 2. Create and send issue (references repo, so it's accepted) 706 .map_err(|e| format!("Test setup failed: could not get repo with issue fixture: {}", e))?;
681 let issue = Self::create_issue_for_repo(client, &repo, "Issue for comment")?; 707
682 Self::send_and_verify_accepted(client, issue.clone(), "issue").await?; 708 // Extract the issue from the repo event (it's stored as the first 'e' tag)
683 709 let issue_id = repo.tags.iter()
684 // 3. Create comment using the helper (which adds NIP-22 tags including 'E') 710 .find(|t| t.kind() == TagKind::e())
685 let comment = Self::create_comment_for_event(client, &issue, "Comment content")?; 711 .and_then(|t| t.content())
686 712 .ok_or("Missing issue reference in RepoWithIssue fixture")?;
687 // 4. Send comment and verify it's accepted (via E tag to accepted issue) 713
714 // Query to get the actual issue event
715 let filter = Filter::new().id(
716 nostr_sdk::EventId::from_hex(issue_id)
717 .map_err(|e| format!("Invalid issue ID: {}", e))?
718 );
719 let issues = client.query(filter).await
720 .map_err(|e| format!("Failed to query issue: {}", e))?;
721 let issue = issues.first()
722 .ok_or("Issue not found")?
723 .clone();
724
725 // Create comment using the helper (which adds NIP-22 tags including 'E')
726 let comment = Self::create_comment_for_event(client, &issue, "Comment content")?;
727
728 // Send comment and verify it's accepted (via E tag to accepted issue)
688 Self::send_and_verify_accepted(client, comment, "comment with E tag to accepted issue").await?; 729 Self::send_and_verify_accepted(client, comment, "comment with E tag to accepted issue").await?;
689 730
690 Ok(()) 731 Ok(())
@@ -693,6 +734,10 @@ impl EventAcceptancePolicyTests {
693 } 734 }
694 735
695 /// Test 2.3: Kind 1 note with 'e' tag reply to accepted kind 1 should be accepted 736 /// Test 2.3: Kind 1 note with 'e' tag reply to accepted kind 1 should be accepted
737 ///
738 /// **Using TestContext pattern:**
739 /// - In CI mode: Creates fresh repo for full isolation
740 /// - In Production mode: Reuses cached repo to minimize events
696 async fn test_accept_kind1_via_e_tag(client: &AuditClient) -> TestResult { 741 async fn test_accept_kind1_via_e_tag(client: &AuditClient) -> TestResult {
697 TestResult::new( 742 TestResult::new(
698 "accept_kind1_via_e_tag", 743 "accept_kind1_via_e_tag",
@@ -700,32 +745,34 @@ impl EventAcceptancePolicyTests {
700 "Accept kind 1 reply via 'e' tag to accepted kind 1", 745 "Accept kind 1 reply via 'e' tag to accepted kind 1",
701 ) 746 )
702 .run(|| async { 747 .run(|| async {
703 748 // Create TestContext
704 // 1. Create and send repo 749 let ctx = TestContext::new(client);
705 let repo = Self::create_test_repo(client, "repo-notes").await?; 750
706 Self::send_and_verify_accepted(client, repo.clone(), "repo").await?; 751 // Get repository fixture (mode-aware)
707 752 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await
708 // 2. Create Kind 1 A that quotes the repo (makes it accepted) 753 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?;
709 let repo_id = Self::extract_d_tag(&repo) 754
710 .ok_or("Failed to extract repo_id")?; 755 // Create Kind 1 A that quotes the repo (makes it accepted)
711 let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id); 756 let repo_id = Self::extract_d_tag(&repo)
712 757 .ok_or("Failed to extract repo_id")?;
713 let kind1_a = client 758 let a_tag_value = format!("30617:{}:{}", repo.pubkey, repo_id);
714 .event_builder(Kind::TextNote, "Note A about repo") 759
715 .tags(vec![Tag::custom(TagKind::custom("q"), vec![a_tag_value])]) 760 let kind1_a = client
716 .build(client.keys()) 761 .event_builder(Kind::TextNote, "Note A about repo")
717 .map_err(|e| format!("Failed to build kind1 A: {}", e))?; 762 .tags(vec![Tag::custom(TagKind::custom("q"), vec![a_tag_value])])
718 763 .build(client.keys())
719 Self::send_and_verify_accepted(client, kind1_a.clone(), "kind 1 A quoting repo").await?; 764 .map_err(|e| format!("Failed to build kind1 A: {}", e))?;
720 765
721 // 3. Create Kind 1 B that replies to Kind 1 A via 'e' tag 766 Self::send_and_verify_accepted(client, kind1_a.clone(), "kind 1 A quoting repo").await?;
722 let kind1_b = client 767
723 .event_builder(Kind::TextNote, "Reply to Note A") 768 // Create Kind 1 B that replies to Kind 1 A via 'e' tag
724 .tags(vec![Tag::event(kind1_a.id)]) 769 let kind1_b = client
725 .build(client.keys()) 770 .event_builder(Kind::TextNote, "Reply to Note A")
726 .map_err(|e| format!("Failed to build kind1 B: {}", e))?; 771 .tags(vec![Tag::event(kind1_a.id)])
727 772 .build(client.keys())
728 // 4. Send Kind 1 B and verify it's accepted (via 'e' tag to accepted kind 1 A) 773 .map_err(|e| format!("Failed to build kind1 B: {}", e))?;
774
775 // Send Kind 1 B and verify it's accepted (via 'e' tag to accepted kind 1 A)
729 Self::send_and_verify_accepted(client, kind1_b, "kind 1 B replying to accepted kind 1 A").await?; 776 Self::send_and_verify_accepted(client, kind1_b, "kind 1 B replying to accepted kind 1 A").await?;
730 777
731 Ok(()) 778 Ok(())
@@ -738,6 +785,10 @@ impl EventAcceptancePolicyTests {
738 // ============================================================ 785 // ============================================================
739 786
740 /// Test 3.1: Kind 1 note should be accepted when referenced by an accepted issue (forward ref) 787 /// Test 3.1: Kind 1 note should be accepted when referenced by an accepted issue (forward ref)
788 ///
789 /// **Using TestContext pattern:**
790 /// - In CI mode: Creates fresh repo for full isolation
791 /// - In Production mode: Reuses cached repo to minimize events
741 async fn test_accept_kind1_referenced_in_issue(client: &AuditClient) -> TestResult { 792 async fn test_accept_kind1_referenced_in_issue(client: &AuditClient) -> TestResult {
742 TestResult::new( 793 TestResult::new(
743 "accept_kind1_referenced_in_issue", 794 "accept_kind1_referenced_in_issue",
@@ -745,37 +796,39 @@ impl EventAcceptancePolicyTests {
745 "Accept kind 1 referenced in accepted issue (forward ref)", 796 "Accept kind 1 referenced in accepted issue (forward ref)",
746 ) 797 )
747 .run(|| async { 798 .run(|| async {
748 799 // Create TestContext
749 // 1. Create and send repo (this establishes the accepted context) 800 let ctx = TestContext::new(client);
750 let repo = Self::create_test_repo(client, "repo-fwd-1").await?; 801
751 Self::send_and_verify_accepted(client, repo.clone(), "repo").await?; 802 // Get repository fixture (mode-aware)
752 803 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await
753 // 2. Create Kind 1 note locally but DON'T send it yet 804 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?;
754 let kind1_note = client 805
755 .event_builder(Kind::TextNote, "Note to be referenced") 806 // Create Kind 1 note locally but DON'T send it yet
756 .build(client.keys()) 807 let kind1_note = client
757 .map_err(|e| format!("Failed to build kind1: {}", e))?; 808 .event_builder(Kind::TextNote, "Note to be referenced")
758 809 .build(client.keys())
759 // 3. Create and send issue that QUOTES the unsent Kind 1 note 810 .map_err(|e| format!("Failed to build kind1: {}", e))?;
760 let issue_tags = vec![ 811
761 // Reference to accepted repo 812 // Create and send issue that QUOTES the unsent Kind 1 note
762 Tag::custom(TagKind::custom("a"), vec![ 813 let issue_tags = vec![
763 format!("30617:{}:{}", repo.pubkey, Self::extract_d_tag(&repo).unwrap()) 814 // Reference to accepted repo
764 ]), 815 Tag::custom(TagKind::custom("a"), vec![
765 Tag::custom(TagKind::custom("subject"), vec!["Issue referencing kind1".to_string()]), 816 format!("30617:{}:{}", repo.pubkey, Self::extract_d_tag(&repo).unwrap())
766 // Quote the Kind 1 that hasn't been sent yet 817 ]),
767 Tag::custom(TagKind::custom("q"), vec![kind1_note.id.to_hex()]), 818 Tag::custom(TagKind::custom("subject"), vec!["Issue referencing kind1".to_string()]),
768 ]; 819 // Quote the Kind 1 that hasn't been sent yet
769 820 Tag::custom(TagKind::custom("q"), vec![kind1_note.id.to_hex()]),
770 let issue = client 821 ];
771 .event_builder(Kind::Custom(1621), "issue content") 822
772 .tags(issue_tags) 823 let issue = client
773 .build(client.keys()) 824 .event_builder(Kind::Custom(1621), "issue content")
774 .map_err(|e| format!("Failed to build issue: {}", e))?; 825 .tags(issue_tags)
775 826 .build(client.keys())
776 Self::send_and_verify_accepted(client, issue, "issue quoting unsent kind1").await?; 827 .map_err(|e| format!("Failed to build issue: {}", e))?;
777 828
778 // 4. NOW send the Kind 1 note - should be accepted because accepted issue quotes it 829 Self::send_and_verify_accepted(client, issue, "issue quoting unsent kind1").await?;
830
831 // NOW send the Kind 1 note - should be accepted because accepted issue quotes it
779 Self::send_and_verify_accepted(client, kind1_note, "kind1 note referenced by accepted issue").await?; 832 Self::send_and_verify_accepted(client, kind1_note, "kind1 note referenced by accepted issue").await?;
780 833
781 Ok(()) 834 Ok(())
@@ -784,6 +837,10 @@ impl EventAcceptancePolicyTests {
784 } 837 }
785 838
786 /// Test 3.2: Comment should be accepted when referenced by another accepted comment (forward ref) 839 /// Test 3.2: Comment should be accepted when referenced by another accepted comment (forward ref)
840 ///
841 /// **Using TestContext pattern:**
842 /// - In CI mode: Creates fresh repo+issue for full isolation
843 /// - In Production mode: Reuses cached repo+issue to minimize events
787 async fn test_accept_comment_referenced_in_comment(client: &AuditClient) -> TestResult { 844 async fn test_accept_comment_referenced_in_comment(client: &AuditClient) -> TestResult {
788 TestResult::new( 845 TestResult::new(
789 "accept_comment_referenced_in_comment", 846 "accept_comment_referenced_in_comment",
@@ -791,38 +848,53 @@ impl EventAcceptancePolicyTests {
791 "Accept comment referenced in another accepted comment (forward ref)", 848 "Accept comment referenced in another accepted comment (forward ref)",
792 ) 849 )
793 .run(|| async { 850 .run(|| async {
794 851 // Create TestContext
795 // 1. Create and send repo 852 let ctx = TestContext::new(client);
796 let repo = Self::create_test_repo(client, "repo-fwd-2").await?; 853
797 Self::send_and_verify_accepted(client, repo.clone(), "repo").await?; 854 // Get repo with issue fixture (mode-aware)
798 855 let repo = ctx.get_fixture(FixtureKind::RepoWithIssue).await
799 // 2. Create and send issue (references repo, so it's accepted) 856 .map_err(|e| format!("Test setup failed: could not get repo with issue fixture: {}", e))?;
800 let issue = Self::create_issue_for_repo(client, &repo, "Issue for comments")?; 857
801 Self::send_and_verify_accepted(client, issue.clone(), "issue").await?; 858 // Extract the issue from the repo event (it's stored as the first 'e' tag)
802 859 let issue_id = repo.tags.iter()
803 // 3. Create Comment A locally but DON'T send it yet 860 .find(|t| t.kind() == TagKind::e())
804 let comment_a = Self::create_comment_for_event(client, &issue, "Comment A")?; 861 .and_then(|t| t.content())
805 862 .ok_or("Missing issue reference in RepoWithIssue fixture")?;
806 // 4. Create and send Comment B that quotes Comment A (which hasn't been sent) 863
807 let comment_b_tags = vec![ 864 // Query to get the actual issue event
808 // NIP-22 tags for the original issue 865 let filter = Filter::new().id(
809 Tag::custom(TagKind::custom("E"), vec![issue.id.to_hex(), "".to_string(), "root".to_string()]), 866 nostr_sdk::EventId::from_hex(issue_id)
810 Tag::event(issue.id), 867 .map_err(|e| format!("Invalid issue ID: {}", e))?
811 Tag::custom(TagKind::custom("K"), vec![issue.kind.as_u16().to_string()]), 868 );
812 Tag::public_key(issue.pubkey), 869 let issues = client.query(filter).await
813 // Quote Comment A which hasn't been sent yet 870 .map_err(|e| format!("Failed to query issue: {}", e))?;
814 Tag::custom(TagKind::custom("q"), vec![comment_a.id.to_hex()]), 871 let issue = issues.first()
815 ]; 872 .ok_or("Issue not found")?
816 873 .clone();
817 let comment_b = client 874
818 .event_builder(Kind::Custom(1111), "Comment B quoting Comment A") 875 // Create Comment A locally but DON'T send it yet
819 .tags(comment_b_tags) 876 let comment_a = Self::create_comment_for_event(client, &issue, "Comment A")?;
820 .build(client.keys()) 877
821 .map_err(|e| format!("Failed to build comment B: {}", e))?; 878 // Create and send Comment B that quotes Comment A (which hasn't been sent)
822 879 let comment_b_tags = vec![
823 Self::send_and_verify_accepted(client, comment_b, "comment B quoting unsent comment A").await?; 880 // NIP-22 tags for the original issue
824 881 Tag::custom(TagKind::custom("E"), vec![issue.id.to_hex(), "".to_string(), "root".to_string()]),
825 // 5. NOW send Comment A - should be accepted because accepted Comment B quotes it 882 Tag::event(issue.id),
883 Tag::custom(TagKind::custom("K"), vec![issue.kind.as_u16().to_string()]),
884 Tag::public_key(issue.pubkey),
885 // Quote Comment A which hasn't been sent yet
886 Tag::custom(TagKind::custom("q"), vec![comment_a.id.to_hex()]),
887 ];
888
889 let comment_b = client
890 .event_builder(Kind::Custom(1111), "Comment B quoting Comment A")
891 .tags(comment_b_tags)
892 .build(client.keys())
893 .map_err(|e| format!("Failed to build comment B: {}", e))?;
894
895 Self::send_and_verify_accepted(client, comment_b, "comment B quoting unsent comment A").await?;
896
897 // NOW send Comment A - should be accepted because accepted Comment B quotes it
826 Self::send_and_verify_accepted(client, comment_a, "comment A referenced by accepted comment B").await?; 898 Self::send_and_verify_accepted(client, comment_a, "comment A referenced by accepted comment B").await?;
827 899
828 Ok(()) 900 Ok(())
@@ -831,6 +903,10 @@ impl EventAcceptancePolicyTests {
831 } 903 }
832 904
833 /// Test 3.3: Kind 1 note should be accepted when referenced by another accepted kind 1 (forward ref) 905 /// Test 3.3: Kind 1 note should be accepted when referenced by another accepted kind 1 (forward ref)
906 ///
907 /// **Using TestContext pattern:**
908 /// - In CI mode: Creates fresh repo for full isolation
909 /// - In Production mode: Reuses cached repo to minimize events
834 async fn test_accept_kind1_referenced_in_kind1(client: &AuditClient) -> TestResult { 910 async fn test_accept_kind1_referenced_in_kind1(client: &AuditClient) -> TestResult {
835 TestResult::new( 911 TestResult::new(
836 "accept_kind1_referenced_in_kind1", 912 "accept_kind1_referenced_in_kind1",
@@ -838,17 +914,20 @@ impl EventAcceptancePolicyTests {
838 "Accept kind 1 referenced in another accepted kind 1 (forward ref)", 914 "Accept kind 1 referenced in another accepted kind 1 (forward ref)",
839 ) 915 )
840 .run(|| async { 916 .run(|| async {
841 // 1. Create and send repo 917 // Create TestContext
842 let repo = Self::create_test_repo(client, "repo-fwd-3").await?; 918 let ctx = TestContext::new(client);
843 Self::send_and_verify_accepted(client, repo.clone(), "repo").await?;
844 919
845 // 2. Create Kind 1 A locally but DON'T send it yet 920 // Get repository fixture (mode-aware)
921 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await
922 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?;
923
924 // Create Kind 1 A locally but DON'T send it yet
846 let kind1_a = client 925 let kind1_a = client
847 .event_builder(Kind::TextNote, "Note A to be referenced") 926 .event_builder(Kind::TextNote, "Note A to be referenced")
848 .build(client.keys()) 927 .build(client.keys())
849 .map_err(|e| format!("Failed to build kind1 A: {}", e))?; 928 .map_err(|e| format!("Failed to build kind1 A: {}", e))?;
850 929
851 // 3. Create and send Kind 1 B that: 930 // Create and send Kind 1 B that:
852 // - Quotes the repo (makes it accepted) 931 // - Quotes the repo (makes it accepted)
853 // - Mentions Kind 1 A via 'e' tag (which hasn't been sent yet) 932 // - Mentions Kind 1 A via 'e' tag (which hasn't been sent yet)
854 let repo_id = Self::extract_d_tag(&repo) 933 let repo_id = Self::extract_d_tag(&repo)
@@ -866,7 +945,7 @@ impl EventAcceptancePolicyTests {
866 945
867 Self::send_and_verify_accepted(client, kind1_b, "kind1 B mentioning unsent kind1 A").await?; 946 Self::send_and_verify_accepted(client, kind1_b, "kind1 B mentioning unsent kind1 A").await?;
868 947
869 // 4. NOW send Kind 1 A - should be accepted because accepted Kind 1 B mentions it 948 // NOW send Kind 1 A - should be accepted because accepted Kind 1 B mentions it
870 Self::send_and_verify_accepted(client, kind1_a, "kind1 A referenced by accepted kind1 B").await?; 949 Self::send_and_verify_accepted(client, kind1_a, "kind1 A referenced by accepted kind1 B").await?;
871 950
872 Ok(()) 951 Ok(())
@@ -923,6 +1002,11 @@ impl EventAcceptancePolicyTests {
923 } 1002 }
924 1003
925 /// Test 4.3: Comment quoting unaccepted repo should be rejected 1004 /// Test 4.3: Comment quoting unaccepted repo should be rejected
1005 ///
1006 /// **Using TestContext pattern:**
1007 /// - In CI mode: Creates fresh accepted repo for full isolation
1008 /// - In Production mode: Reuses cached accepted repo to minimize events
1009 /// - Note: Unaccepted repo B is always created fresh (not cached) since it must remain unaccepted
926 async fn test_reject_comment_quoting_other_repo(client: &AuditClient) -> TestResult { 1010 async fn test_reject_comment_quoting_other_repo(client: &AuditClient) -> TestResult {
927 TestResult::new( 1011 TestResult::new(
928 "reject_comment_quoting_other_repo", 1012 "reject_comment_quoting_other_repo",
@@ -930,19 +1014,22 @@ impl EventAcceptancePolicyTests {
930 "Reject comment quoting unaccepted repo", 1014 "Reject comment quoting unaccepted repo",
931 ) 1015 )
932 .run(|| async { 1016 .run(|| async {
933 // 1. Create and send Repo A (this one IS accepted) 1017 // Create TestContext
934 let repo_a = Self::create_test_repo(client, "accepted-repo-a").await?; 1018 let ctx = TestContext::new(client);
935 Self::send_and_verify_accepted(client, repo_a.clone(), "repo A").await?; 1019
1020 // Get accepted repo A fixture (mode-aware)
1021 let _repo_a = ctx.get_fixture(FixtureKind::ValidRepo).await
1022 .map_err(|e| format!("Test setup failed: could not get valid repository fixture: {}", e))?;
936 1023
937 // 2. Create Repo B but DON'T send it (unaccepted) 1024 // Create Repo B but DON'T send it (unaccepted)
938 let repo_b = Self::create_test_repo(client, "unaccepted-repo-b").await?; 1025 let repo_b = Self::create_test_repo(client, "unaccepted-repo-b").await?;
939 1026
940 // 3. Extract repo_b info and create comment that quotes repo B (not repo A) 1027 // Extract repo_b info and create comment that quotes repo B (not repo A)
941 let repo_b_id = Self::extract_d_tag(&repo_b) 1028 let repo_b_id = Self::extract_d_tag(&repo_b)
942 .ok_or("Failed to extract repo_b id")?; 1029 .ok_or("Failed to extract repo_b id")?;
943 let repo_b_a_tag = format!("30617:{}:{}", repo_b.pubkey, repo_b_id); 1030 let repo_b_a_tag = format!("30617:{}:{}", repo_b.pubkey, repo_b_id);
944 1031
945 // 4. Create comment that references ONLY repo B (unaccepted) 1032 // Create comment that references ONLY repo B (unaccepted)
946 let tags = vec![ 1033 let tags = vec![
947 Tag::custom(TagKind::custom("A"), vec![repo_b_a_tag, "".to_string(), "root".to_string()]), 1034 Tag::custom(TagKind::custom("A"), vec![repo_b_a_tag, "".to_string(), "root".to_string()]),
948 Tag::custom(TagKind::custom("K"), vec!["30617".to_string()]), 1035 Tag::custom(TagKind::custom("K"), vec!["30617".to_string()]),
@@ -955,7 +1042,7 @@ impl EventAcceptancePolicyTests {
955 .build(client.keys()) 1042 .build(client.keys())
956 .map_err(|e| format!("Failed to build comment: {}", e))?; 1043 .map_err(|e| format!("Failed to build comment: {}", e))?;
957 1044
958 // 5. Send comment and verify it's REJECTED (only references unaccepted repo B) 1045 // Send comment and verify it's REJECTED (only references unaccepted repo B)
959 Self::send_and_verify_rejected(client, comment, "comment quoting only unaccepted repo").await?; 1046 Self::send_and_verify_rejected(client, comment, "comment quoting only unaccepted repo").await?;
960 1047
961 Ok(()) 1048 Ok(())