upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/fixtures.rs
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/fixtures.rs
parentad6b8a825a500896d613fed72c11e7cbce3ddfd9 (diff)
fix cli runs to prevent rate limiting
Diffstat (limited to 'grasp-audit/src/fixtures.rs')
-rw-r--r--grasp-audit/src/fixtures.rs312
1 files changed, 312 insertions, 0 deletions
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