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-26 15:36:12 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-26 17:16:24 +0000
commit734d255efaa26bcb18b29d655bf30f8affb3a852 (patch)
treeb0d5b72e38bd4ceb6d35334741708f2a774a4994 /grasp-audit/src/fixtures.rs
parent158d3f0722e731f2b534951069c322c5cbb5a721 (diff)
test: use fixtures in push tests
Diffstat (limited to 'grasp-audit/src/fixtures.rs')
-rw-r--r--grasp-audit/src/fixtures.rs378
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;
28use std::collections::HashMap; 28use std::collections::HashMap;
29use std::sync::{Arc, Mutex}; 29use 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)
40pub const DETERMINISTIC_COMMIT_HASH: &str = "64ea71d79a57a7acb334cd9651f8aec067c0ce5d"; 40pub 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
52pub 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
64pub 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)]
44pub enum FixtureKind { 79pub 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