diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-06 12:59:29 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-06 15:17:32 +0000 |
| commit | 16d14d07b023614c1da0fbb11693d131327a3532 (patch) | |
| tree | c6e4b2fe6bae57bd35e8d526f281a84b515641f2 /grasp-audit/src | |
| parent | ad6b8a825a500896d613fed72c11e7cbce3ddfd9 (diff) | |
fix cli runs to prevent rate limiting
Diffstat (limited to 'grasp-audit/src')
| -rw-r--r-- | grasp-audit/src/audit.rs | 2 | ||||
| -rw-r--r-- | grasp-audit/src/client.rs | 14 | ||||
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 312 | ||||
| -rw-r--r-- | grasp-audit/src/isolation.rs | 2 | ||||
| -rw-r--r-- | grasp-audit/src/lib.rs | 2 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/event_acceptance_policy.rs | 495 |
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 { | |||
| 31 | impl AuditConfig { | 31 | impl 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 | ||
| 15 | impl AuditClient { | 15 | impl 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 | |||
| 25 | use crate::{AuditClient, AuditMode}; | ||
| 26 | use anyhow::{Context, Result}; | ||
| 27 | use nostr_sdk::prelude::Event; | ||
| 28 | use std::collections::HashMap; | ||
| 29 | use std::sync::{Arc, Mutex}; | ||
| 30 | |||
| 31 | /// Types of test fixtures available | ||
| 32 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 33 | pub 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)] | ||
| 49 | pub 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 | |||
| 57 | impl 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 | /// ``` | ||
| 89 | pub struct TestContext<'a> { | ||
| 90 | client: &'a AuditClient, | ||
| 91 | mode: ContextMode, | ||
| 92 | cache: Arc<Mutex<HashMap<FixtureKind, Event>>>, | ||
| 93 | } | ||
| 94 | |||
| 95 | impl<'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)] | ||
| 279 | mod 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 |
| 19 | pub fn generate_ci_run_id() -> String { | 19 | pub 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 | ||
| 31 | pub mod audit; | 31 | pub mod audit; |
| 32 | pub mod fixtures; | ||
| 32 | pub mod client; | 33 | pub mod client; |
| 33 | pub mod isolation; | 34 | pub mod isolation; |
| 34 | pub mod result; | 35 | pub mod result; |
| 35 | pub mod specs; | 36 | pub mod specs; |
| 36 | 37 | ||
| 37 | pub use audit::{AuditConfig, AuditMode}; | 38 | pub use audit::{AuditConfig, AuditMode}; |
| 39 | pub use fixtures::{ContextMode, FixtureKind, TestContext}; | ||
| 38 | pub use client::AuditClient; | 40 | pub use client::AuditClient; |
| 39 | pub use result::{AuditResult, TestResult}; | 41 | pub 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 | ||
| 91 | use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp}; | 91 | use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp}; |
| 92 | use crate::{AuditClient, AuditResult, TestResult}; | 92 | use crate::{AuditClient, AuditResult, TestResult, TestContext, FixtureKind}; |
| 93 | use std::time::Duration; | 93 | use 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(()) |