diff options
Diffstat (limited to 'grasp-audit/src')
| -rw-r--r-- | grasp-audit/src/specs/grasp01/event_acceptance_policy.rs | 149 |
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 | ||
| 91 | use crate::fixtures::{send_and_verify_accepted, send_and_verify_rejected}; | 94 | use crate::fixtures::{send_and_verify_accepted, send_and_verify_rejected}; |
| 92 | use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; | 95 | use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; |
| 93 | use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp}; | 96 | use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp, ToBech32}; |
| 94 | use std::time::Duration; | 97 | use 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 |