diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-26 15:36:12 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-26 17:16:24 +0000 |
| commit | 734d255efaa26bcb18b29d655bf30f8affb3a852 (patch) | |
| tree | b0d5b72e38bd4ceb6d35334741708f2a774a4994 /grasp-audit/src/fixtures.rs | |
| parent | 158d3f0722e731f2b534951069c322c5cbb5a721 (diff) | |
test: use fixtures in push tests
Diffstat (limited to 'grasp-audit/src/fixtures.rs')
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 378 |
1 files changed, 359 insertions, 19 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index 9ccd703..f7988a0 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs | |||
| @@ -28,7 +28,7 @@ use nostr_sdk::prelude::Event; | |||
| 28 | use std::collections::HashMap; | 28 | use std::collections::HashMap; |
| 29 | use std::sync::{Arc, Mutex}; | 29 | use std::sync::{Arc, Mutex}; |
| 30 | 30 | ||
| 31 | /// Deterministic commit hash used in RepoState fixtures | 31 | /// Deterministic commit hash used in RepoState fixtures (Owner variant) |
| 32 | /// This is the hash produced by creating a commit with: | 32 | /// This is the hash produced by creating a commit with: |
| 33 | /// - Message: "Initial commit" | 33 | /// - Message: "Initial commit" |
| 34 | /// - File: test.txt containing "Initial commit" | 34 | /// - File: test.txt containing "Initial commit" |
| @@ -39,20 +39,101 @@ use std::sync::{Arc, Mutex}; | |||
| 39 | /// - Parent: Initial empty commit (09cc37de80f3434fa98864a86730b8d7777bd6ae) | 39 | /// - Parent: Initial empty commit (09cc37de80f3434fa98864a86730b8d7777bd6ae) |
| 40 | pub const DETERMINISTIC_COMMIT_HASH: &str = "64ea71d79a57a7acb334cd9651f8aec067c0ce5d"; | 40 | pub const DETERMINISTIC_COMMIT_HASH: &str = "64ea71d79a57a7acb334cd9651f8aec067c0ce5d"; |
| 41 | 41 | ||
| 42 | /// Deterministic commit hash for maintainer fixtures (Maintainer variant) | ||
| 43 | /// This is the hash produced by creating a commit with: | ||
| 44 | /// - Message: "Maintainer initial commit" | ||
| 45 | /// - File: test.txt containing "Maintainer initial commit" | ||
| 46 | /// - Author date: 2024-01-01T00:00:00Z | ||
| 47 | /// - Committer date: 2024-01-01T00:00:00Z | ||
| 48 | /// - GPG signing: disabled | ||
| 49 | /// - User: "GRASP Audit Test <test@grasp-audit.local>" | ||
| 50 | /// - Parent: none (root commit) | ||
| 51 | /// NOTE: This value is different from DETERMINISTIC_COMMIT_HASH due to different content | ||
| 52 | pub const MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = "1c2d472c9b71ed51968a66500281a3c4a6840464"; | ||
| 53 | |||
| 54 | /// Deterministic commit hash for recursive maintainer fixtures (RecursiveMaintainer variant) | ||
| 55 | /// This is the hash produced by creating a commit with: | ||
| 56 | /// - Message: "Recursive maintainer initial commit" | ||
| 57 | /// - File: test.txt containing "Recursive maintainer initial commit" | ||
| 58 | /// - Author date: 2024-01-01T00:00:00Z | ||
| 59 | /// - Committer date: 2024-01-01T00:00:00Z | ||
| 60 | /// - GPG signing: disabled | ||
| 61 | /// - User: "GRASP Audit Test <test@grasp-audit.local>" | ||
| 62 | /// - Parent: none (root commit) | ||
| 63 | /// NOTE: This value is different from DETERMINISTIC_COMMIT_HASH due to different content | ||
| 64 | pub const RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = "05939b82de66fbdb9c077d0a64fc68522f3cb8e0"; | ||
| 65 | |||
| 42 | /// Types of test fixtures available | 66 | /// Types of test fixtures available |
| 67 | /// | ||
| 68 | /// ## Fixture Dependencies | ||
| 69 | /// | ||
| 70 | /// Several fixtures depend on `ValidRepo` - they all use the SAME repo_id | ||
| 71 | /// within a single TestContext instance to ensure proper fixture relationships: | ||
| 72 | /// - `RepoState` → uses ValidRepo's repo_id | ||
| 73 | /// - `MaintainerAnnouncement` + `MaintainerState` → uses ValidRepo's repo_id | ||
| 74 | /// - `RecursiveMaintainerRepoAndState` → uses ValidRepo's repo_id | ||
| 75 | /// | ||
| 76 | /// This enables testing recursive maintainer authorization chains where multiple | ||
| 77 | /// parties publish announcements and state events for the same repository. | ||
| 43 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | 78 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| 44 | pub enum FixtureKind { | 79 | pub enum FixtureKind { |
| 45 | /// Basic repository announcement (kind 30617) | 80 | /// Basic repository announcement (kind 30617) |
| 81 | /// - Signed by owner keys (`client.keys()`) | ||
| 82 | /// - Lists `client.maintainer_pubkey_hex()` in maintainers tag | ||
| 46 | ValidRepo, | 83 | ValidRepo, |
| 47 | 84 | ||
| 48 | /// Repository with one issue (kind 1621) | 85 | /// Repository with one issue (kind 1621) |
| 86 | /// - Requires ValidRepo (reuses same repo_id) | ||
| 49 | RepoWithIssue, | 87 | RepoWithIssue, |
| 50 | 88 | ||
| 51 | /// Repository with issue and comment (kind 1111) | 89 | /// Repository with issue and comment (kind 1111) |
| 90 | /// - Requires RepoWithIssue (reuses same repo_id) | ||
| 52 | RepoWithComment, | 91 | RepoWithComment, |
| 53 | 92 | ||
| 54 | /// Repository state announcement (kind 30618) | 93 | /// Repository state announcement (kind 30618) for owner |
| 94 | /// - Requires ValidRepo (uses same repo_id) | ||
| 95 | /// - Signed by owner keys (`client.keys()`) | ||
| 96 | /// - Points to DETERMINISTIC_COMMIT_HASH | ||
| 97 | /// - Timestamp: 10 seconds in the past | ||
| 55 | RepoState, | 98 | RepoState, |
| 99 | |||
| 100 | /// Maintainer's repo announcement only for the SAME repo_id as ValidRepo | ||
| 101 | /// - Requires ValidRepo (uses same repo_id for maintainer chain) | ||
| 102 | /// - Announcement signed by `client.maintainer_keys()` | ||
| 103 | /// - Lists `client.recursive_maintainer_pubkey_hex()` in maintainers tag | ||
| 104 | /// - Does NOT include state event (use MaintainerState for that) | ||
| 105 | MaintainerAnnouncement, | ||
| 106 | |||
| 107 | /// Maintainer's state event only for the SAME repo_id as ValidRepo | ||
| 108 | /// - Requires ValidRepo (uses same repo_id for maintainer chain) | ||
| 109 | /// - State event signed by `client.maintainer_keys()` | ||
| 110 | /// - Points to MAINTAINER_DETERMINISTIC_COMMIT_HASH | ||
| 111 | /// - Timestamp: 5 seconds in the past (more recent than owner's state) | ||
| 112 | /// - Does NOT include announcement (use MaintainerAnnouncement for that) | ||
| 113 | MaintainerState, | ||
| 114 | |||
| 115 | /// Recursive maintainer's announcement only for the SAME repo_id as ValidRepo | ||
| 116 | /// - Requires ValidRepo (uses same repo_id for recursive chain) | ||
| 117 | /// - Announcement signed by `client.recursive_maintainer_keys()` | ||
| 118 | /// - Lists owner and maintainer in maintainers tag | ||
| 119 | /// - Does NOT include state event (use RecursiveMaintainerState for that) | ||
| 120 | RecursiveMaintainerAnnouncement, | ||
| 121 | |||
| 122 | /// Recursive maintainer's state event only for the SAME repo_id as ValidRepo | ||
| 123 | /// - Requires ValidRepo (uses same repo_id for recursive chain) | ||
| 124 | /// - State event signed by `client.recursive_maintainer_keys()` | ||
| 125 | /// - Points to RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH | ||
| 126 | /// - Timestamp: 2 seconds in the past (most recent) | ||
| 127 | /// - Does NOT include announcement (use RecursiveMaintainerAnnouncement for that) | ||
| 128 | RecursiveMaintainerState, | ||
| 129 | |||
| 130 | /// Recursive maintainer's announcement + state for the SAME repo_id as ValidRepo | ||
| 131 | /// - Requires ValidRepo (uses same repo_id for recursive chain) | ||
| 132 | /// - Announcement signed by `client.recursive_maintainer_keys()` | ||
| 133 | /// - Lists owner and maintainer in maintainers tag | ||
| 134 | /// - State event points to RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH | ||
| 135 | /// - Timestamp: 2 seconds in the past (most recent) | ||
| 136 | RecursiveMaintainerRepoAndState, | ||
| 56 | } | 137 | } |
| 57 | 138 | ||
| 58 | /// Context mode for fixture management | 139 | /// Context mode for fixture management |
| @@ -214,12 +295,17 @@ impl<'a> TestContext<'a> { | |||
| 214 | Ok(event) | 295 | Ok(event) |
| 215 | } | 296 | } |
| 216 | 297 | ||
| 217 | /// Get or create a ValidRepo, with mode-appropriate caching. | 298 | /// Get or create a ValidRepo, with caching within the TestContext. |
| 218 | /// This is a helper method that avoids async recursion by not going | 299 | /// This is a helper method that avoids async recursion by not going |
| 219 | /// through get_fixture. It handles the repo specifically. | 300 | /// through get_fixture. It handles the repo specifically. |
| 301 | /// | ||
| 302 | /// IMPORTANT: We always cache within a TestContext instance to ensure | ||
| 303 | /// fixture dependencies work correctly. The isolation between tests | ||
| 304 | /// comes from each test having its own TestContext with a fresh cache. | ||
| 220 | async fn get_or_create_repo(&self) -> Result<Event> { | 305 | async fn get_or_create_repo(&self) -> Result<Event> { |
| 221 | // In Shared mode, check cache first | 306 | // Always check cache first - this ensures fixture dependencies work |
| 222 | if self.mode == ContextMode::Shared { | 307 | // (e.g., MaintainerRepoAndState needs the SAME repo_id as RepoState) |
| 308 | { | ||
| 223 | let cache = self.cache.lock().unwrap(); | 309 | let cache = self.cache.lock().unwrap(); |
| 224 | if let Some(event) = cache.get(&FixtureKind::ValidRepo) { | 310 | if let Some(event) = cache.get(&FixtureKind::ValidRepo) { |
| 225 | return Ok(event.clone()); | 311 | return Ok(event.clone()); |
| @@ -237,8 +323,8 @@ impl<'a> TestContext<'a> { | |||
| 237 | // Send it | 323 | // Send it |
| 238 | self.client.send_event(repo.clone()).await?; | 324 | self.client.send_event(repo.clone()).await?; |
| 239 | 325 | ||
| 240 | // Cache it in Shared mode | 326 | // Always cache it - isolation comes from each test having its own TestContext |
| 241 | if self.mode == ContextMode::Shared { | 327 | { |
| 242 | let mut cache = self.cache.lock().unwrap(); | 328 | let mut cache = self.cache.lock().unwrap(); |
| 243 | cache.insert(FixtureKind::ValidRepo, repo.clone()); | 329 | cache.insert(FixtureKind::ValidRepo, repo.clone()); |
| 244 | } | 330 | } |
| @@ -246,18 +332,18 @@ impl<'a> TestContext<'a> { | |||
| 246 | Ok(repo) | 332 | Ok(repo) |
| 247 | } | 333 | } |
| 248 | 334 | ||
| 249 | /// Get or create a RepoWithIssue, with mode-appropriate caching. | 335 | /// Get or create a RepoWithIssue, with caching within the TestContext. |
| 250 | /// Returns the issue event (repo is already sent/cached via get_or_create_repo). | 336 | /// Returns the issue event (repo is already sent/cached via get_or_create_repo). |
| 251 | async fn get_or_create_issue(&self) -> Result<Event> { | 337 | async fn get_or_create_issue(&self) -> Result<Event> { |
| 252 | // In Shared mode, check cache first | 338 | // Always check cache first - ensures fixture dependencies work |
| 253 | if self.mode == ContextMode::Shared { | 339 | { |
| 254 | let cache = self.cache.lock().unwrap(); | 340 | let cache = self.cache.lock().unwrap(); |
| 255 | if let Some(event) = cache.get(&FixtureKind::RepoWithIssue) { | 341 | if let Some(event) = cache.get(&FixtureKind::RepoWithIssue) { |
| 256 | return Ok(event.clone()); | 342 | return Ok(event.clone()); |
| 257 | } | 343 | } |
| 258 | } | 344 | } |
| 259 | 345 | ||
| 260 | // Get or create repo (reuses cached in Shared mode) | 346 | // Get or create repo (reuses cached within this TestContext) |
| 261 | let repo = self.get_or_create_repo().await?; | 347 | let repo = self.get_or_create_repo().await?; |
| 262 | 348 | ||
| 263 | // Create the issue | 349 | // Create the issue |
| @@ -271,8 +357,8 @@ impl<'a> TestContext<'a> { | |||
| 271 | // Send it | 357 | // Send it |
| 272 | self.client.send_event(issue.clone()).await?; | 358 | self.client.send_event(issue.clone()).await?; |
| 273 | 359 | ||
| 274 | // Cache it in Shared mode | 360 | // Always cache it - isolation comes from each test having its own TestContext |
| 275 | if self.mode == ContextMode::Shared { | 361 | { |
| 276 | let mut cache = self.cache.lock().unwrap(); | 362 | let mut cache = self.cache.lock().unwrap(); |
| 277 | cache.insert(FixtureKind::RepoWithIssue, issue.clone()); | 363 | cache.insert(FixtureKind::RepoWithIssue, issue.clone()); |
| 278 | } | 364 | } |
| @@ -284,12 +370,8 @@ impl<'a> TestContext<'a> { | |||
| 284 | async fn build_fixture(&self, kind: FixtureKind) -> Result<Event> { | 370 | async fn build_fixture(&self, kind: FixtureKind) -> Result<Event> { |
| 285 | match kind { | 371 | match kind { |
| 286 | FixtureKind::ValidRepo => { | 372 | FixtureKind::ValidRepo => { |
| 287 | let test_name = format!( | 373 | // Delegate to get_or_create_repo() which handles caching properly. |
| 288 | "fixture-{:?}-{}", | 374 | self.get_or_create_repo().await |
| 289 | kind, | ||
| 290 | &uuid::Uuid::new_v4().to_string()[..8] | ||
| 291 | ); | ||
| 292 | self.client.create_repo_announcement(&test_name).await | ||
| 293 | } | 375 | } |
| 294 | 376 | ||
| 295 | FixtureKind::RepoWithIssue => { | 377 | FixtureKind::RepoWithIssue => { |
| @@ -340,6 +422,9 @@ impl<'a> TestContext<'a> { | |||
| 340 | .to_string(); | 422 | .to_string(); |
| 341 | 423 | ||
| 342 | // Create state announcement with deterministic commit hash | 424 | // Create state announcement with deterministic commit hash |
| 425 | let base_time = Timestamp::now().as_u64(); | ||
| 426 | let older_timestamp = Timestamp::from(base_time - 10); // 10 seconds ago | ||
| 427 | |||
| 343 | // Tag format: ["refs/heads/main", "<commit_hash>"] | 428 | // Tag format: ["refs/heads/main", "<commit_hash>"] |
| 344 | // Note: We build the state but DON'T send it here - the caller will send it | 429 | // Note: We build the state but DON'T send it here - the caller will send it |
| 345 | self.client | 430 | self.client |
| @@ -353,12 +438,267 @@ impl<'a> TestContext<'a> { | |||
| 353 | TagKind::custom("HEAD"), | 438 | TagKind::custom("HEAD"), |
| 354 | vec!["ref: refs/heads/main".to_string()], | 439 | vec!["ref: refs/heads/main".to_string()], |
| 355 | )) | 440 | )) |
| 441 | .custom_time(older_timestamp) | ||
| 356 | .build(self.client.keys()) | 442 | .build(self.client.keys()) |
| 357 | .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e)) | 443 | .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e)) |
| 358 | } | 444 | } |
| 445 | |||
| 446 | FixtureKind::MaintainerAnnouncement => { | ||
| 447 | use nostr_sdk::prelude::*; | ||
| 448 | |||
| 449 | // Get the owner's repo to use the SAME repo_id | ||
| 450 | let owner_repo = self.get_or_create_repo().await?; | ||
| 451 | |||
| 452 | // Extract repo_id from owner's repo announcement | ||
| 453 | let repo_id = owner_repo | ||
| 454 | .tags | ||
| 455 | .iter() | ||
| 456 | .find(|t| t.kind() == TagKind::d()) | ||
| 457 | .and_then(|t| t.content()) | ||
| 458 | .ok_or_else(|| anyhow::anyhow!("Missing d tag in owner repo announcement"))? | ||
| 459 | .to_string(); | ||
| 460 | |||
| 461 | self.build_maintainer_announcement(&repo_id).await | ||
| 462 | } | ||
| 463 | |||
| 464 | FixtureKind::MaintainerState => { | ||
| 465 | use nostr_sdk::prelude::*; | ||
| 466 | |||
| 467 | // Get the owner's repo to use the SAME repo_id | ||
| 468 | let owner_repo = self.get_or_create_repo().await?; | ||
| 469 | |||
| 470 | // Extract repo_id from owner's repo announcement | ||
| 471 | let repo_id = owner_repo | ||
| 472 | .tags | ||
| 473 | .iter() | ||
| 474 | .find(|t| t.kind() == TagKind::d()) | ||
| 475 | .and_then(|t| t.content()) | ||
| 476 | .ok_or_else(|| anyhow::anyhow!("Missing d tag in owner repo announcement"))? | ||
| 477 | .to_string(); | ||
| 478 | |||
| 479 | // Build state event ONLY - does NOT send announcement | ||
| 480 | // This allows testing state-only scenarios | ||
| 481 | self.build_maintainer_state(&repo_id) | ||
| 482 | } | ||
| 483 | |||
| 484 | FixtureKind::RecursiveMaintainerAnnouncement => { | ||
| 485 | use nostr_sdk::prelude::*; | ||
| 486 | |||
| 487 | // Get the owner's repo to use the SAME repo_id | ||
| 488 | let owner_repo = self.get_or_create_repo().await?; | ||
| 489 | |||
| 490 | // Extract repo_id from owner's repo announcement | ||
| 491 | let repo_id = owner_repo | ||
| 492 | .tags | ||
| 493 | .iter() | ||
| 494 | .find(|t| t.kind() == TagKind::d()) | ||
| 495 | .and_then(|t| t.content()) | ||
| 496 | .ok_or_else(|| anyhow::anyhow!("Missing d tag in owner repo announcement"))? | ||
| 497 | .to_string(); | ||
| 498 | |||
| 499 | self.build_recursive_maintainer_announcement(&repo_id).await | ||
| 500 | } | ||
| 501 | |||
| 502 | FixtureKind::RecursiveMaintainerState => { | ||
| 503 | use nostr_sdk::prelude::*; | ||
| 504 | |||
| 505 | // Get the owner's repo to use the SAME repo_id | ||
| 506 | let owner_repo = self.get_or_create_repo().await?; | ||
| 507 | |||
| 508 | // Extract repo_id from owner's repo announcement | ||
| 509 | let repo_id = owner_repo | ||
| 510 | .tags | ||
| 511 | .iter() | ||
| 512 | .find(|t| t.kind() == TagKind::d()) | ||
| 513 | .and_then(|t| t.content()) | ||
| 514 | .ok_or_else(|| anyhow::anyhow!("Missing d tag in owner repo announcement"))? | ||
| 515 | .to_string(); | ||
| 516 | |||
| 517 | // Build state event ONLY - does NOT send announcement | ||
| 518 | self.build_recursive_maintainer_state(&repo_id) | ||
| 519 | } | ||
| 520 | |||
| 521 | FixtureKind::RecursiveMaintainerRepoAndState => { | ||
| 522 | use nostr_sdk::prelude::*; | ||
| 523 | |||
| 524 | // Get the owner's repo to use the SAME repo_id | ||
| 525 | let owner_repo = self.get_or_create_repo().await?; | ||
| 526 | |||
| 527 | // Extract repo_id from owner's repo announcement | ||
| 528 | let repo_id = owner_repo | ||
| 529 | .tags | ||
| 530 | .iter() | ||
| 531 | .find(|t| t.kind() == TagKind::d()) | ||
| 532 | .and_then(|t| t.content()) | ||
| 533 | .ok_or_else(|| anyhow::anyhow!("Missing d tag in owner repo announcement"))? | ||
| 534 | .to_string(); | ||
| 535 | |||
| 536 | // Build and send the recursive maintainer's repo announcement | ||
| 537 | let recursive_maintainer_announcement = self.build_recursive_maintainer_announcement(&repo_id).await?; | ||
| 538 | self.client.send_event(recursive_maintainer_announcement).await?; | ||
| 539 | |||
| 540 | // Return the state event (caller will send it) | ||
| 541 | self.build_recursive_maintainer_state(&repo_id) | ||
| 542 | } | ||
| 359 | } | 543 | } |
| 360 | } | 544 | } |
| 361 | 545 | ||
| 546 | /// Build maintainer announcement event for the given repo_id | ||
| 547 | async fn build_maintainer_announcement(&self, repo_id: &str) -> Result<Event> { | ||
| 548 | use nostr_sdk::prelude::*; | ||
| 549 | |||
| 550 | // Get relay URL for clone tag | ||
| 551 | let relay_url = self.client | ||
| 552 | .client() | ||
| 553 | .relays() | ||
| 554 | .await | ||
| 555 | .keys() | ||
| 556 | .next() | ||
| 557 | .ok_or_else(|| anyhow::anyhow!("No relay connected"))? | ||
| 558 | .to_string(); | ||
| 559 | let http_url = relay_url | ||
| 560 | .replace("ws://", "http://") | ||
| 561 | .replace("wss://", "https://"); | ||
| 562 | |||
| 563 | // Create maintainer's repo announcement for the SAME repo_id | ||
| 564 | let maintainer_npub = self.client | ||
| 565 | .maintainer_keys() | ||
| 566 | .public_key() | ||
| 567 | .to_bech32() | ||
| 568 | .map_err(|e| anyhow::anyhow!("Failed to convert maintainer pubkey: {}", e))?; | ||
| 569 | |||
| 570 | self.client | ||
| 571 | .event_builder( | ||
| 572 | Kind::GitRepoAnnouncement, | ||
| 573 | format!("Maintainer announcement for {}", repo_id), | ||
| 574 | ) | ||
| 575 | .tag(Tag::identifier(repo_id)) | ||
| 576 | .tag(Tag::custom( | ||
| 577 | TagKind::custom("name"), | ||
| 578 | vec![format!("{} (maintainer)", repo_id)], | ||
| 579 | )) | ||
| 580 | .tag(Tag::custom( | ||
| 581 | TagKind::custom("clone"), | ||
| 582 | vec![format!("{}/{}/{}.git", http_url, maintainer_npub, repo_id)], | ||
| 583 | )) | ||
| 584 | .tag(Tag::custom( | ||
| 585 | TagKind::custom("relays"), | ||
| 586 | vec![relay_url], | ||
| 587 | )) | ||
| 588 | .tag(Tag::custom( | ||
| 589 | TagKind::custom("maintainers"), | ||
| 590 | vec![self.client.recursive_maintainer_pubkey_hex()], | ||
| 591 | )) | ||
| 592 | .build(self.client.maintainer_keys()) | ||
| 593 | .map_err(|e| anyhow::anyhow!("Failed to build maintainer repo announcement: {}", e)) | ||
| 594 | } | ||
| 595 | |||
| 596 | /// Build maintainer state event for the given repo_id | ||
| 597 | fn build_maintainer_state(&self, repo_id: &str) -> Result<Event> { | ||
| 598 | use nostr_sdk::prelude::*; | ||
| 599 | |||
| 600 | // Create state announcement 5 seconds in the past, signed by maintainer | ||
| 601 | let base_time = Timestamp::now().as_u64(); | ||
| 602 | let older_timestamp = Timestamp::from(base_time - 5); // 5 seconds ago | ||
| 603 | |||
| 604 | self.client | ||
| 605 | .event_builder(Kind::Custom(30618), "") | ||
| 606 | .tag(Tag::identifier(repo_id)) | ||
| 607 | .tag(Tag::custom( | ||
| 608 | TagKind::custom("refs/heads/main"), | ||
| 609 | vec![MAINTAINER_DETERMINISTIC_COMMIT_HASH.to_string()], | ||
| 610 | )) | ||
| 611 | .tag(Tag::custom( | ||
| 612 | TagKind::custom("HEAD"), | ||
| 613 | vec!["ref: refs/heads/main".to_string()], | ||
| 614 | )) | ||
| 615 | .custom_time(older_timestamp) | ||
| 616 | .build(self.client.maintainer_keys()) | ||
| 617 | .map_err(|e| anyhow::anyhow!("Failed to build maintainer state announcement: {}", e)) | ||
| 618 | } | ||
| 619 | |||
| 620 | /// Build recursive maintainer announcement event for the given repo_id | ||
| 621 | async fn build_recursive_maintainer_announcement(&self, repo_id: &str) -> Result<Event> { | ||
| 622 | use nostr_sdk::prelude::*; | ||
| 623 | |||
| 624 | // Get relay URL for clone tag | ||
| 625 | let relay_url = self.client | ||
| 626 | .client() | ||
| 627 | .relays() | ||
| 628 | .await | ||
| 629 | .keys() | ||
| 630 | .next() | ||
| 631 | .ok_or_else(|| anyhow::anyhow!("No relay connected"))? | ||
| 632 | .to_string(); | ||
| 633 | let http_url = relay_url | ||
| 634 | .replace("ws://", "http://") | ||
| 635 | .replace("wss://", "https://"); | ||
| 636 | |||
| 637 | // Create recursive maintainer's repo announcement for the SAME repo_id | ||
| 638 | let recursive_maintainer_npub = self.client | ||
| 639 | .recursive_maintainer_keys() | ||
| 640 | .public_key() | ||
| 641 | .to_bech32() | ||
| 642 | .map_err(|e| anyhow::anyhow!("Failed to convert recursive maintainer pubkey: {}", e))?; | ||
| 643 | |||
| 644 | self.client | ||
| 645 | .event_builder( | ||
| 646 | Kind::GitRepoAnnouncement, | ||
| 647 | format!("Recursive maintainer announcement for {}", repo_id), | ||
| 648 | ) | ||
| 649 | .tag(Tag::identifier(repo_id)) | ||
| 650 | .tag(Tag::custom( | ||
| 651 | TagKind::custom("name"), | ||
| 652 | vec![format!("{} (recursive maintainer)", repo_id)], | ||
| 653 | )) | ||
| 654 | .tag(Tag::custom( | ||
| 655 | TagKind::custom("clone"), | ||
| 656 | vec![format!("{}/{}/{}.git", http_url, recursive_maintainer_npub, repo_id)], | ||
| 657 | )) | ||
| 658 | .tag(Tag::custom( | ||
| 659 | TagKind::custom("relays"), | ||
| 660 | vec![relay_url], | ||
| 661 | )) | ||
| 662 | .tag(Tag::custom( | ||
| 663 | TagKind::custom("maintainers"), | ||
| 664 | vec![ | ||
| 665 | self.client.public_key().to_hex(), | ||
| 666 | self.client.maintainer_pubkey_hex(), | ||
| 667 | ], | ||
| 668 | )) | ||
| 669 | .build(self.client.recursive_maintainer_keys()) | ||
| 670 | .map_err(|e| anyhow::anyhow!("Failed to build recursive maintainer repo announcement: {}", e)) | ||
| 671 | } | ||
| 672 | |||
| 673 | /// Build recursive maintainer state event for the given repo_id | ||
| 674 | fn build_recursive_maintainer_state(&self, repo_id: &str) -> Result<Event> { | ||
| 675 | use nostr_sdk::prelude::*; | ||
| 676 | |||
| 677 | // Create state announcement 2 seconds in the past, signed by recursive maintainer | ||
| 678 | let base_time = Timestamp::now().as_u64(); | ||
| 679 | let older_timestamp = Timestamp::from(base_time - 2); // 2 seconds ago | ||
| 680 | |||
| 681 | self.client | ||
| 682 | .event_builder(Kind::Custom(30618), "") | ||
| 683 | .tag(Tag::identifier(repo_id)) | ||
| 684 | .tag(Tag::custom( | ||
| 685 | TagKind::custom("refs/heads/main"), | ||
| 686 | vec![RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH.to_string()], | ||
| 687 | )) | ||
| 688 | .tag(Tag::custom( | ||
| 689 | TagKind::custom("HEAD"), | ||
| 690 | vec!["ref: refs/heads/main".to_string()], | ||
| 691 | )) | ||
| 692 | .custom_time(older_timestamp) | ||
| 693 | .build(self.client.recursive_maintainer_keys()) | ||
| 694 | .map_err(|e| { | ||
| 695 | anyhow::anyhow!( | ||
| 696 | "Failed to build recursive maintainer state announcement: {}", | ||
| 697 | e | ||
| 698 | ) | ||
| 699 | }) | ||
| 700 | } | ||
| 701 | |||
| 362 | /// Clear the fixture cache | 702 | /// Clear the fixture cache |
| 363 | /// | 703 | /// |
| 364 | /// This is useful for tests that want to ensure fresh fixtures | 704 | /// This is useful for tests that want to ensure fresh fixtures |