upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src
diff options
context:
space:
mode:
Diffstat (limited to 'grasp-audit/src')
-rw-r--r--grasp-audit/src/audit.rs78
-rw-r--r--grasp-audit/src/client.rs125
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs1186
3 files changed, 1366 insertions, 23 deletions
diff --git a/grasp-audit/src/audit.rs b/grasp-audit/src/audit.rs
index 5e84409..b97ddb6 100644
--- a/grasp-audit/src/audit.rs
+++ b/grasp-audit/src/audit.rs
@@ -129,6 +129,7 @@ pub struct AuditEventBuilder {
129 content: String, 129 content: String,
130 tags: Vec<Tag>, 130 tags: Vec<Tag>,
131 config: AuditConfig, 131 config: AuditConfig,
132 custom_timestamp: Option<Timestamp>,
132} 133}
133 134
134impl AuditEventBuilder { 135impl AuditEventBuilder {
@@ -139,6 +140,7 @@ impl AuditEventBuilder {
139 content: content.into(), 140 content: content.into(),
140 tags: Vec::new(), 141 tags: Vec::new(),
141 config, 142 config,
143 custom_timestamp: None,
142 } 144 }
143 } 145 }
144 146
@@ -154,14 +156,49 @@ impl AuditEventBuilder {
154 self 156 self
155 } 157 }
156 158
159 /// Set a custom timestamp for the event
160 ///
161 /// By default, events use the current time. Use this method to create
162 /// events with a specific timestamp, which is useful for testing
163 /// timestamp-based prioritization logic.
164 ///
165 /// # Example
166 ///
167 /// ```rust
168 /// use nostr_sdk::prelude::*;
169 /// use grasp_audit::{AuditConfig, AuditEventBuilder};
170 ///
171 /// let config = AuditConfig::ci();
172 /// let keys = Keys::generate();
173 ///
174 /// // Create an event with a past timestamp
175 /// let past_event = AuditEventBuilder::new(Kind::TextNote, "test", config)
176 /// .custom_time(Timestamp::from(1700000000))
177 /// .build(&keys)
178 /// .unwrap();
179 ///
180 /// assert_eq!(past_event.created_at, Timestamp::from(1700000000));
181 /// ```
182 pub fn custom_time(mut self, timestamp: Timestamp) -> Self {
183 self.custom_timestamp = Some(timestamp);
184 self
185 }
186
157 /// Build the event with audit tags 187 /// Build the event with audit tags
158 pub fn build(self, keys: &Keys) -> anyhow::Result<Event> { 188 pub fn build(self, keys: &Keys) -> anyhow::Result<Event> {
159 let mut all_tags = self.tags; 189 let mut all_tags = self.tags;
160 all_tags.extend(self.config.audit_tags()); 190 all_tags.extend(self.config.audit_tags());
161 191
162 let event = EventBuilder::new(self.kind, self.content) 192 let builder = EventBuilder::new(self.kind, self.content).tags(all_tags);
163 .tags(all_tags) 193
164 .sign_with_keys(keys)?; 194 // Apply custom timestamp if set
195 let builder = if let Some(timestamp) = self.custom_timestamp {
196 builder.custom_created_at(timestamp)
197 } else {
198 builder
199 };
200
201 let event = builder.sign_with_keys(keys)?;
165 202
166 Ok(event) 203 Ok(event)
167 } 204 }
@@ -243,4 +280,39 @@ mod tests {
243 // Verify event is valid 280 // Verify event is valid
244 assert!(event.verify().is_ok()); 281 assert!(event.verify().is_ok());
245 } 282 }
283
284 #[test]
285 fn test_custom_timestamp_applied() {
286 let config = AuditConfig::ci();
287 let keys = Keys::generate();
288 let custom_ts = Timestamp::from(1700000000);
289
290 // Build event with custom timestamp
291 let event = AuditEventBuilder::new(Kind::TextNote, "test with custom time", config.clone())
292 .custom_time(custom_ts)
293 .build(&keys)
294 .unwrap();
295
296 // Verify the custom timestamp was applied
297 assert_eq!(event.created_at, custom_ts);
298
299 // Verify event is still valid
300 assert!(event.verify().is_ok());
301 }
302
303 #[test]
304 fn test_default_timestamp_uses_current_time() {
305 let config = AuditConfig::ci();
306 let keys = Keys::generate();
307
308 let before = Timestamp::now();
309 let event = AuditEventBuilder::new(Kind::TextNote, "test default time", config.clone())
310 .build(&keys)
311 .unwrap();
312 let after = Timestamp::now();
313
314 // Event timestamp should be between before and after (inclusive)
315 assert!(event.created_at.as_u64() >= before.as_u64());
316 assert!(event.created_at.as_u64() <= after.as_u64());
317 }
246} 318}
diff --git a/grasp-audit/src/client.rs b/grasp-audit/src/client.rs
index 35aaccd..b2a4e38 100644
--- a/grasp-audit/src/client.rs
+++ b/grasp-audit/src/client.rs
@@ -286,6 +286,80 @@ impl AuditClient {
286 Ok(event) 286 Ok(event)
287 } 287 }
288 288
289 /// Create a NIP-34 repository announcement event with maintainers
290 ///
291 /// This helper creates a properly formatted NIP-34 announcement that will be
292 /// accepted by GRASP relays (which require events to list the relay in clone/relays tags).
293 /// This variant also includes a maintainers tag for push authorization testing.
294 ///
295 /// # Arguments
296 /// * `test_name` - Name of the test (used to create unique repo identifier)
297 /// * `maintainer_pubkeys` - Hex pubkeys of maintainers who can push to the repository
298 ///
299 /// # Returns
300 /// A built and signed Event ready to be sent to the relay
301 pub async fn create_repo_announcement_with_maintainers(
302 &self,
303 test_name: &str,
304 maintainer_pubkeys: &[String],
305 ) -> Result<Event> {
306 // Get relay URL from client
307 let relay_url = self
308 .client
309 .relays()
310 .await
311 .keys()
312 .next()
313 .ok_or_else(|| anyhow!("No relay connected"))?
314 .to_string();
315
316 // Convert WebSocket URL to HTTP URL for clone tag
317 let http_url = relay_url
318 .replace("ws://", "http://")
319 .replace("wss://", "https://");
320
321 // Create unique repository identifier using UUID for consistency
322 let repo_id = format!("{}-{}", test_name, &uuid::Uuid::new_v4().to_string()[..8]);
323
324 // Get npub for clone URL
325 let npub = self
326 .public_key()
327 .to_bech32()
328 .map_err(|e| anyhow!("Failed to convert public key to bech32 npub format: {}", e))?;
329
330 // Build kind 30617 repository announcement with maintainers tag
331 let event = self
332 .event_builder(
333 Kind::GitRepoAnnouncement,
334 format!("Test repository for {}", test_name),
335 )
336 .tag(Tag::identifier(&repo_id))
337 .tag(Tag::custom(
338 TagKind::custom("name"),
339 vec![format!("{} Test Repository", test_name)],
340 ))
341 .tag(Tag::custom(
342 TagKind::custom("description"),
343 vec![format!("Repository for {} testing", test_name)],
344 ))
345 .tag(Tag::custom(
346 TagKind::custom("clone"),
347 vec![format!("{}/{}/{}.git", http_url, npub, repo_id)],
348 ))
349 .tag(Tag::custom(
350 TagKind::custom("relays"),
351 vec![relay_url.clone()],
352 ))
353 .tag(Tag::custom(
354 TagKind::custom("maintainers"),
355 maintainer_pubkeys.to_vec(),
356 ))
357 .build(self.keys())
358 .map_err(|e| anyhow!("Failed to build repository announcement event: {}", e))?;
359
360 Ok(event)
361 }
362
289 /// Create an issue (kind 1621) that references a repository 363 /// Create an issue (kind 1621) that references a repository
290 /// 364 ///
291 /// # Arguments 365 /// # Arguments
@@ -456,4 +530,55 @@ mod tests {
456 "Missing custom tag value" 530 "Missing custom tag value"
457 ); 531 );
458 } 532 }
533
534 #[tokio::test]
535 async fn test_create_repo_announcement_with_maintainers() {
536 let config = AuditConfig::ci();
537 let client = AuditClient::new_test(config);
538
539 // Create test maintainer pubkeys (hex format)
540 let maintainer_pubkeys = vec![
541 "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2".to_string(),
542 "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3".to_string(),
543 ];
544
545 // Note: We can't test create_repo_announcement_with_maintainers directly in unit tests
546 // because it requires a connected relay. Instead, we test the underlying event building
547 // with maintainers tag to verify the tag format is correct.
548
549 // Build an event with maintainers tag directly to test the tag format
550 let event = client
551 .event_builder(
552 Kind::GitRepoAnnouncement,
553 "Test repository",
554 )
555 .tag(Tag::identifier("test-repo"))
556 .tag(Tag::custom(
557 TagKind::custom("maintainers"),
558 maintainer_pubkeys.clone(),
559 ))
560 .build(client.keys())
561 .unwrap();
562
563 // Verify the maintainers tag is present and correctly formatted
564 let maintainers_tag = event
565 .tags
566 .iter()
567 .find(|t| t.kind() == TagKind::custom("maintainers"));
568
569 assert!(
570 maintainers_tag.is_some(),
571 "Missing 'maintainers' tag in event"
572 );
573
574 // Verify the tag contains the maintainer pubkeys
575 let tag = maintainers_tag.unwrap();
576 let tag_vec: Vec<String> = tag.clone().to_vec();
577
578 // First element is "maintainers", rest are the pubkeys
579 assert_eq!(tag_vec[0], "maintainers");
580 assert_eq!(tag_vec.len(), 3, "Expected 3 elements: tag name + 2 pubkeys");
581 assert_eq!(tag_vec[1], maintainer_pubkeys[0]);
582 assert_eq!(tag_vec[2], maintainer_pubkeys[1]);
583 }
459} 584}
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index 974ccd4..5545b1a 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -426,34 +426,907 @@ impl PushAuthorizationTests {
426 } 426 }
427 } 427 }
428 428
429 /// Test recursive maintainer authorization 429 /// Test that latest state event is used for authorization
430 ///
431 /// GRASP-01 requires that the relay use the LATEST state event (by created_at
432 /// timestamp) when determining push authorization. This test verifies that
433 /// a newer state event takes precedence over an older one.
434 ///
435 /// Scenario:
436 /// 1. Owner creates repo with maintainer
437 /// 2. Owner publishes state event for commit_a at t=100 (older)
438 /// 3. Maintainer publishes state event for commit_b at t=200 (newer)
439 /// 4. Push commit_b should be ACCEPTED (newer timestamp wins)
440 /// 5. Push commit_a should be REJECTED (older state event superseded)
441 pub async fn test_latest_state_event_used(
442 client: &AuditClient,
443 git_data_dir: &Path,
444 relay_domain: &str,
445 ) -> TestResult {
446 let test_name = "test_latest_state_event_used";
447 let description = "Latest state event takes precedence";
448
449 // 1. Generate maintainer keypair
450 let maintainer_keys = Keys::generate();
451 let maintainer_pubkey = maintainer_keys.public_key().to_hex();
452
453 // 2. Owner creates repo with maintainer
454 let repo_event = match client
455 .create_repo_announcement_with_maintainers(test_name, &[maintainer_pubkey.clone()])
456 .await
457 {
458 Ok(e) => e,
459 Err(e) => {
460 return TestResult::new(test_name, "GRASP-01", description)
461 .fail(&format!("Failed to create repo with maintainers: {}", e))
462 }
463 };
464
465 // Send the owner's repo event
466 if let Err(e) = client.send_event(repo_event.clone()).await {
467 return TestResult::new(test_name, "GRASP-01", description)
468 .fail(&format!("Failed to send owner repo event: {}", e));
469 }
470
471 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
472
473 // Extract repo details
474 let repo_id = match repo_event
475 .tags
476 .iter()
477 .find(|t| t.kind() == TagKind::d())
478 .and_then(|t| t.content())
479 {
480 Some(id) => id.to_string(),
481 None => {
482 return TestResult::new(test_name, "GRASP-01", description)
483 .fail("Repository event missing d tag")
484 }
485 };
486
487 // Get relay URL for maintainer's repo announcement
488 let relay_url = match client.relay_url().await {
489 Ok(u) => u,
490 Err(e) => {
491 return TestResult::new(test_name, "GRASP-01", description)
492 .fail(&format!("Failed to get relay URL: {}", e))
493 }
494 };
495 let http_url = relay_url
496 .replace("ws://", "http://")
497 .replace("wss://", "https://");
498 let maintainer_npub = match maintainer_keys.public_key().to_bech32() {
499 Ok(n) => n,
500 Err(e) => {
501 return TestResult::new(test_name, "GRASP-01", description)
502 .fail(&format!("Failed to convert maintainer pubkey to npub: {}", e))
503 }
504 };
505
506 // 3. Maintainer creates their own repo announcement (same d-tag)
507 let maintainer_repo_event = match client
508 .event_builder(
509 Kind::GitRepoAnnouncement,
510 format!("Maintainer's view of {} repository", test_name),
511 )
512 .tag(Tag::identifier(&repo_id))
513 .tag(Tag::custom(
514 TagKind::custom("name"),
515 vec![format!("{} Test Repository (Maintainer)", test_name)],
516 ))
517 .tag(Tag::custom(
518 TagKind::custom("clone"),
519 vec![format!("{}/{}/{}.git", http_url, maintainer_npub, repo_id)],
520 ))
521 .tag(Tag::custom(
522 TagKind::custom("relays"),
523 vec![relay_url.clone()],
524 ))
525 .build(&maintainer_keys)
526 {
527 Ok(e) => e,
528 Err(e) => {
529 return TestResult::new(test_name, "GRASP-01", description)
530 .fail(&format!("Failed to build maintainer repo event: {}", e))
531 }
532 };
533
534 if let Err(e) = client.client().send_event(&maintainer_repo_event).await {
535 return TestResult::new(test_name, "GRASP-01", description)
536 .fail(&format!("Failed to send maintainer repo event: {}", e));
537 }
538
539 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
540
541 // Verify maintainer's repo was created
542 let maintainer_repo_path = git_data_dir
543 .join(&maintainer_npub)
544 .join(format!("{}.git", repo_id));
545 if !maintainer_repo_path.exists() {
546 return TestResult::new(test_name, "GRASP-01", description).fail(&format!(
547 "Maintainer repo not created at: {}",
548 maintainer_repo_path.display()
549 ));
550 }
551
552 // 4. Clone maintainer's repo
553 let clone_path = match clone_repo(relay_domain, &maintainer_npub, &repo_id) {
554 Ok(p) => p,
555 Err(e) => {
556 return TestResult::new(test_name, "GRASP-01", description)
557 .fail(&format!("Failed to clone maintainer repo: {}", e))
558 }
559 };
560
561 // 5. Create first commit (commit_a) - this will be the one with OLDER timestamp
562 let commit_a = match create_commit(&clone_path, "Commit A - older state") {
563 Ok(h) => h,
564 Err(e) => {
565 let _ = fs::remove_dir_all(&clone_path);
566 return TestResult::new(test_name, "GRASP-01", description)
567 .fail(&format!("Failed to create commit_a: {}", e));
568 }
569 };
570
571 // 6. Create second commit (commit_b) - this will be the one with NEWER timestamp
572 let commit_b = match create_commit(&clone_path, "Commit B - newer state") {
573 Ok(h) => h,
574 Err(e) => {
575 let _ = fs::remove_dir_all(&clone_path);
576 return TestResult::new(test_name, "GRASP-01", description)
577 .fail(&format!("Failed to create commit_b: {}", e));
578 }
579 };
580
581 // 7. Calculate timestamps: older_timestamp (100 seconds ago) and newer_timestamp (now)
582 let base_time = Timestamp::now().as_u64();
583 let older_timestamp = Timestamp::from(base_time - 100); // 100 seconds ago
584 let newer_timestamp = Timestamp::from(base_time); // now
585
586 // 8. Owner publishes state event for commit_a at OLDER timestamp
587 let owner_state_event = match client
588 .event_builder(Kind::Custom(30618), "")
589 .tag(Tag::identifier(&repo_id))
590 .tag(Tag::custom(
591 TagKind::custom("refs/heads/main"),
592 vec![commit_a.clone()],
593 ))
594 .custom_time(older_timestamp)
595 .build(client.keys())
596 {
597 Ok(e) => e,
598 Err(e) => {
599 let _ = fs::remove_dir_all(&clone_path);
600 return TestResult::new(test_name, "GRASP-01", description)
601 .fail(&format!("Failed to build owner state event: {}", e));
602 }
603 };
604
605 if let Err(e) = client.client().send_event(&owner_state_event).await {
606 let _ = fs::remove_dir_all(&clone_path);
607 return TestResult::new(test_name, "GRASP-01", description)
608 .fail(&format!("Failed to send owner state event: {}", e));
609 }
610
611 // 9. Maintainer publishes state event for commit_b at NEWER timestamp
612 let maintainer_state_event = match client
613 .event_builder(Kind::Custom(30618), "")
614 .tag(Tag::identifier(&repo_id))
615 .tag(Tag::custom(
616 TagKind::custom("refs/heads/main"),
617 vec![commit_b.clone()],
618 ))
619 .custom_time(newer_timestamp)
620 .build(&maintainer_keys)
621 {
622 Ok(e) => e,
623 Err(e) => {
624 let _ = fs::remove_dir_all(&clone_path);
625 return TestResult::new(test_name, "GRASP-01", description)
626 .fail(&format!("Failed to build maintainer state event: {}", e));
627 }
628 };
629
630 if let Err(e) = client.client().send_event(&maintainer_state_event).await {
631 let _ = fs::remove_dir_all(&clone_path);
632 return TestResult::new(test_name, "GRASP-01", description)
633 .fail(&format!("Failed to send maintainer state event: {}", e));
634 }
635
636 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
637
638 // 10. Create and checkout main branch pointing to commit_b (the newer state)
639 let branch_output = Command::new("git")
640 .args(["branch", "main"])
641 .current_dir(&clone_path)
642 .output();
643
644 if let Ok(output) = branch_output {
645 if !output.status.success() {
646 let _ = fs::remove_dir_all(&clone_path);
647 return TestResult::new(test_name, "GRASP-01", description).fail(&format!(
648 "Failed to create main branch: {}",
649 String::from_utf8_lossy(&output.stderr)
650 ));
651 }
652 }
653
654 let checkout_output = Command::new("git")
655 .args(["checkout", "main"])
656 .current_dir(&clone_path)
657 .output();
658
659 if let Ok(output) = checkout_output {
660 if !output.status.success() {
661 let _ = fs::remove_dir_all(&clone_path);
662 return TestResult::new(test_name, "GRASP-01", description).fail(&format!(
663 "Failed to checkout main branch: {}",
664 String::from_utf8_lossy(&output.stderr)
665 ));
666 }
667 }
668
669 // 11. Attempt push - should be ACCEPTED because maintainer's newer state event
670 // announces commit_b which is now HEAD of main
671 let push_result = try_push(&clone_path);
672 let _ = fs::remove_dir_all(&clone_path);
673
674 match push_result {
675 Ok(true) => TestResult::new(test_name, "GRASP-01", description).pass(),
676 Ok(false) => TestResult::new(test_name, "GRASP-01", description).fail(&format!(
677 "Push was rejected but should have been accepted. \
678 The maintainer published a state event at timestamp {} announcing commit_b ({}). \
679 The owner published an older state event at timestamp {} announcing commit_a ({}). \
680 The relay should use the NEWER state event (maintainer's) for authorization.",
681 newer_timestamp.as_u64(),
682 commit_b,
683 older_timestamp.as_u64(),
684 commit_a
685 )),
686 Err(e) => {
687 TestResult::new(test_name, "GRASP-01", description).fail(&format!("Push error: {}", e))
688 }
689 }
690 }
691
692 /// Test push authorized by direct maintainer state event
430 /// 693 ///
431 /// GRASP-01: "respecting the recursive maintainer set" 694 /// GRASP-01: "respecting the recursive maintainer set"
432 pub async fn test_recursive_maintainer_authorization( 695 /// This tests the first level: direct maintainers listed in the maintainers tag.
433 _client: &AuditClient, 696 ///
434 _git_data_dir: &Path, 697 /// Scenario:
435 _relay_domain: &str, 698 /// 1. Owner creates repo with `["maintainers", "<maintainer-pubkey>"]` tag
699 /// 2. Maintainer creates their own repo announcement (same d-tag)
700 /// 3. Maintainer publishes state event with a commit hash
701 /// 4. Push to that commit should be ACCEPTED
702 pub async fn test_push_authorized_by_direct_maintainer_state(
703 client: &AuditClient,
704 git_data_dir: &Path,
705 relay_domain: &str,
436 ) -> TestResult { 706 ) -> TestResult {
437 let test_name = "test_recursive_maintainer_authorization"; 707 let test_name = "test_push_authorized_by_direct_maintainer_state";
708
709 // 1. Generate maintainer keypair
710 let maintainer_keys = Keys::generate();
711 let maintainer_pubkey = maintainer_keys.public_key().to_hex();
712
713 // 2. Owner creates repo with maintainer listed
714 let repo_event = match client
715 .create_repo_announcement_with_maintainers(test_name, &[maintainer_pubkey.clone()])
716 .await
717 {
718 Ok(e) => e,
719 Err(e) => {
720 return TestResult::new(
721 test_name,
722 "GRASP-01",
723 "Push authorized by direct maintainer state event",
724 )
725 .fail(&format!("Failed to create repo with maintainers: {}", e))
726 }
727 };
728
729 // Send the owner's repo event
730 if let Err(e) = client.send_event(repo_event.clone()).await {
731 return TestResult::new(
732 test_name,
733 "GRASP-01",
734 "Push authorized by direct maintainer state event",
735 )
736 .fail(&format!("Failed to send owner repo event: {}", e));
737 }
738
739 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
740
741 // Extract repo details
742 let repo_id = match repo_event
743 .tags
744 .iter()
745 .find(|t| t.kind() == TagKind::d())
746 .and_then(|t| t.content())
747 {
748 Some(id) => id.to_string(),
749 None => {
750 return TestResult::new(
751 test_name,
752 "GRASP-01",
753 "Push authorized by direct maintainer state event",
754 )
755 .fail("Repository event missing d tag")
756 }
757 };
758
759 // Get relay URL for maintainer's repo announcement
760 let relay_url = match client.relay_url().await {
761 Ok(u) => u,
762 Err(e) => {
763 return TestResult::new(
764 test_name,
765 "GRASP-01",
766 "Push authorized by direct maintainer state event",
767 )
768 .fail(&format!("Failed to get relay URL: {}", e))
769 }
770 };
771 let http_url = relay_url
772 .replace("ws://", "http://")
773 .replace("wss://", "https://");
774 let maintainer_npub = match maintainer_keys.public_key().to_bech32() {
775 Ok(n) => n,
776 Err(e) => {
777 return TestResult::new(
778 test_name,
779 "GRASP-01",
780 "Push authorized by direct maintainer state event",
781 )
782 .fail(&format!("Failed to convert maintainer pubkey to npub: {}", e))
783 }
784 };
785
786 // 3. Maintainer creates their own repo announcement (same d-tag)
787 // This creates a separate repo at maintainer-npub/repo-id.git
788 let maintainer_repo_event = match client
789 .event_builder(
790 Kind::GitRepoAnnouncement,
791 format!("Maintainer's view of {} repository", test_name),
792 )
793 .tag(Tag::identifier(&repo_id))
794 .tag(Tag::custom(
795 TagKind::custom("name"),
796 vec![format!("{} Test Repository (Maintainer)", test_name)],
797 ))
798 .tag(Tag::custom(
799 TagKind::custom("clone"),
800 vec![format!("{}/{}/{}.git", http_url, maintainer_npub, repo_id)],
801 ))
802 .tag(Tag::custom(
803 TagKind::custom("relays"),
804 vec![relay_url.clone()],
805 ))
806 .build(&maintainer_keys)
807 {
808 Ok(e) => e,
809 Err(e) => {
810 return TestResult::new(
811 test_name,
812 "GRASP-01",
813 "Push authorized by direct maintainer state event",
814 )
815 .fail(&format!("Failed to build maintainer repo event: {}", e))
816 }
817 };
818
819 if let Err(e) = client.client().send_event(&maintainer_repo_event).await {
820 return TestResult::new(
821 test_name,
822 "GRASP-01",
823 "Push authorized by direct maintainer state event",
824 )
825 .fail(&format!("Failed to send maintainer repo event: {}", e));
826 }
827
828 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
829
830 // Verify maintainer's repo was created
831 let maintainer_repo_path = git_data_dir
832 .join(&maintainer_npub)
833 .join(format!("{}.git", repo_id));
834 if !maintainer_repo_path.exists() {
835 return TestResult::new(
836 test_name,
837 "GRASP-01",
838 "Push authorized by direct maintainer state event",
839 )
840 .fail(&format!(
841 "Maintainer repo not created at: {}",
842 maintainer_repo_path.display()
843 ));
844 }
845
846 // 4. Clone maintainer's repo
847 let clone_path = match clone_repo(relay_domain, &maintainer_npub, &repo_id) {
848 Ok(p) => p,
849 Err(e) => {
850 return TestResult::new(
851 test_name,
852 "GRASP-01",
853 "Push authorized by direct maintainer state event",
854 )
855 .fail(&format!("Failed to clone maintainer repo: {}", e))
856 }
857 };
858
859 // 5. Create deterministic commit
860 let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") {
861 Ok(h) => h,
862 Err(e) => {
863 let _ = fs::remove_dir_all(&clone_path);
864 return TestResult::new(
865 test_name,
866 "GRASP-01",
867 "Push authorized by direct maintainer state event",
868 )
869 .fail(&format!("Failed to create commit: {}", e));
870 }
871 };
872
873 // 6. Maintainer publishes state event with commit hash
874 let state_event = match client
875 .event_builder(Kind::Custom(30618), "")
876 .tag(Tag::identifier(&repo_id))
877 .tag(Tag::custom(
878 TagKind::custom("refs/heads/main"),
879 vec![commit_hash.clone()],
880 ))
881 .build(&maintainer_keys)
882 {
883 Ok(e) => e,
884 Err(e) => {
885 let _ = fs::remove_dir_all(&clone_path);
886 return TestResult::new(
887 test_name,
888 "GRASP-01",
889 "Push authorized by direct maintainer state event",
890 )
891 .fail(&format!("Failed to build state event: {}", e));
892 }
893 };
438 894
439 // This test requires two separate clients (owner and maintainer) 895 if let Err(e) = client.client().send_event(&state_event).await {
440 // For now, return not implemented 896 let _ = fs::remove_dir_all(&clone_path);
441 TestResult::new(test_name, "GRASP-01", "Maintainer can authorize pushes") 897 return TestResult::new(
442 .fail("Not implemented: requires multiple client support") 898 test_name,
899 "GRASP-01",
900 "Push authorized by direct maintainer state event",
901 )
902 .fail(&format!("Failed to send state event: {}", e));
903 }
904
905 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
906
907 // 7. Create and checkout main branch
908 let branch_output = Command::new("git")
909 .args(["branch", "main"])
910 .current_dir(&clone_path)
911 .output();
912
913 if let Ok(output) = branch_output {
914 if !output.status.success() {
915 let _ = fs::remove_dir_all(&clone_path);
916 return TestResult::new(
917 test_name,
918 "GRASP-01",
919 "Push authorized by direct maintainer state event",
920 )
921 .fail(&format!(
922 "Failed to create main branch: {}",
923 String::from_utf8_lossy(&output.stderr)
924 ));
925 }
926 }
927
928 let checkout_output = Command::new("git")
929 .args(["checkout", "main"])
930 .current_dir(&clone_path)
931 .output();
932
933 if let Ok(output) = checkout_output {
934 if !output.status.success() {
935 let _ = fs::remove_dir_all(&clone_path);
936 return TestResult::new(
937 test_name,
938 "GRASP-01",
939 "Push authorized by direct maintainer state event",
940 )
941 .fail(&format!(
942 "Failed to checkout main branch: {}",
943 String::from_utf8_lossy(&output.stderr)
944 ));
945 }
946 }
947
948 // 8. Attempt push - should be ACCEPTED because maintainer's state event authorizes it
949 let push_result = try_push(&clone_path);
950 let _ = fs::remove_dir_all(&clone_path);
951
952 match push_result {
953 Ok(true) => TestResult::new(
954 test_name,
955 "GRASP-01",
956 "Push authorized by direct maintainer state event",
957 )
958 .pass(),
959 Ok(false) => TestResult::new(
960 test_name,
961 "GRASP-01",
962 "Push authorized by direct maintainer state event",
963 )
964 .fail(&format!(
965 "Push was rejected but should have been accepted. \
966 The maintainer (pubkey: {}) is listed in the owner's maintainers tag \
967 and published a state event announcing commit {}. \
968 The relay should authorize pushes matching this state event.",
969 maintainer_pubkey, commit_hash
970 )),
971 Err(e) => TestResult::new(
972 test_name,
973 "GRASP-01",
974 "Push authorized by direct maintainer state event",
975 )
976 .fail(&format!("Push error: {}", e)),
977 }
443 } 978 }
444 979
445 /// Test that latest state event is used for authorization 980 /// Test push authorized by recursive maintainer state event
446 pub async fn test_latest_state_event_used( 981 ///
447 _client: &AuditClient, 982 /// GRASP-01: "respecting the recursive maintainer set"
448 _git_data_dir: &Path, 983 /// This tests recursive maintainer chains: Owner -> MaintainerA -> MaintainerB
449 _relay_domain: &str, 984 ///
985 /// Scenario:
986 /// 1. Owner creates repo with `["maintainers", "<maintainerA-pubkey>"]` tag
987 /// 2. MaintainerA creates their own repo announcement (same d-tag) with MaintainerB
988 /// 3. MaintainerB creates their own repo announcement (same d-tag, no further maintainers)
989 /// 4. MaintainerB publishes state event with a commit hash
990 /// 5. Push to that commit should be ACCEPTED (recursive maintainer chain)
991 pub async fn test_push_authorized_by_recursive_maintainer_state(
992 client: &AuditClient,
993 git_data_dir: &Path,
994 relay_domain: &str,
450 ) -> TestResult { 995 ) -> TestResult {
451 let test_name = "test_latest_state_event_used"; 996 let test_name = "test_push_authorized_by_recursive_maintainer_state";
997
998 // 1. Generate MaintainerA and MaintainerB keypairs
999 let maintainer_a_keys = Keys::generate();
1000 let maintainer_a_pubkey = maintainer_a_keys.public_key().to_hex();
1001
1002 let maintainer_b_keys = Keys::generate();
1003 let maintainer_b_pubkey = maintainer_b_keys.public_key().to_hex();
452 1004
453 // This test requires publishing multiple state events with timestamps 1005 // 2. Owner creates repo with MaintainerA listed
454 // and verifying the latest one is used 1006 let repo_event = match client
455 TestResult::new(test_name, "GRASP-01", "Latest state event takes precedence") 1007 .create_repo_announcement_with_maintainers(test_name, &[maintainer_a_pubkey.clone()])
456 .fail("Not implemented: requires timestamp manipulation") 1008 .await
1009 {
1010 Ok(e) => e,
1011 Err(e) => {
1012 return TestResult::new(
1013 test_name,
1014 "GRASP-01",
1015 "Push authorized by recursive maintainer state event",
1016 )
1017 .fail(&format!("Failed to create repo with maintainers: {}", e))
1018 }
1019 };
1020
1021 // Send the owner's repo event
1022 if let Err(e) = client.send_event(repo_event.clone()).await {
1023 return TestResult::new(
1024 test_name,
1025 "GRASP-01",
1026 "Push authorized by recursive maintainer state event",
1027 )
1028 .fail(&format!("Failed to send owner repo event: {}", e));
1029 }
1030
1031 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1032
1033 // Extract repo details
1034 let repo_id = match repo_event
1035 .tags
1036 .iter()
1037 .find(|t| t.kind() == TagKind::d())
1038 .and_then(|t| t.content())
1039 {
1040 Some(id) => id.to_string(),
1041 None => {
1042 return TestResult::new(
1043 test_name,
1044 "GRASP-01",
1045 "Push authorized by recursive maintainer state event",
1046 )
1047 .fail("Repository event missing d tag")
1048 }
1049 };
1050
1051 // Get relay URL for maintainers' repo announcements
1052 let relay_url = match client.relay_url().await {
1053 Ok(u) => u,
1054 Err(e) => {
1055 return TestResult::new(
1056 test_name,
1057 "GRASP-01",
1058 "Push authorized by recursive maintainer state event",
1059 )
1060 .fail(&format!("Failed to get relay URL: {}", e))
1061 }
1062 };
1063 let http_url = relay_url
1064 .replace("ws://", "http://")
1065 .replace("wss://", "https://");
1066
1067 let maintainer_a_npub = match maintainer_a_keys.public_key().to_bech32() {
1068 Ok(n) => n,
1069 Err(e) => {
1070 return TestResult::new(
1071 test_name,
1072 "GRASP-01",
1073 "Push authorized by recursive maintainer state event",
1074 )
1075 .fail(&format!("Failed to convert maintainer A pubkey to npub: {}", e))
1076 }
1077 };
1078
1079 let maintainer_b_npub = match maintainer_b_keys.public_key().to_bech32() {
1080 Ok(n) => n,
1081 Err(e) => {
1082 return TestResult::new(
1083 test_name,
1084 "GRASP-01",
1085 "Push authorized by recursive maintainer state event",
1086 )
1087 .fail(&format!("Failed to convert maintainer B pubkey to npub: {}", e))
1088 }
1089 };
1090
1091 // 3. MaintainerA creates their own repo announcement (same d-tag) with MaintainerB listed
1092 let maintainer_a_repo_event = match client
1093 .event_builder(
1094 Kind::GitRepoAnnouncement,
1095 format!("MaintainerA's view of {} repository", test_name),
1096 )
1097 .tag(Tag::identifier(&repo_id))
1098 .tag(Tag::custom(
1099 TagKind::custom("name"),
1100 vec![format!("{} Test Repository (MaintainerA)", test_name)],
1101 ))
1102 .tag(Tag::custom(
1103 TagKind::custom("clone"),
1104 vec![format!("{}/{}/{}.git", http_url, maintainer_a_npub, repo_id)],
1105 ))
1106 .tag(Tag::custom(
1107 TagKind::custom("relays"),
1108 vec![relay_url.clone()],
1109 ))
1110 .tag(Tag::custom(
1111 TagKind::custom("maintainers"),
1112 vec![maintainer_b_pubkey.clone()],
1113 ))
1114 .build(&maintainer_a_keys)
1115 {
1116 Ok(e) => e,
1117 Err(e) => {
1118 return TestResult::new(
1119 test_name,
1120 "GRASP-01",
1121 "Push authorized by recursive maintainer state event",
1122 )
1123 .fail(&format!("Failed to build maintainer A repo event: {}", e))
1124 }
1125 };
1126
1127 if let Err(e) = client.client().send_event(&maintainer_a_repo_event).await {
1128 return TestResult::new(
1129 test_name,
1130 "GRASP-01",
1131 "Push authorized by recursive maintainer state event",
1132 )
1133 .fail(&format!("Failed to send maintainer A repo event: {}", e));
1134 }
1135
1136 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1137
1138 // 4. MaintainerB creates their own repo announcement (same d-tag, no further maintainers)
1139 let maintainer_b_repo_event = match client
1140 .event_builder(
1141 Kind::GitRepoAnnouncement,
1142 format!("MaintainerB's view of {} repository", test_name),
1143 )
1144 .tag(Tag::identifier(&repo_id))
1145 .tag(Tag::custom(
1146 TagKind::custom("name"),
1147 vec![format!("{} Test Repository (MaintainerB)", test_name)],
1148 ))
1149 .tag(Tag::custom(
1150 TagKind::custom("clone"),
1151 vec![format!("{}/{}/{}.git", http_url, maintainer_b_npub, repo_id)],
1152 ))
1153 .tag(Tag::custom(
1154 TagKind::custom("relays"),
1155 vec![relay_url.clone()],
1156 ))
1157 .build(&maintainer_b_keys)
1158 {
1159 Ok(e) => e,
1160 Err(e) => {
1161 return TestResult::new(
1162 test_name,
1163 "GRASP-01",
1164 "Push authorized by recursive maintainer state event",
1165 )
1166 .fail(&format!("Failed to build maintainer B repo event: {}", e))
1167 }
1168 };
1169
1170 if let Err(e) = client.client().send_event(&maintainer_b_repo_event).await {
1171 return TestResult::new(
1172 test_name,
1173 "GRASP-01",
1174 "Push authorized by recursive maintainer state event",
1175 )
1176 .fail(&format!("Failed to send maintainer B repo event: {}", e));
1177 }
1178
1179 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1180
1181 // Verify maintainer B's repo was created
1182 let maintainer_b_repo_path = git_data_dir
1183 .join(&maintainer_b_npub)
1184 .join(format!("{}.git", repo_id));
1185 if !maintainer_b_repo_path.exists() {
1186 return TestResult::new(
1187 test_name,
1188 "GRASP-01",
1189 "Push authorized by recursive maintainer state event",
1190 )
1191 .fail(&format!(
1192 "Maintainer B repo not created at: {}",
1193 maintainer_b_repo_path.display()
1194 ));
1195 }
1196
1197 // 5. Clone maintainer B's repo
1198 let clone_path = match clone_repo(relay_domain, &maintainer_b_npub, &repo_id) {
1199 Ok(p) => p,
1200 Err(e) => {
1201 return TestResult::new(
1202 test_name,
1203 "GRASP-01",
1204 "Push authorized by recursive maintainer state event",
1205 )
1206 .fail(&format!("Failed to clone maintainer B repo: {}", e))
1207 }
1208 };
1209
1210 // 6. Create deterministic commit
1211 let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") {
1212 Ok(h) => h,
1213 Err(e) => {
1214 let _ = fs::remove_dir_all(&clone_path);
1215 return TestResult::new(
1216 test_name,
1217 "GRASP-01",
1218 "Push authorized by recursive maintainer state event",
1219 )
1220 .fail(&format!("Failed to create commit: {}", e));
1221 }
1222 };
1223
1224 // 7. MaintainerB publishes state event with commit hash
1225 let state_event = match client
1226 .event_builder(Kind::Custom(30618), "")
1227 .tag(Tag::identifier(&repo_id))
1228 .tag(Tag::custom(
1229 TagKind::custom("refs/heads/main"),
1230 vec![commit_hash.clone()],
1231 ))
1232 .build(&maintainer_b_keys)
1233 {
1234 Ok(e) => e,
1235 Err(e) => {
1236 let _ = fs::remove_dir_all(&clone_path);
1237 return TestResult::new(
1238 test_name,
1239 "GRASP-01",
1240 "Push authorized by recursive maintainer state event",
1241 )
1242 .fail(&format!("Failed to build state event: {}", e));
1243 }
1244 };
1245
1246 if let Err(e) = client.client().send_event(&state_event).await {
1247 let _ = fs::remove_dir_all(&clone_path);
1248 return TestResult::new(
1249 test_name,
1250 "GRASP-01",
1251 "Push authorized by recursive maintainer state event",
1252 )
1253 .fail(&format!("Failed to send state event: {}", e));
1254 }
1255
1256 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1257
1258 // 8. Create and checkout main branch
1259 let branch_output = Command::new("git")
1260 .args(["branch", "main"])
1261 .current_dir(&clone_path)
1262 .output();
1263
1264 if let Ok(output) = branch_output {
1265 if !output.status.success() {
1266 let _ = fs::remove_dir_all(&clone_path);
1267 return TestResult::new(
1268 test_name,
1269 "GRASP-01",
1270 "Push authorized by recursive maintainer state event",
1271 )
1272 .fail(&format!(
1273 "Failed to create main branch: {}",
1274 String::from_utf8_lossy(&output.stderr)
1275 ));
1276 }
1277 }
1278
1279 let checkout_output = Command::new("git")
1280 .args(["checkout", "main"])
1281 .current_dir(&clone_path)
1282 .output();
1283
1284 if let Ok(output) = checkout_output {
1285 if !output.status.success() {
1286 let _ = fs::remove_dir_all(&clone_path);
1287 return TestResult::new(
1288 test_name,
1289 "GRASP-01",
1290 "Push authorized by recursive maintainer state event",
1291 )
1292 .fail(&format!(
1293 "Failed to checkout main branch: {}",
1294 String::from_utf8_lossy(&output.stderr)
1295 ));
1296 }
1297 }
1298
1299 // 9. Attempt push - should be ACCEPTED because recursive maintainer chain authorizes it
1300 // Owner -> MaintainerA -> MaintainerB, and MaintainerB has published the state event
1301 let push_result = try_push(&clone_path);
1302 let _ = fs::remove_dir_all(&clone_path);
1303
1304 match push_result {
1305 Ok(true) => TestResult::new(
1306 test_name,
1307 "GRASP-01",
1308 "Push authorized by recursive maintainer state event",
1309 )
1310 .pass(),
1311 Ok(false) => TestResult::new(
1312 test_name,
1313 "GRASP-01",
1314 "Push authorized by recursive maintainer state event",
1315 )
1316 .fail(&format!(
1317 "Push was rejected but should have been accepted. \
1318 The recursive maintainer chain is: Owner -> MaintainerA (pubkey: {}) -> MaintainerB (pubkey: {}). \
1319 MaintainerB published a state event announcing commit {}. \
1320 The relay should authorize pushes matching this state event through recursive maintainer traversal.",
1321 maintainer_a_pubkey, maintainer_b_pubkey, commit_hash
1322 )),
1323 Err(e) => TestResult::new(
1324 test_name,
1325 "GRASP-01",
1326 "Push authorized by recursive maintainer state event",
1327 )
1328 .fail(&format!("Push error: {}", e)),
1329 }
457 } 1330 }
458 1331
459 /// Test that non-maintainer state event is ignored 1332 /// Test that non-maintainer state event is ignored
@@ -540,6 +1413,279 @@ impl PushAuthorizationTests {
540 Err(e) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").fail(&e), 1413 Err(e) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").fail(&e),
541 } 1414 }
542 } 1415 }
1416
1417 /// Test that owner's newer state event beats maintainer's older state event
1418 ///
1419 /// GRASP-01 requires that the relay use the LATEST state event (by created_at
1420 /// timestamp) when determining push authorization. This test is the MIRROR of
1421 /// test_latest_state_event_used - confirming that timestamp is the deciding factor,
1422 /// not who authored the state event.
1423 ///
1424 /// Scenario:
1425 /// 1. Owner creates repo with maintainer
1426 /// 2. Maintainer publishes state event for commit_a at t=100 (older)
1427 /// 3. Owner publishes state event for commit_b at t=200 (newer)
1428 /// 4. Push commit_b should be ACCEPTED (owner's newer state wins)
1429 /// 5. Push commit_a should be REJECTED (maintainer's older state superseded)
1430 ///
1431 /// Key difference from test_latest_state_event_used:
1432 /// - Task 8: Owner=older, Maintainer=newer → Maintainer wins
1433 /// - Task 9: Maintainer=older, Owner=newer → Owner wins
1434 /// - **This confirms symmetry**: timestamp is the deciding factor
1435 pub async fn test_owner_newer_state_beats_maintainer(
1436 client: &AuditClient,
1437 git_data_dir: &Path,
1438 relay_domain: &str,
1439 ) -> TestResult {
1440 let test_name = "test_owner_newer_state_beats_maintainer";
1441 let description = "Owner's newer state event beats maintainer's older state";
1442
1443 // 1. Generate maintainer keypair
1444 let maintainer_keys = Keys::generate();
1445 let maintainer_pubkey = maintainer_keys.public_key().to_hex();
1446
1447 // 2. Owner creates repo with maintainer
1448 let repo_event = match client
1449 .create_repo_announcement_with_maintainers(test_name, &[maintainer_pubkey.clone()])
1450 .await
1451 {
1452 Ok(e) => e,
1453 Err(e) => {
1454 return TestResult::new(test_name, "GRASP-01", description)
1455 .fail(&format!("Failed to create repo with maintainers: {}", e))
1456 }
1457 };
1458
1459 // Send the owner's repo event
1460 if let Err(e) = client.send_event(repo_event.clone()).await {
1461 return TestResult::new(test_name, "GRASP-01", description)
1462 .fail(&format!("Failed to send owner repo event: {}", e));
1463 }
1464
1465 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1466
1467 // Extract repo details
1468 let repo_id = match repo_event
1469 .tags
1470 .iter()
1471 .find(|t| t.kind() == TagKind::d())
1472 .and_then(|t| t.content())
1473 {
1474 Some(id) => id.to_string(),
1475 None => {
1476 return TestResult::new(test_name, "GRASP-01", description)
1477 .fail("Repository event missing d tag")
1478 }
1479 };
1480
1481 // Get relay URL for maintainer's repo announcement
1482 let relay_url = match client.relay_url().await {
1483 Ok(u) => u,
1484 Err(e) => {
1485 return TestResult::new(test_name, "GRASP-01", description)
1486 .fail(&format!("Failed to get relay URL: {}", e))
1487 }
1488 };
1489 let http_url = relay_url
1490 .replace("ws://", "http://")
1491 .replace("wss://", "https://");
1492 let maintainer_npub = match maintainer_keys.public_key().to_bech32() {
1493 Ok(n) => n,
1494 Err(e) => {
1495 return TestResult::new(test_name, "GRASP-01", description)
1496 .fail(&format!("Failed to convert maintainer pubkey to npub: {}", e))
1497 }
1498 };
1499
1500 // 3. Maintainer creates their own repo announcement (same d-tag)
1501 let maintainer_repo_event = match client
1502 .event_builder(
1503 Kind::GitRepoAnnouncement,
1504 format!("Maintainer's view of {} repository", test_name),
1505 )
1506 .tag(Tag::identifier(&repo_id))
1507 .tag(Tag::custom(
1508 TagKind::custom("name"),
1509 vec![format!("{} Test Repository (Maintainer)", test_name)],
1510 ))
1511 .tag(Tag::custom(
1512 TagKind::custom("clone"),
1513 vec![format!("{}/{}/{}.git", http_url, maintainer_npub, repo_id)],
1514 ))
1515 .tag(Tag::custom(
1516 TagKind::custom("relays"),
1517 vec![relay_url.clone()],
1518 ))
1519 .build(&maintainer_keys)
1520 {
1521 Ok(e) => e,
1522 Err(e) => {
1523 return TestResult::new(test_name, "GRASP-01", description)
1524 .fail(&format!("Failed to build maintainer repo event: {}", e))
1525 }
1526 };
1527
1528 if let Err(e) = client.client().send_event(&maintainer_repo_event).await {
1529 return TestResult::new(test_name, "GRASP-01", description)
1530 .fail(&format!("Failed to send maintainer repo event: {}", e));
1531 }
1532
1533 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1534
1535 // Verify maintainer's repo was created
1536 let maintainer_repo_path = git_data_dir
1537 .join(&maintainer_npub)
1538 .join(format!("{}.git", repo_id));
1539 if !maintainer_repo_path.exists() {
1540 return TestResult::new(test_name, "GRASP-01", description).fail(&format!(
1541 "Maintainer repo not created at: {}",
1542 maintainer_repo_path.display()
1543 ));
1544 }
1545
1546 // 4. Clone maintainer's repo
1547 let clone_path = match clone_repo(relay_domain, &maintainer_npub, &repo_id) {
1548 Ok(p) => p,
1549 Err(e) => {
1550 return TestResult::new(test_name, "GRASP-01", description)
1551 .fail(&format!("Failed to clone maintainer repo: {}", e))
1552 }
1553 };
1554
1555 // 5. Create first commit (commit_a) - MAINTAINER will announce this with OLDER timestamp
1556 let commit_a = match create_commit(&clone_path, "Commit A - older state (maintainer)") {
1557 Ok(h) => h,
1558 Err(e) => {
1559 let _ = fs::remove_dir_all(&clone_path);
1560 return TestResult::new(test_name, "GRASP-01", description)
1561 .fail(&format!("Failed to create commit_a: {}", e));
1562 }
1563 };
1564
1565 // 6. Create second commit (commit_b) - OWNER will announce this with NEWER timestamp
1566 let commit_b = match create_commit(&clone_path, "Commit B - newer state (owner)") {
1567 Ok(h) => h,
1568 Err(e) => {
1569 let _ = fs::remove_dir_all(&clone_path);
1570 return TestResult::new(test_name, "GRASP-01", description)
1571 .fail(&format!("Failed to create commit_b: {}", e));
1572 }
1573 };
1574
1575 // 7. Calculate timestamps: older_timestamp (100 seconds ago) and newer_timestamp (now)
1576 let base_time = Timestamp::now().as_u64();
1577 let older_timestamp = Timestamp::from(base_time - 100); // 100 seconds ago - for MAINTAINER
1578 let newer_timestamp = Timestamp::from(base_time); // now - for OWNER
1579
1580 // 8. MAINTAINER publishes state event for commit_a at OLDER timestamp
1581 // This is the KEY DIFFERENCE from test_latest_state_event_used:
1582 // - In Task 8: Owner was older, Maintainer was newer
1583 // - In Task 9 (this test): Maintainer is older, Owner is newer
1584 let maintainer_state_event = match client
1585 .event_builder(Kind::Custom(30618), "")
1586 .tag(Tag::identifier(&repo_id))
1587 .tag(Tag::custom(
1588 TagKind::custom("refs/heads/main"),
1589 vec![commit_a.clone()],
1590 ))
1591 .custom_time(older_timestamp)
1592 .build(&maintainer_keys)
1593 {
1594 Ok(e) => e,
1595 Err(e) => {
1596 let _ = fs::remove_dir_all(&clone_path);
1597 return TestResult::new(test_name, "GRASP-01", description)
1598 .fail(&format!("Failed to build maintainer state event: {}", e));
1599 }
1600 };
1601
1602 if let Err(e) = client.client().send_event(&maintainer_state_event).await {
1603 let _ = fs::remove_dir_all(&clone_path);
1604 return TestResult::new(test_name, "GRASP-01", description)
1605 .fail(&format!("Failed to send maintainer state event: {}", e));
1606 }
1607
1608 // 9. OWNER publishes state event for commit_b at NEWER timestamp
1609 let owner_state_event = match client
1610 .event_builder(Kind::Custom(30618), "")
1611 .tag(Tag::identifier(&repo_id))
1612 .tag(Tag::custom(
1613 TagKind::custom("refs/heads/main"),
1614 vec![commit_b.clone()],
1615 ))
1616 .custom_time(newer_timestamp)
1617 .build(client.keys())
1618 {
1619 Ok(e) => e,
1620 Err(e) => {
1621 let _ = fs::remove_dir_all(&clone_path);
1622 return TestResult::new(test_name, "GRASP-01", description)
1623 .fail(&format!("Failed to build owner state event: {}", e));
1624 }
1625 };
1626
1627 if let Err(e) = client.client().send_event(&owner_state_event).await {
1628 let _ = fs::remove_dir_all(&clone_path);
1629 return TestResult::new(test_name, "GRASP-01", description)
1630 .fail(&format!("Failed to send owner state event: {}", e));
1631 }
1632
1633 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1634
1635 // 10. Create and checkout main branch pointing to commit_b (the newer state)
1636 let branch_output = Command::new("git")
1637 .args(["branch", "main"])
1638 .current_dir(&clone_path)
1639 .output();
1640
1641 if let Ok(output) = branch_output {
1642 if !output.status.success() {
1643 let _ = fs::remove_dir_all(&clone_path);
1644 return TestResult::new(test_name, "GRASP-01", description).fail(&format!(
1645 "Failed to create main branch: {}",
1646 String::from_utf8_lossy(&output.stderr)
1647 ));
1648 }
1649 }
1650
1651 let checkout_output = Command::new("git")
1652 .args(["checkout", "main"])
1653 .current_dir(&clone_path)
1654 .output();
1655
1656 if let Ok(output) = checkout_output {
1657 if !output.status.success() {
1658 let _ = fs::remove_dir_all(&clone_path);
1659 return TestResult::new(test_name, "GRASP-01", description).fail(&format!(
1660 "Failed to checkout main branch: {}",
1661 String::from_utf8_lossy(&output.stderr)
1662 ));
1663 }
1664 }
1665
1666 // 11. Attempt push - should be ACCEPTED because owner's newer state event
1667 // announces commit_b which is now HEAD of main
1668 let push_result = try_push(&clone_path);
1669 let _ = fs::remove_dir_all(&clone_path);
1670
1671 match push_result {
1672 Ok(true) => TestResult::new(test_name, "GRASP-01", description).pass(),
1673 Ok(false) => TestResult::new(test_name, "GRASP-01", description).fail(&format!(
1674 "Push was rejected but should have been accepted. \
1675 The OWNER published a state event at timestamp {} announcing commit_b ({}). \
1676 The MAINTAINER published an older state event at timestamp {} announcing commit_a ({}). \
1677 The relay should use the NEWER state event (owner's) for authorization. \
1678 This confirms symmetry with test_latest_state_event_used: timestamp is the deciding factor.",
1679 newer_timestamp.as_u64(),
1680 commit_b,
1681 older_timestamp.as_u64(),
1682 commit_a
1683 )),
1684 Err(e) => {
1685 TestResult::new(test_name, "GRASP-01", description).fail(&format!("Push error: {}", e))
1686 }
1687 }
1688 }
543} 1689}
544 1690
545#[cfg(test)] 1691#[cfg(test)]