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-28 04:27:07 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-28 04:27:07 +0000
commiteee0a521afe0492f04bee58c9a236683fb23601b (patch)
treeb0414e164de2946e39613a45f5a2724eadfe1821 /grasp-audit/src
parentb2a43ffbd965a2b561d7b4e4c582dfeadd099ed3 (diff)
audit: fix shared test context to minimise events sent to production relays
Diffstat (limited to 'grasp-audit/src')
-rw-r--r--grasp-audit/src/bin/grasp-audit.rs42
-rw-r--r--grasp-audit/src/client.rs26
-rw-r--r--grasp-audit/src/fixtures.rs109
3 files changed, 119 insertions, 58 deletions
diff --git a/grasp-audit/src/bin/grasp-audit.rs b/grasp-audit/src/bin/grasp-audit.rs
index 29f9c9a..2aabefe 100644
--- a/grasp-audit/src/bin/grasp-audit.rs
+++ b/grasp-audit/src/bin/grasp-audit.rs
@@ -120,7 +120,27 @@ async fn main() -> Result<()> {
120 "all" => { 120 "all" => {
121 println!("Running all tests...\n"); 121 println!("Running all tests...\n");
122 let mut all_results = AuditResult::new("All GRASP-01 Tests"); 122 let mut all_results = AuditResult::new("All GRASP-01 Tests");
123 123
124 // Repository creation tests
125 println!(" → Repository creation tests...");
126 let repo_results = specs::RepositoryCreationTests::run_all(&client, &relay_domain).await;
127 all_results.merge(repo_results);
128
129 // Git clone tests
130 println!(" → Git clone tests...");
131 let clone_results = specs::GitCloneTests::run_all(&client, &relay_domain).await;
132 all_results.merge(clone_results);
133
134 // Push authorization tests
135 println!(" → Push authorization tests...");
136 let push_results = specs::PushAuthorizationTests::run_all(&client, &relay_domain).await;
137 all_results.merge(push_results);
138
139 // Event acceptance policy tests
140 println!(" → Event acceptance policy tests...");
141 let event_results = specs::EventAcceptancePolicyTests::run_all(&client).await;
142 all_results.merge(event_results);
143
124 // NIP-01 smoke tests 144 // NIP-01 smoke tests
125 println!(" → NIP-01 smoke tests..."); 145 println!(" → NIP-01 smoke tests...");
126 let nip01_results = specs::Nip01SmokeTests::run_all(&client).await; 146 let nip01_results = specs::Nip01SmokeTests::run_all(&client).await;
@@ -131,30 +151,10 @@ async fn main() -> Result<()> {
131 let nip11_results = specs::Nip11DocumentTests::run_all(&client).await; 151 let nip11_results = specs::Nip11DocumentTests::run_all(&client).await;
132 all_results.merge(nip11_results); 152 all_results.merge(nip11_results);
133 153
134 // Event acceptance policy tests
135 println!(" → Event acceptance policy tests...");
136 let event_results = specs::EventAcceptancePolicyTests::run_all(&client).await;
137 all_results.merge(event_results);
138
139 // CORS tests 154 // CORS tests
140 println!(" → CORS tests..."); 155 println!(" → CORS tests...");
141 let cors_results = specs::CorsTests::run_all(&client, &relay_domain).await; 156 let cors_results = specs::CorsTests::run_all(&client, &relay_domain).await;
142 all_results.merge(cors_results); 157 all_results.merge(cors_results);
143
144 // Git clone tests
145 println!(" → Git clone tests...");
146 let clone_results = specs::GitCloneTests::run_all(&client, &relay_domain).await;
147 all_results.merge(clone_results);
148
149 // Push authorization tests
150 println!(" → Push authorization tests...");
151 let push_results = specs::PushAuthorizationTests::run_all(&client, &relay_domain).await;
152 all_results.merge(push_results);
153
154 // Repository creation tests
155 println!(" → Repository creation tests...");
156 let repo_results = specs::RepositoryCreationTests::run_all(&client, &relay_domain).await;
157 all_results.merge(repo_results);
158 158
159 println!(); 159 println!();
160 all_results 160 all_results
diff --git a/grasp-audit/src/client.rs b/grasp-audit/src/client.rs
index 8b96f4f..4f3401c 100644
--- a/grasp-audit/src/client.rs
+++ b/grasp-audit/src/client.rs
@@ -1,11 +1,22 @@
1//! Audit client for testing GRASP implementations 1//! Audit client for testing GRASP implementations
2 2
3use crate::audit::{AuditConfig, AuditEventBuilder, AuditMode}; 3use crate::audit::{AuditConfig, AuditEventBuilder, AuditMode};
4use crate::fixtures::FixtureKind;
4use anyhow::{anyhow, Result}; 5use anyhow::{anyhow, Result};
5use nostr_sdk::prelude::*; 6use nostr_sdk::prelude::*;
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
6use std::time::Duration; 9use std::time::Duration;
7 10
11/// Type alias for the fixture cache - shared across TestContext instances
12pub type FixtureCache = Arc<Mutex<HashMap<FixtureKind, Event>>>;
13
8/// Client for auditing GRASP implementations 14/// Client for auditing GRASP implementations
15///
16/// The AuditClient owns a fixture cache that is shared across all TestContext
17/// instances created from this client. This provides natural cache sharing:
18/// - CLI creates one AuditClient → fixtures shared across all tests
19/// - cargo test creates one AuditClient per test → fixtures isolated per test
9pub struct AuditClient { 20pub struct AuditClient {
10 client: Client, 21 client: Client,
11 pub config: AuditConfig, 22 pub config: AuditConfig,
@@ -14,6 +25,8 @@ pub struct AuditClient {
14 maintainer_keys: Keys, 25 maintainer_keys: Keys,
15 /// Recursive maintainer keys for testing recursive authorization scenarios 26 /// Recursive maintainer keys for testing recursive authorization scenarios
16 recursive_maintainer_keys: Keys, 27 recursive_maintainer_keys: Keys,
28 /// Fixture cache for TestContext instances - shared across all contexts using this client
29 fixture_cache: FixtureCache,
17} 30}
18 31
19impl AuditClient { 32impl AuditClient {
@@ -30,6 +43,7 @@ impl AuditClient {
30 keys, 43 keys,
31 maintainer_keys, 44 maintainer_keys,
32 recursive_maintainer_keys, 45 recursive_maintainer_keys,
46 fixture_cache: Arc::new(Mutex::new(HashMap::new())),
33 } 47 }
34 } 48 }
35 49
@@ -88,9 +102,19 @@ impl AuditClient {
88 keys, 102 keys,
89 maintainer_keys, 103 maintainer_keys,
90 recursive_maintainer_keys, 104 recursive_maintainer_keys,
105 fixture_cache: Arc::new(Mutex::new(HashMap::new())),
91 }) 106 })
92 } 107 }
93 108
109 /// Get the fixture cache for TestContext usage
110 ///
111 /// This cache is shared across all TestContext instances created from this client.
112 /// In CLI mode (one client for all tests), fixtures are reused.
113 /// In test mode (one client per test), fixtures are isolated.
114 pub fn fixture_cache(&self) -> &FixtureCache {
115 &self.fixture_cache
116 }
117
94 /// Get the public key for this audit client 118 /// Get the public key for this audit client
95 pub fn public_key(&self) -> PublicKey { 119 pub fn public_key(&self) -> PublicKey {
96 self.keys.public_key() 120 self.keys.public_key()
@@ -488,6 +512,7 @@ mod tests {
488 keys: keys.clone(), 512 keys: keys.clone(),
489 maintainer_keys, 513 maintainer_keys,
490 recursive_maintainer_keys, 514 recursive_maintainer_keys,
515 fixture_cache: Arc::new(Mutex::new(HashMap::new())),
491 }; 516 };
492 517
493 let _builder = client.event_builder(Kind::TextNote, "test content"); 518 let _builder = client.event_builder(Kind::TextNote, "test content");
@@ -508,6 +533,7 @@ mod tests {
508 keys: keys.clone(), 533 keys: keys.clone(),
509 maintainer_keys, 534 maintainer_keys,
510 recursive_maintainer_keys, 535 recursive_maintainer_keys,
536 fixture_cache: Arc::new(Mutex::new(HashMap::new())),
511 }; 537 };
512 538
513 // Create an event with a custom tag 539 // Create an event with a custom tag
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs
index 89531c6..81620f6 100644
--- a/grasp-audit/src/fixtures.rs
+++ b/grasp-audit/src/fixtures.rs
@@ -6,6 +6,17 @@
6//! - **CI Mode (Isolated)**: Creates fresh events for each test, ensuring complete isolation 6//! - **CI Mode (Isolated)**: Creates fresh events for each test, ensuring complete isolation
7//! - **Production Mode (Shared)**: Reuses shared fixtures to minimize event publication 7//! - **Production Mode (Shared)**: Reuses shared fixtures to minimize event publication
8//! 8//!
9//! # Cache Sharing Strategy
10//!
11//! The fixture cache lives on the `AuditClient`, not on `TestContext`. This provides
12//! natural cache sharing semantics:
13//!
14//! - **CLI mode**: Creates one `AuditClient` → fixtures shared across all tests
15//! - **cargo test**: Creates one `AuditClient` per test → fixtures isolated per test
16//!
17//! This eliminates the need for global state while still enabling fixture reuse
18//! when appropriate.
19//!
9//! # Example 20//! # Example
10//! 21//!
11//! ```no_run 22//! ```no_run
@@ -25,8 +36,6 @@
25use crate::{AuditClient, AuditMode}; 36use crate::{AuditClient, AuditMode};
26use anyhow::{Context, Result}; 37use anyhow::{Context, Result};
27use nostr_sdk::prelude::Event; 38use nostr_sdk::prelude::Event;
28use std::collections::HashMap;
29use std::sync::{Arc, Mutex};
30 39
31/// Deterministic commit hash used in RepoState fixtures (Owner variant) 40/// Deterministic commit hash used in RepoState fixtures (Owner variant)
32/// This is the hash produced by creating a commit with: 41/// This is the hash produced by creating a commit with:
@@ -161,6 +170,13 @@ impl From<AuditMode> for ContextMode {
161/// - In Isolated mode: Creates fresh events for each test 170/// - In Isolated mode: Creates fresh events for each test
162/// - In Shared mode: Caches and reuses events across tests 171/// - In Shared mode: Caches and reuses events across tests
163/// 172///
173/// # Cache Location
174///
175/// The fixture cache lives on `AuditClient`, not on `TestContext`. This means:
176/// - Multiple `TestContext` instances from the same client share the cache
177/// - CLI mode (one client) naturally shares fixtures across all tests
178/// - Test mode (one client per test) naturally isolates fixtures
179///
164/// # Example 180/// # Example
165/// 181///
166/// ```no_run 182/// ```no_run
@@ -181,20 +197,18 @@ impl From<AuditMode> for ContextMode {
181pub struct TestContext<'a> { 197pub struct TestContext<'a> {
182 client: &'a AuditClient, 198 client: &'a AuditClient,
183 mode: ContextMode, 199 mode: ContextMode,
184 cache: Arc<Mutex<HashMap<FixtureKind, Event>>>,
185} 200}
186 201
187impl<'a> TestContext<'a> { 202impl<'a> TestContext<'a> {
188 /// Create a new test context 203 /// Create a new test context
189 /// 204 ///
190 /// The context mode is automatically determined from the client's audit config. 205 /// The context mode is automatically determined from the client's audit config.
206 /// The fixture cache is borrowed from the client, enabling natural sharing:
207 /// - Same client = shared cache (CLI mode behavior)
208 /// - Different clients = isolated caches (test mode behavior)
191 pub fn new(client: &'a AuditClient) -> Self { 209 pub fn new(client: &'a AuditClient) -> Self {
192 let mode = ContextMode::from(client.config.mode); 210 let mode = ContextMode::from(client.config.mode);
193 Self { 211 Self { client, mode }
194 client,
195 mode,
196 cache: Arc::new(Mutex::new(HashMap::new())),
197 }
198 } 212 }
199 213
200 /// Create a test context with explicit mode override 214 /// Create a test context with explicit mode override
@@ -202,11 +216,7 @@ impl<'a> TestContext<'a> {
202 /// This is useful for testing the context itself or for advanced use cases 216 /// This is useful for testing the context itself or for advanced use cases
203 /// where you want to override the default mode behavior. 217 /// where you want to override the default mode behavior.
204 pub fn with_mode(client: &'a AuditClient, mode: ContextMode) -> Self { 218 pub fn with_mode(client: &'a AuditClient, mode: ContextMode) -> Self {
205 Self { 219 Self { client, mode }
206 client,
207 mode,
208 cache: Arc::new(Mutex::new(HashMap::new())),
209 }
210 } 220 }
211 221
212 /// Get a fixture, creating it if needed based on mode 222 /// Get a fixture, creating it if needed based on mode
@@ -261,15 +271,28 @@ impl<'a> TestContext<'a> {
261 } 271 }
262 272
263 /// Get or create a shared fixture (caches for reuse) 273 /// Get or create a shared fixture (caches for reuse)
274 ///
275 /// Uses the client's fixture cache to ensure fixtures are reused across
276 /// all TestContext instances in Production mode.
264 async fn get_or_create_shared(&self, kind: FixtureKind) -> Result<Event> { 277 async fn get_or_create_shared(&self, kind: FixtureKind) -> Result<Event> {
265 // Check cache first 278 // Check client's cache first (shared across all TestContext instances using same client)
266 { 279 {
267 let cache = self.cache.lock().unwrap(); 280 let cache = self.client.fixture_cache().lock().unwrap();
268 if let Some(event) = cache.get(&kind) { 281 if let Some(event) = cache.get(&kind) {
282 tracing::debug!("get_or_create_shared({:?}) found in client cache", kind);
269 return Ok(event.clone()); 283 return Ok(event.clone());
270 } 284 }
271 } 285 }
272 286
287 // Check relay connection before attempting to build
288 let is_connected = self.client.is_connected().await;
289 if !is_connected {
290 return Err(anyhow::anyhow!(
291 "Relay connection lost before building {:?} fixture (shared cache mode)",
292 kind
293 ));
294 }
295
273 // Not in cache, create it 296 // Not in cache, create it
274 let event = self 297 let event = self
275 .build_fixture(kind) 298 .build_fixture(kind)
@@ -286,64 +309,76 @@ impl<'a> TestContext<'a> {
286 ) 309 )
287 })?; 310 })?;
288 311
289 // Store in cache 312 // Store in client's cache (shared across all TestContext instances using same client)
290 { 313 {
291 let mut cache = self.cache.lock().unwrap(); 314 let mut cache = self.client.fixture_cache().lock().unwrap();
292 cache.insert(kind, event.clone()); 315 cache.insert(kind, event.clone());
316 tracing::debug!("get_or_create_shared({:?}) stored in client cache ({} entries)", kind, cache.len());
293 } 317 }
294 318
295 Ok(event) 319 Ok(event)
296 } 320 }
297 321
298 /// Get or create a ValidRepo, with caching within the TestContext. 322 /// Get or create a ValidRepo, with caching.
299 /// This is a helper method that avoids async recursion by not going 323 /// This is a helper method that avoids async recursion by not going
300 /// through get_fixture. It handles the repo specifically. 324 /// through get_fixture. It handles the repo specifically.
301 /// 325 ///
302 /// IMPORTANT: We always cache within a TestContext instance to ensure 326 /// Uses client's fixture_cache for caching - in Shared mode this enables
303 /// fixture dependencies work correctly. The isolation between tests 327 /// cross-test reuse when the same client is used.
304 /// comes from each test having its own TestContext with a fresh cache.
305 async fn get_or_create_repo(&self) -> Result<Event> { 328 async fn get_or_create_repo(&self) -> Result<Event> {
306 // Always check cache first - this ensures fixture dependencies work 329 // Check client's cache first (shared across all TestContext instances using same client)
307 // (e.g., MaintainerRepoAndState needs the SAME repo_id as RepoState)
308 { 330 {
309 let cache = self.cache.lock().unwrap(); 331 let cache = self.client.fixture_cache().lock().unwrap();
310 if let Some(event) = cache.get(&FixtureKind::ValidRepo) { 332 if let Some(event) = cache.get(&FixtureKind::ValidRepo) {
333 tracing::debug!("get_or_create_repo() found in client cache");
311 return Ok(event.clone()); 334 return Ok(event.clone());
312 } 335 }
313 } 336 }
314 337
338 // Check relay connection before creating repo
339 let is_connected = self.client.is_connected().await;
340 if !is_connected {
341 return Err(anyhow::anyhow!(
342 "Relay connection lost before creating ValidRepo fixture"
343 ));
344 }
345
315 // Create a new repo 346 // Create a new repo
316 let test_name = format!( 347 let test_name = format!(
317 "fixture-{:?}-{}", 348 "fixture-{:?}-{}",
318 FixtureKind::ValidRepo, 349 FixtureKind::ValidRepo,
319 &uuid::Uuid::new_v4().to_string()[..8] 350 &uuid::Uuid::new_v4().to_string()[..8]
320 ); 351 );
321 let repo = self.client.create_repo_announcement(&test_name).await?; 352
353 let repo = self.client.create_repo_announcement(&test_name).await
354 .with_context(|| format!("create_repo_announcement failed for {}", test_name))?;
322 355
323 // Send it 356 // Send it
324 self.client.send_event(repo.clone()).await?; 357 self.client.send_event(repo.clone()).await
358 .with_context(|| "Failed to send repo announcement to relay")?;
325 359
326 // Always cache it - isolation comes from each test having its own TestContext 360 // Store in client's cache (shared across all TestContext instances using same client)
327 { 361 {
328 let mut cache = self.cache.lock().unwrap(); 362 let mut cache = self.client.fixture_cache().lock().unwrap();
329 cache.insert(FixtureKind::ValidRepo, repo.clone()); 363 cache.insert(FixtureKind::ValidRepo, repo.clone());
364 tracing::debug!("get_or_create_repo() stored in client cache ({} entries)", cache.len());
330 } 365 }
331 366
332 Ok(repo) 367 Ok(repo)
333 } 368 }
334 369
335 /// Get or create a RepoWithIssue, with caching within the TestContext. 370 /// Get or create a RepoWithIssue, with caching via the client.
336 /// Returns the issue event (repo is already sent/cached via get_or_create_repo). 371 /// Returns the issue event (repo is already sent/cached via get_or_create_repo).
337 async fn get_or_create_issue(&self) -> Result<Event> { 372 async fn get_or_create_issue(&self) -> Result<Event> {
338 // Always check cache first - ensures fixture dependencies work 373 // Check client's cache first
339 { 374 {
340 let cache = self.cache.lock().unwrap(); 375 let cache = self.client.fixture_cache().lock().unwrap();
341 if let Some(event) = cache.get(&FixtureKind::RepoWithIssue) { 376 if let Some(event) = cache.get(&FixtureKind::RepoWithIssue) {
342 return Ok(event.clone()); 377 return Ok(event.clone());
343 } 378 }
344 } 379 }
345 380
346 // Get or create repo (reuses cached within this TestContext) 381 // Get or create repo (reuses cached via client)
347 let repo = self.get_or_create_repo().await?; 382 let repo = self.get_or_create_repo().await?;
348 383
349 // Create the issue 384 // Create the issue
@@ -357,9 +392,9 @@ impl<'a> TestContext<'a> {
357 // Send it 392 // Send it
358 self.client.send_event(issue.clone()).await?; 393 self.client.send_event(issue.clone()).await?;
359 394
360 // Always cache it - isolation comes from each test having its own TestContext 395 // Store in client's cache
361 { 396 {
362 let mut cache = self.cache.lock().unwrap(); 397 let mut cache = self.client.fixture_cache().lock().unwrap();
363 cache.insert(FixtureKind::RepoWithIssue, issue.clone()); 398 cache.insert(FixtureKind::RepoWithIssue, issue.clone());
364 } 399 }
365 400
@@ -707,10 +742,10 @@ impl<'a> TestContext<'a> {
707 742
708 /// Clear the fixture cache 743 /// Clear the fixture cache
709 /// 744 ///
710 /// This is useful for tests that want to ensure fresh fixtures 745 /// This clears the client's fixture cache, affecting all TestContext
711 /// even in shared mode. 746 /// instances using the same client.
712 pub fn clear_cache(&self) { 747 pub fn clear_cache(&self) {
713 let mut cache = self.cache.lock().unwrap(); 748 let mut cache = self.client.fixture_cache().lock().unwrap();
714 cache.clear(); 749 cache.clear();
715 } 750 }
716} 751}