upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/specs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-03 17:06:59 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-03 17:06:59 +0000
commitde683147779eaf57376a90e73bbdd123846a01e3 (patch)
tree3f4ecaf6e49bdb3d7b338df669ecd8cead98e2e6 /grasp-audit/src/specs
parentdd1b44132199aa72c2b699e1160fbe6b885f0ef6 (diff)
feat: accept maintainer announcements without service listing
Diffstat (limited to 'grasp-audit/src/specs')
-rw-r--r--grasp-audit/src/specs/grasp01/event_acceptance_policy.rs149
1 files changed, 142 insertions, 7 deletions
diff --git a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs
index 6474399..c34fe66 100644
--- a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs
+++ b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs
@@ -5,6 +5,9 @@
5//! This file validates that a GRASP-01 compliant relay: 5//! This file validates that a GRASP-01 compliant relay:
6//! - Accepts valid NIP-34 repository announcements listing the service 6//! - Accepts valid NIP-34 repository announcements listing the service
7//! - Rejects announcements that don't list the service in clone and relays tags 7//! - Rejects announcements that don't list the service in clone and relays tags
8//! EXCEPTION: maintainer announcements (from authors in the maintainer chain)
9//! MUST be accepted even without listing the service - this enables recursive maintainer
10//! chain discovery and more reliable GRASP-02 sync capabilities
8//! - Accepts repository state announcements 11//! - Accepts repository state announcements
9//! - Accepts events that TAG accepted repositories 12//! - Accepts events that TAG accepted repositories
10//! - Accepts events that ARE TAGGED BY accepted events (transitive) 13//! - Accepts events that ARE TAGGED BY accepted events (transitive)
@@ -90,7 +93,7 @@
90 93
91use crate::fixtures::{send_and_verify_accepted, send_and_verify_rejected}; 94use crate::fixtures::{send_and_verify_accepted, send_and_verify_rejected};
92use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; 95use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult};
93use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp}; 96use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp, ToBech32};
94use std::time::Duration; 97use std::time::Duration;
95 98
96/// Test suite for GRASP-01 event acceptance policy 99/// Test suite for GRASP-01 event acceptance policy
@@ -105,6 +108,7 @@ impl EventAcceptancePolicyTests {
105 results.add(Self::test_accept_valid_repo_announcement(client).await); 108 results.add(Self::test_accept_valid_repo_announcement(client).await);
106 results.add(Self::test_reject_repo_announcement_missing_clone_tag(client).await); 109 results.add(Self::test_reject_repo_announcement_missing_clone_tag(client).await);
107 results.add(Self::test_reject_repo_announcement_missing_relays_tag(client).await); 110 results.add(Self::test_reject_repo_announcement_missing_relays_tag(client).await);
111 results.add(Self::test_accept_maintainer_announcement_without_service_listed(client).await);
108 112
109 // Repository State Announcement Tests 113 // Repository State Announcement Tests
110 results.add(Self::test_accept_valid_repo_state_announcement(client).await); 114 results.add(Self::test_accept_valid_repo_state_announcement(client).await);
@@ -404,6 +408,134 @@ impl EventAcceptancePolicyTests {
404 .await 408 .await
405 } 409 }
406 410
411 /// Test: Accept recursive maintainer announcement without service in clone tag
412 ///
413 /// Spec: Line 9 of ../grasp/01.md (EXCEPTION to rejection rule)
414 /// Requirement: MUST accept recursive maintainer announcements for chain discovery
415 ///
416 /// GRASP-01: "respecting the recursive maintainer set"
417 ///
418 /// When a recursive maintainer is listed in a maintainer's announcement, they may
419 /// publish their own announcement for the same repo (with their own maintainers).
420 /// The relay MUST accept this recursive maintainer's announcement even if it doesn't
421 /// list this GRASP server in its clone tag - because the relay needs it to discover
422 /// the full recursive maintainer chain.
423 ///
424 /// This also enables GRASP-02 to sync state events and git data when authoritative
425 /// users publish them to other relays/git servers, keeping repos up-to-date.
426 pub async fn test_accept_maintainer_announcement_without_service_listed(
427 client: &AuditClient,
428 ) -> TestResult {
429 TestResult::new(
430 "accept_recursive_maintainer_announcement_without_service",
431 "GRASP-01:nostr-relay:9",
432 "Accept recursive maintainer announcement for chain discovery (even without GRASP server in clone)",
433 )
434 .run(|| async {
435 // Create TestContext for mode-aware fixture management
436 let ctx = TestContext::new(client);
437
438 // Step 1: Get RecursiveMaintainerStateDataPushed fixture
439 // This establishes: Owner -> Maintainer -> RecursiveMaintainer chain
440 // with all git data pushed. The recursive maintainer is already listed
441 // in maintainer's announcement (and maintainer in owner's announcement).
442 let recursive_state = ctx
443 .get_fixture(FixtureKind::RecursiveMaintainerStateDataPushed)
444 .await
445 .map_err(|e| {
446 format!(
447 "Test setup failed: could not get RecursiveMaintainerStateDataPushed fixture: {}",
448 e
449 )
450 })?;
451
452 // Extract repo_id from the recursive maintainer's state event
453 let repo_id = recursive_state
454 .tags
455 .iter()
456 .find(|t| t.kind() == TagKind::d())
457 .and_then(|t| t.content())
458 .ok_or("Missing d tag in recursive maintainer state")?
459 .to_string();
460
461 // Step 2: Build a recursive maintainer announcement that DOES NOT include
462 // this GRASP server in its clone tag - simulating an announcement pointing
463 // to a different server (e.g., another GRASP server)
464 let recursive_maintainer_npub = client
465 .recursive_maintainer_keys()
466 .public_key()
467 .to_bech32()
468 .map_err(|e| format!("Failed to convert recursive maintainer pubkey: {}", e))?;
469
470 // Create announcement with external clone URL (not this server)
471 let recursive_maintainer_announcement = client
472 .event_builder(
473 Kind::GitRepoAnnouncement,
474 format!(
475 "Recursive maintainer announcement for {} (external clone)",
476 repo_id
477 ),
478 )
479 .tag(Tag::identifier(&repo_id))
480 .tag(Tag::custom(
481 TagKind::custom("name"),
482 vec![format!("{} (recursive maintainer view)", repo_id)],
483 ))
484 // Clone points to another server, NOT the GRASP server
485 .tag(Tag::custom(
486 TagKind::custom("clone"),
487 vec![format!(
488 "https://another-grasp-server.com/{}/{}.git",
489 recursive_maintainer_npub, repo_id
490 )],
491 ))
492 // Relays also points elsewhere (not this server)
493 .tag(Tag::custom(
494 TagKind::custom("relays"),
495 vec!["wss://relay.damus.io"],
496 ))
497 .build(client.recursive_maintainer_keys())
498 .map_err(|e| format!("Failed to build recursive maintainer announcement: {}", e))?;
499
500 let event_id = recursive_maintainer_announcement.id;
501
502 // Step 3: Send the recursive maintainer announcement
503 client
504 .send_event(recursive_maintainer_announcement)
505 .await
506 .map_err(|e| format!("Failed to send recursive maintainer announcement: {}", e))?;
507
508 // Wait for propagation
509 tokio::time::sleep(Duration::from_millis(200)).await;
510
511 // Step 4: Query to verify it was accepted
512 let filter = Filter::new()
513 .kind(Kind::GitRepoAnnouncement)
514 .author(client.recursive_maintainer_keys().public_key())
515 .identifier(&repo_id);
516
517 let events = client
518 .query(filter)
519 .await
520 .map_err(|e| format!("Failed to query events: {}", e))?;
521
522 // Verify the recursive maintainer's announcement was stored
523 if !events.iter().any(|e| e.id == event_id) {
524 return Err(format!(
525 "Recursive maintainer announcement was NOT accepted by relay. \
526 The recursive maintainer (listed in maintainer's announcement, which is \
527 listed in owner's announcement) published their own announcement for \
528 repo {} with an external clone URL. The relay should accept this to \
529 enable full recursive maintainer chain discovery. Event ID: {}",
530 repo_id, event_id
531 ));
532 }
533
534 Ok(())
535 })
536 .await
537 }
538
407 // ============================================================ 539 // ============================================================
408 // Repository State Announcement Tests 540 // Repository State Announcement Tests
409 // ============================================================ 541 // ============================================================
@@ -432,12 +564,15 @@ impl EventAcceptancePolicyTests {
432 // 2. Pushes git data with the deterministic commit 564 // 2. Pushes git data with the deterministic commit
433 // 3. Sends the state announcement 565 // 3. Sends the state announcement
434 // This ensures the state event references a commit that actually exists 566 // This ensures the state event references a commit that actually exists
435 let state_event = ctx.get_fixture(FixtureKind::OwnerStateDataPushed).await.map_err(|e| { 567 let state_event = ctx
436 format!( 568 .get_fixture(FixtureKind::OwnerStateDataPushed)
437 "Test setup failed: could not get repository state fixture: {}", 569 .await
438 e 570 .map_err(|e| {
439 ) 571 format!(
440 })?; 572 "Test setup failed: could not get repository state fixture: {}",
573 e
574 )
575 })?;
441 576
442 // Extract repo_id from the state event 577 // Extract repo_id from the state event
443 let repo_id = state_event 578 let repo_id = state_event