diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-23 15:41:32 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-23 15:41:32 +0000 |
| commit | c54ce061d6d278cce8362d5af085808ca60c239b (patch) | |
| tree | ec967d6195d9f7ec4f061449596611afe3a0950f /grasp-audit/src | |
| parent | e0ad39a489b3398f8208713bf728db0cb11475b0 (diff) | |
| parent | 113928aa84894ea8f65c247d9987527e792b32a9 (diff) | |
feat: announcement purgatory
Extends purgatory to hold repository announcements until git data arrives,
preventing empty repositories from being served to clients.
When an announcement is received, a bare repo is created immediately and the
announcement is held in purgatory. It is only promoted and served once a git
push confirms real content exists. If no push arrives before expiry, the bare
repo is deleted and the announcement is silently discarded.
Key behaviours:
- Soft expiry: announcements are hidden from clients but kept alive while git
pushes are in progress, reviving on successful push
- Expiry is extended when a matching state event or git push is observed
- NIP-09 deletion events remove announcements from purgatory
- Purgatory state (announcements, state events, PR events, expired set) is
persisted to disk on graceful shutdown and restored on startup, with elapsed
downtime subtracted from expiry deadlines
- Purgatory announcements drive StateOnly sync in the sync system so state
events are fetched from listed relays before promotion
- SyncLevel added to RepoSyncIndex to distinguish purgatory repos (StateOnly)
from promoted repos (Full L2+L3 sync)
Diffstat (limited to 'grasp-audit/src')
| -rw-r--r-- | grasp-audit/src/client.rs | 30 | ||||
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 522 | ||||
| -rw-r--r-- | grasp-audit/src/result.rs | 43 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/cors.rs | 45 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/event_acceptance_policy.rs | 165 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/git_clone.rs | 49 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/git_filter.rs | 43 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/mod.rs | 6 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/nip01_smoke.rs | 39 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/nip11_document.rs | 17 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/purgatory.rs | 983 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/push_authorization.rs | 232 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/repository_creation.rs | 35 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/spec_requirements.rs | 150 | ||||
| -rw-r--r-- | grasp-audit/src/specs/mod.rs | 2 |
15 files changed, 1918 insertions, 443 deletions
diff --git a/grasp-audit/src/client.rs b/grasp-audit/src/client.rs index 91a93dc..5c263ad 100644 --- a/grasp-audit/src/client.rs +++ b/grasp-audit/src/client.rs | |||
| @@ -209,6 +209,36 @@ impl AuditClient { | |||
| 209 | Ok(event_id) | 209 | Ok(event_id) |
| 210 | } | 210 | } |
| 211 | 211 | ||
| 212 | /// Send event and note whether it entered purgatory (not served) or was served immediately. | ||
| 213 | /// | ||
| 214 | /// This is a tolerant version of `send_event_expect_purgatory_not_served` that doesn't | ||
| 215 | /// fail if purgatory is not observed. It returns whether purgatory was observed so | ||
| 216 | /// fixtures can proceed regardless of relay implementation status. | ||
| 217 | /// | ||
| 218 | /// Returns (EventId, bool) where bool = true if event was NOT served (purgatory observed). | ||
| 219 | pub async fn send_event_and_note_purgatory(&self, event: Event) -> Result<(EventId, bool)> { | ||
| 220 | if self.config.read_only { | ||
| 221 | return Err(anyhow!("Client is in read-only mode")); | ||
| 222 | } | ||
| 223 | |||
| 224 | let output = self.client.send_event(&event).await?; | ||
| 225 | let event_id = *output.id(); | ||
| 226 | |||
| 227 | // Check if any relay rejected the event and return the error message | ||
| 228 | if !output.failed.is_empty() { | ||
| 229 | let (relay_url, error) = output.failed.iter().next().unwrap(); | ||
| 230 | return Err(anyhow!("Relay {} rejected event: {}", relay_url, error)); | ||
| 231 | } | ||
| 232 | |||
| 233 | // Wait a bit for event to propagate | ||
| 234 | tokio::time::sleep(Duration::from_millis(300)).await; | ||
| 235 | |||
| 236 | // Check if event is served (not in purgatory) or not served (in purgatory) | ||
| 237 | let in_purgatory = !self.is_event_on_relay(event.id).await?; | ||
| 238 | |||
| 239 | Ok((event_id, in_purgatory)) | ||
| 240 | } | ||
| 241 | |||
| 212 | /// check if an event is on the relay | 242 | /// check if an event is on the relay |
| 213 | pub async fn is_event_on_relay(&self, id: EventId) -> Result<bool> { | 243 | pub async fn is_event_on_relay(&self, id: EventId) -> Result<bool> { |
| 214 | Ok(!self | 244 | Ok(!self |
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index bbc7740..45d3094 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs | |||
| @@ -47,7 +47,7 @@ | |||
| 47 | //! let ctx = TestContext::new(&client); | 47 | //! let ctx = TestContext::new(&client); |
| 48 | //! | 48 | //! |
| 49 | //! // Request a fixture - behavior depends on mode | 49 | //! // Request a fixture - behavior depends on mode |
| 50 | //! let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?; | 50 | //! let repo = ctx.get_fixture(FixtureKind::ValidRepoSent).await?; |
| 51 | //! # Ok(()) | 51 | //! # Ok(()) |
| 52 | //! # } | 52 | //! # } |
| 53 | //! ``` | 53 | //! ``` |
| @@ -61,59 +61,68 @@ use std::sync::{Arc, Mutex}; | |||
| 61 | /// Deterministic commit hash used in RepoState fixtures (Owner variant) | 61 | /// Deterministic commit hash used in RepoState fixtures (Owner variant) |
| 62 | /// This is the hash produced by creating a commit with: | 62 | /// This is the hash produced by creating a commit with: |
| 63 | /// - Message: "Initial commit" | 63 | /// - Message: "Initial commit" |
| 64 | /// - File: test.txt containing "Initial commit" | 64 | /// - File: test.txt containing "Initial commit\n" (with trailing newline) |
| 65 | /// - Author date: 2024-01-01T00:00:00Z | 65 | /// - Author date: 2024-01-01T00:00:00Z |
| 66 | /// - Committer date: 2024-01-01T00:00:00Z | 66 | /// - Committer date: 2024-01-01T00:00:00Z |
| 67 | /// - GPG signing: disabled | 67 | /// - GPG signing: disabled |
| 68 | /// - User: "GRASP Audit Test <test@grasp-audit.local>" | 68 | /// - User: "GRASP Audit Test <test@grasp-audit.local>" |
| 69 | /// - Parent: Initial empty commit (09cc37de80f3434fa98864a86730b8d7777bd6ae) | 69 | /// - Parent: none (root commit) |
| 70 | pub const DETERMINISTIC_COMMIT_HASH: &str = "64ea71d79a57a7acb334cd9651f8aec067c0ce5d"; | 70 | pub const DETERMINISTIC_COMMIT_HASH: &str = "d6e4b26ccf9c268d18d60e6d09804313cc850821"; |
| 71 | 71 | ||
| 72 | /// Deterministic commit hash for maintainer fixtures (Maintainer variant) | 72 | /// Deterministic commit hash for maintainer fixtures (Maintainer variant) |
| 73 | /// This is the hash produced by creating a commit with: | 73 | /// This is the hash produced by creating a commit with: |
| 74 | /// - Message: "Maintainer initial commit" | 74 | /// - Message: "Maintainer initial commit" |
| 75 | /// - File: test.txt containing "Maintainer initial commit" | 75 | /// - File: test.txt containing "Maintainer initial commit\n" (with trailing newline) |
| 76 | /// - Author date: 2024-01-01T00:00:00Z | 76 | /// - Author date: 2024-01-01T00:00:00Z |
| 77 | /// - Committer date: 2024-01-01T00:00:00Z | 77 | /// - Committer date: 2024-01-01T00:00:00Z |
| 78 | /// - GPG signing: disabled | 78 | /// - GPG signing: disabled |
| 79 | /// - User: "GRASP Audit Test <test@grasp-audit.local>" | 79 | /// - User: "GRASP Audit Test <test@grasp-audit.local>" |
| 80 | /// - Parent: none (root commit) | 80 | /// - Parent: none (root commit) |
| 81 | /// NOTE: This value is different from DETERMINISTIC_COMMIT_HASH due to different content | 81 | pub const MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = "d26703c007eff6d17fee3bb70ce8be5d1427d0e7"; |
| 82 | pub const MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = "1c2d472c9b71ed51968a66500281a3c4a6840464"; | ||
| 83 | 82 | ||
| 84 | /// Deterministic commit hash for recursive maintainer fixtures (RecursiveMaintainer variant) | 83 | /// Deterministic commit hash for recursive maintainer fixtures (RecursiveMaintainer variant) |
| 85 | /// This is the hash produced by creating a commit with: | 84 | /// This is the hash produced by creating a commit with: |
| 86 | /// - Message: "Recursive maintainer initial commit" | 85 | /// - Message: "Recursive maintainer initial commit" |
| 87 | /// - File: test.txt containing "Recursive maintainer initial commit" | 86 | /// - File: test.txt containing "Recursive maintainer initial commit\n" (with trailing newline) |
| 88 | /// - Author date: 2024-01-01T00:00:00Z | 87 | /// - Author date: 2024-01-01T00:00:00Z |
| 89 | /// - Committer date: 2024-01-01T00:00:00Z | 88 | /// - Committer date: 2024-01-01T00:00:00Z |
| 90 | /// - GPG signing: disabled | 89 | /// - GPG signing: disabled |
| 91 | /// - User: "GRASP Audit Test <test@grasp-audit.local>" | 90 | /// - User: "GRASP Audit Test <test@grasp-audit.local>" |
| 92 | /// - Parent: none (root commit) | 91 | /// - Parent: none (root commit) |
| 93 | /// NOTE: This value is different from DETERMINISTIC_COMMIT_HASH due to different content | ||
| 94 | pub const RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = | 92 | pub const RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = |
| 95 | "05939b82de66fbdb9c077d0a64fc68522f3cb8e0"; | 93 | "54a2b4b3cbc3373ad1438b8ffad1681d12bc6c4a"; |
| 96 | 94 | ||
| 97 | /// Deterministic commit hash for PR test fixtures (PRTestCommit variant) | 95 | /// Deterministic commit hash for PR test fixtures (PRTestCommit variant) |
| 98 | /// This is the hash produced by creating a commit with: | 96 | /// This is the hash produced by creating a commit with: |
| 99 | /// - Message: "PR test deterministic commit" | 97 | /// - Message: "PR test deterministic commit" |
| 100 | /// - File: test.txt containing "PR test deterministic commit" | 98 | /// - File: test.txt containing "PR test deterministic commit\n" (with trailing newline) |
| 99 | /// - Author date: 2024-01-01T00:00:00Z | ||
| 100 | /// - Committer date: 2024-01-01T00:00:00Z | ||
| 101 | /// - GPG signing: disabled | ||
| 102 | /// - User: "GRASP Audit Test <test@grasp-audit.local>" | ||
| 103 | /// - Parent: none (root commit) | ||
| 104 | pub const PR_TEST_COMMIT_HASH: &str = "5a51b30e4615b572dcd5b9e487861b58605a5c21"; | ||
| 105 | |||
| 106 | /// Deterministic commit hash for second PR test fixtures (PRTestCommit2 variant) | ||
| 107 | /// This is the hash produced by creating a commit with: | ||
| 108 | /// - Message: "PR test deterministic commit 2" | ||
| 109 | /// - File: test.txt containing "PR test deterministic commit 2\n" (with trailing newline) | ||
| 101 | /// - Author date: 2024-01-01T00:00:00Z | 110 | /// - Author date: 2024-01-01T00:00:00Z |
| 102 | /// - Committer date: 2024-01-01T00:00:00Z | 111 | /// - Committer date: 2024-01-01T00:00:00Z |
| 103 | /// - GPG signing: disabled | 112 | /// - GPG signing: disabled |
| 104 | /// - User: "GRASP Audit Test <test@grasp-audit.local>" | 113 | /// - User: "GRASP Audit Test <test@grasp-audit.local>" |
| 105 | /// - Parent: none (root commit) | 114 | /// - Parent: none (root commit) |
| 106 | pub const PR_TEST_COMMIT_HASH: &str = "5d40fb1555a0c28bf4d650515a73aaa54d4d9bfb"; | 115 | pub const PR_TEST_COMMIT_HASH_2: &str = "99420bc57835f5bc8ca20ab21a8d12850043920e"; |
| 107 | 116 | ||
| 108 | /// Types of test fixtures available | 117 | /// Types of test fixtures available |
| 109 | /// | 118 | /// |
| 110 | /// ## Fixture Dependencies | 119 | /// ## Fixture Dependencies |
| 111 | /// | 120 | /// |
| 112 | /// Several fixtures depend on `ValidRepo` - they all use the SAME repo_id | 121 | /// Several fixtures depend on `ValidRepoSent` - they all use the SAME repo_id |
| 113 | /// within a single TestContext instance to ensure proper fixture relationships: | 122 | /// within a single TestContext instance to ensure proper fixture relationships: |
| 114 | /// - `RepoState` → uses ValidRepo's repo_id | 123 | /// - `RepoState` → uses ValidRepoSent's repo_id |
| 115 | /// - `MaintainerAnnouncement` + `MaintainerState` → uses ValidRepo's repo_id | 124 | /// - `MaintainerAnnouncement` + `MaintainerState` → uses ValidRepoSent's repo_id |
| 116 | /// - `RecursiveMaintainerRepoAndState` → uses ValidRepo's repo_id | 125 | /// - `RecursiveMaintainerRepoAndState` → uses ValidRepoSent's repo_id |
| 117 | /// | 126 | /// |
| 118 | /// This enables testing recursive maintainer authorization chains where multiple | 127 | /// This enables testing recursive maintainer authorization chains where multiple |
| 119 | /// parties publish announcements and state events for the same repository. | 128 | /// parties publish announcements and state events for the same repository. |
| @@ -122,10 +131,16 @@ pub enum FixtureKind { | |||
| 122 | /// Basic repository announcement (kind 30617) | 131 | /// Basic repository announcement (kind 30617) |
| 123 | /// - Signed by owner keys (`client.keys()`) | 132 | /// - Signed by owner keys (`client.keys()`) |
| 124 | /// - Lists `client.maintainer_pubkey_hex()` in maintainers tag | 133 | /// - Lists `client.maintainer_pubkey_hex()` in maintainers tag |
| 125 | ValidRepo, | 134 | ValidRepoSent, |
| 135 | |||
| 136 | /// Repository announcement that is queryable from the relay (served, not in purgatory) | ||
| 137 | /// - Depends on OwnerStateDataPushed (git data pushed, announcement promoted) | ||
| 138 | /// - Returns the same event as ValidRepoSent (now queryable) | ||
| 139 | /// - Use this for tests that need to query the announcement back from the relay | ||
| 140 | ValidRepoServed, | ||
| 126 | 141 | ||
| 127 | /// Repository with one issue (kind 1621) | 142 | /// Repository with one issue (kind 1621) |
| 128 | /// - Requires ValidRepo (reuses same repo_id) | 143 | /// - Requires ValidRepoServed (needs queryable repo for issue to reference) |
| 129 | RepoWithIssue, | 144 | RepoWithIssue, |
| 130 | 145 | ||
| 131 | /// Repository with issue and comment (kind 1111) | 146 | /// Repository with issue and comment (kind 1111) |
| @@ -133,14 +148,30 @@ pub enum FixtureKind { | |||
| 133 | RepoWithComment, | 148 | RepoWithComment, |
| 134 | 149 | ||
| 135 | /// Repository state announcement (kind 30618) for owner | 150 | /// Repository state announcement (kind 30618) for owner |
| 136 | /// - Requires ValidRepo (uses same repo_id) | 151 | /// - Requires ValidRepoSent (uses same repo_id) |
| 137 | /// - Signed by owner keys (`client.keys()`) | 152 | /// - Signed by owner keys (`client.keys()`) |
| 138 | /// - Points to DETERMINISTIC_COMMIT_HASH | 153 | /// - Points to DETERMINISTIC_COMMIT_HASH |
| 139 | /// - Timestamp: 10 seconds in the past | 154 | /// - Timestamp: 10 seconds in the past |
| 140 | RepoState, | 155 | RepoState, |
| 141 | 156 | ||
| 142 | /// PR (Pull Request) event for the SAME repo_id as ValidRepo | 157 | /// Owner's repository state announcement (kind 30618) sent to relay and accepted into purgatory |
| 143 | /// - Requires ValidRepo (uses same repo_id) | 158 | /// |
| 159 | /// This is the "sent" stage: the state event has been published to the relay and | ||
| 160 | /// accepted (OK response), but no git data has been pushed yet so it remains in | ||
| 161 | /// purgatory and is not served to clients. | ||
| 162 | /// | ||
| 163 | /// Use this when you need the state event to exist on the relay but do not need | ||
| 164 | /// the full push/serve cycle. For the complete cycle (git pushed + verified served), | ||
| 165 | /// use `OwnerStateDataPushed`. | ||
| 166 | /// | ||
| 167 | /// - Requires ValidRepoSent (uses same repo_id) | ||
| 168 | /// - Signed by owner keys (`client.keys()`) | ||
| 169 | /// - Points to DETERMINISTIC_COMMIT_HASH | ||
| 170 | /// - Timestamp: 10 seconds in the past | ||
| 171 | OwnerRepoStateSent, | ||
| 172 | |||
| 173 | /// PR (Pull Request) event for the SAME repo_id as ValidRepoServed | ||
| 174 | /// - Requires ValidRepoServed (uses same repo_id, needs queryable repo) | ||
| 144 | /// - Signed by `client.pr_author_keys()` | 175 | /// - Signed by `client.pr_author_keys()` |
| 145 | /// - Kind 1618 (NIP-34 PR) | 176 | /// - Kind 1618 (NIP-34 PR) |
| 146 | /// - Includes `a` tag referencing the repo | 177 | /// - Includes `a` tag referencing the repo |
| @@ -153,7 +184,7 @@ pub enum FixtureKind { | |||
| 153 | /// This is a "Generated" stage fixture - the event is created but not published. | 184 | /// This is a "Generated" stage fixture - the event is created but not published. |
| 154 | /// Useful for tests that need the PR event ID before the event exists on the relay. | 185 | /// Useful for tests that need the PR event ID before the event exists on the relay. |
| 155 | /// | 186 | /// |
| 156 | /// - Requires ValidRepo (uses same repo_id) | 187 | /// - Requires ValidRepoServed (uses same repo_id, needs queryable repo) |
| 157 | /// - Signed by `client.pr_author_keys()` | 188 | /// - Signed by `client.pr_author_keys()` |
| 158 | /// - Kind 1618 (NIP-34 PR) | 189 | /// - Kind 1618 (NIP-34 PR) |
| 159 | /// - Includes `c` tag pointing to PR_TEST_COMMIT_HASH | 190 | /// - Includes `c` tag pointing to PR_TEST_COMMIT_HASH |
| @@ -187,7 +218,7 @@ pub enum FixtureKind { | |||
| 187 | /// (the "wrong" commit), but no PR event exists yet on the relay. | 218 | /// (the "wrong" commit), but no PR event exists yet on the relay. |
| 188 | /// | 219 | /// |
| 189 | /// Server state after this fixture: | 220 | /// Server state after this fixture: |
| 190 | /// - ValidRepo announcement on relay | 221 | /// - ValidRepoServed announcement on relay (repo is queryable) |
| 191 | /// - refs/nostr/<pr-event-id> exists on git server with wrong commit | 222 | /// - refs/nostr/<pr-event-id> exists on git server with wrong commit |
| 192 | /// - PR event is NOT on relay (but returned for tests to publish later) | 223 | /// - PR event is NOT on relay (but returned for tests to publish later) |
| 193 | /// | 224 | /// |
| @@ -203,7 +234,7 @@ pub enum FixtureKind { | |||
| 203 | /// then the PR event was published (which may trigger cleanup). | 234 | /// then the PR event was published (which may trigger cleanup). |
| 204 | /// | 235 | /// |
| 205 | /// Server state after this fixture: | 236 | /// Server state after this fixture: |
| 206 | /// - ValidRepo announcement on relay | 237 | /// - ValidRepoServed announcement on relay |
| 207 | /// - PR event is on relay | 238 | /// - PR event is on relay |
| 208 | /// - refs/nostr/<pr-event-id> may have been cleaned up (that's what tests verify) | 239 | /// - refs/nostr/<pr-event-id> may have been cleaned up (that's what tests verify) |
| 209 | /// | 240 | /// |
| @@ -212,6 +243,50 @@ pub enum FixtureKind { | |||
| 212 | /// - Returns: the sent PR event | 243 | /// - Returns: the sent PR event |
| 213 | PREventSentAfterWrongPush, | 244 | PREventSentAfterWrongPush, |
| 214 | 245 | ||
| 246 | /// Second PR event generated (built) but NOT sent to relay | ||
| 247 | /// | ||
| 248 | /// Uses PR_TEST_COMMIT_HASH_2 (different from PR_TEST_COMMIT_HASH). | ||
| 249 | /// This allows testing purgatory mechanism with a separate PR event | ||
| 250 | /// that doesn't conflict with existing PR fixtures. | ||
| 251 | /// | ||
| 252 | /// - Requires ValidRepoServed (uses same repo_id, needs git data to exist) | ||
| 253 | /// - Signed by `client.pr_author_keys()` | ||
| 254 | /// - Kind 1618 (NIP-34 PR) | ||
| 255 | /// - Includes `c` tag pointing to PR_TEST_COMMIT_HASH_2 | ||
| 256 | /// - NOT sent to relay | ||
| 257 | PREvent2Generated, | ||
| 258 | |||
| 259 | /// Second PR event sent to relay (enters purgatory) | ||
| 260 | /// | ||
| 261 | /// After this fixture: | ||
| 262 | /// - PR event is on relay but NOT served (in purgatory) | ||
| 263 | /// - No git data at refs/nostr/<pr-event-id> | ||
| 264 | /// | ||
| 265 | /// - Requires PREvent2Generated | ||
| 266 | /// - Sends the PR event to relay | ||
| 267 | /// - Returns: the sent PR event (in purgatory) | ||
| 268 | PREvent2Sent, | ||
| 269 | |||
| 270 | /// Git data pushed for second PR event AFTER event was sent | ||
| 271 | /// | ||
| 272 | /// After this fixture: | ||
| 273 | /// - PR event was in purgatory | ||
| 274 | /// - Correct commit pushed to refs/nostr/<pr-event-id> | ||
| 275 | /// - PR event should be released from purgatory | ||
| 276 | /// | ||
| 277 | /// - Requires PREvent2Sent | ||
| 278 | /// - Pushes correct commit (PR_TEST_COMMIT_HASH_2) to refs/nostr/<pr-event-id> | ||
| 279 | /// - Returns: the PR event (should now be served) | ||
| 280 | PREvent2GitDataPushed, | ||
| 281 | |||
| 282 | /// Full fixture: second PR event sent, git pushed, event served | ||
| 283 | /// | ||
| 284 | /// Combines PREvent2Sent + PREvent2GitDataPushed for convenience. | ||
| 285 | /// | ||
| 286 | /// - Requires PREvent2GitDataPushed | ||
| 287 | /// - Returns: the served PR event | ||
| 288 | PREvent2Served, | ||
| 289 | |||
| 215 | /// Owner's state event with git data successfully pushed (full 4-stage fixture) | 290 | /// Owner's state event with git data successfully pushed (full 4-stage fixture) |
| 216 | /// | 291 | /// |
| 217 | /// This fixture represents the complete flow for testing state push authorization: | 292 | /// This fixture represents the complete flow for testing state push authorization: |
| @@ -221,7 +296,7 @@ pub enum FixtureKind { | |||
| 221 | /// 4. **DataPushed**: Clones repo, creates deterministic commit, pushes to relay | 296 | /// 4. **DataPushed**: Clones repo, creates deterministic commit, pushes to relay |
| 222 | /// 5. **Verified**: Confirms event is served by relay | 297 | /// 5. **Verified**: Confirms event is served by relay |
| 223 | /// | 298 | /// |
| 224 | /// - Requires ValidRepo (uses same repo_id) | 299 | /// - Requires ValidRepoSent (uses same repo_id) |
| 225 | /// - State event signed by owner keys (`client.keys()`) | 300 | /// - State event signed by owner keys (`client.keys()`) |
| 226 | /// - Points to DETERMINISTIC_COMMIT_HASH | 301 | /// - Points to DETERMINISTIC_COMMIT_HASH |
| 227 | /// - Git push verified to succeed (state matches pushed commit) | 302 | /// - Git push verified to succeed (state matches pushed commit) |
| @@ -252,7 +327,7 @@ pub enum FixtureKind { | |||
| 252 | /// not the owner's announcement, so this tests the recursive maintainer traversal. | 327 | /// not the owner's announcement, so this tests the recursive maintainer traversal. |
| 253 | /// | 328 | /// |
| 254 | /// This fixture represents the complete flow for testing recursive maintainer push authorization: | 329 | /// This fixture represents the complete flow for testing recursive maintainer push authorization: |
| 255 | /// 1. **Generated**: (MaintainerStateDataPushed dependency includes ValidRepo + OwnerStateDataPushed) | 330 | /// 1. **Generated**: (MaintainerStateDataPushed dependency includes ValidRepoSent + OwnerStateDataPushed) |
| 256 | /// Creates MaintainerAnnouncement + RecursiveMaintainerState | 331 | /// Creates MaintainerAnnouncement + RecursiveMaintainerState |
| 257 | /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message) | 332 | /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message) |
| 258 | /// 3. **Verify Not Served**: Confirms event is not served by relays | 333 | /// 3. **Verify Not Served**: Confirms event is not served by relays |
| @@ -276,16 +351,29 @@ impl FixtureKind { | |||
| 276 | pub fn dependencies(&self) -> Vec<FixtureKind> { | 351 | pub fn dependencies(&self) -> Vec<FixtureKind> { |
| 277 | match self { | 352 | match self { |
| 278 | // Base fixtures - no dependencies | 353 | // Base fixtures - no dependencies |
| 279 | Self::ValidRepo => vec![], | 354 | Self::ValidRepoSent => vec![], |
| 280 | 355 | ||
| 281 | // Fixtures that depend on ValidRepo | 356 | // ValidRepoServed depends on OwnerStateDataPushed (announcement promoted after git push) |
| 282 | Self::RepoWithIssue => vec![Self::ValidRepo], | 357 | Self::ValidRepoServed => vec![Self::OwnerStateDataPushed], |
| 283 | Self::RepoState => vec![Self::ValidRepo], | 358 | |
| 284 | Self::PREvent => vec![Self::ValidRepo], | 359 | // Fixtures that depend on ValidRepoServed (need queryable announcement) |
| 285 | Self::PREventGenerated => vec![Self::ValidRepo], | 360 | Self::RepoWithIssue => vec![Self::ValidRepoServed], |
| 361 | Self::RepoState => vec![Self::ValidRepoSent], | ||
| 362 | // OwnerRepoStateSent depends on ValidRepoSent: state event sent, sitting in purgatory | ||
| 363 | Self::OwnerRepoStateSent => vec![Self::ValidRepoSent], | ||
| 364 | Self::PREvent => vec![Self::ValidRepoServed], | ||
| 365 | Self::PREventGenerated => vec![Self::ValidRepoServed], | ||
| 286 | Self::PRWrongCommitPushedBeforeEvent => vec![Self::PREventGenerated], | 366 | Self::PRWrongCommitPushedBeforeEvent => vec![Self::PREventGenerated], |
| 287 | Self::PREventSentAfterWrongPush => vec![Self::PRWrongCommitPushedBeforeEvent], | 367 | Self::PREventSentAfterWrongPush => vec![Self::PRWrongCommitPushedBeforeEvent], |
| 288 | Self::OwnerStateDataPushed => vec![Self::ValidRepo], | 368 | |
| 369 | // Second PR event fixtures (for purgatory testing) | ||
| 370 | Self::PREvent2Generated => vec![Self::ValidRepoServed], | ||
| 371 | Self::PREvent2Sent => vec![Self::PREvent2Generated], | ||
| 372 | Self::PREvent2GitDataPushed => vec![Self::PREvent2Sent], | ||
| 373 | Self::PREvent2Served => vec![Self::PREvent2GitDataPushed], | ||
| 374 | |||
| 375 | // OwnerStateDataPushed depends on OwnerRepoStateSent (git push + purgatory release) | ||
| 376 | Self::OwnerStateDataPushed => vec![Self::OwnerRepoStateSent], | ||
| 289 | 377 | ||
| 290 | // Fixtures that depend on RepoWithIssue | 378 | // Fixtures that depend on RepoWithIssue |
| 291 | Self::RepoWithComment => vec![Self::RepoWithIssue], | 379 | Self::RepoWithComment => vec![Self::RepoWithIssue], |
| @@ -321,8 +409,17 @@ impl FixtureKind { | |||
| 321 | Self::PRWrongCommitPushedBeforeEvent => true, | 409 | Self::PRWrongCommitPushedBeforeEvent => true, |
| 322 | // PREventSentAfterWrongPush sends the PR event internally | 410 | // PREventSentAfterWrongPush sends the PR event internally |
| 323 | Self::PREventSentAfterWrongPush => true, | 411 | Self::PREventSentAfterWrongPush => true, |
| 412 | // Second PR event fixtures handle their own events/git data | ||
| 413 | Self::PREvent2Generated => true, | ||
| 414 | Self::PREvent2Sent => true, | ||
| 415 | Self::PREvent2GitDataPushed => true, | ||
| 416 | Self::PREvent2Served => true, | ||
| 324 | // HeadSetToDevelopBranch sends its state event internally | 417 | // HeadSetToDevelopBranch sends its state event internally |
| 325 | Self::HeadSetToDevelopBranch => true, | 418 | Self::HeadSetToDevelopBranch => true, |
| 419 | // ValidRepoServed doesn't send anything itself, just returns cached event | ||
| 420 | Self::ValidRepoServed => true, | ||
| 421 | // OwnerRepoStateSent sends its state event and notes purgatory internally | ||
| 422 | Self::OwnerRepoStateSent => true, | ||
| 326 | // All other fixtures return a single event for the caller to send | 423 | // All other fixtures return a single event for the caller to send |
| 327 | _ => false, | 424 | _ => false, |
| 328 | } | 425 | } |
| @@ -373,7 +470,7 @@ impl From<AuditMode> for ContextMode { | |||
| 373 | /// let ctx = TestContext::new(&client); | 470 | /// let ctx = TestContext::new(&client); |
| 374 | /// | 471 | /// |
| 375 | /// // Get a repository fixture - will be reused by subsequent TestContexts | 472 | /// // Get a repository fixture - will be reused by subsequent TestContexts |
| 376 | /// let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?; | 473 | /// let repo = ctx.get_fixture(FixtureKind::ValidRepoSent).await?; |
| 377 | /// | 474 | /// |
| 378 | /// // For cargo test (isolated fixtures) | 475 | /// // For cargo test (isolated fixtures) |
| 379 | /// let config = AuditConfig::isolated(); | 476 | /// let config = AuditConfig::isolated(); |
| @@ -381,7 +478,7 @@ impl From<AuditMode> for ContextMode { | |||
| 381 | /// let ctx = TestContext::new(&client); | 478 | /// let ctx = TestContext::new(&client); |
| 382 | /// | 479 | /// |
| 383 | /// // Get a repository fixture - fresh for this TestContext only | 480 | /// // Get a repository fixture - fresh for this TestContext only |
| 384 | /// let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?; | 481 | /// let repo = ctx.get_fixture(FixtureKind::ValidRepoSent).await?; |
| 385 | /// # Ok(()) | 482 | /// # Ok(()) |
| 386 | /// # } | 483 | /// # } |
| 387 | /// ``` | 484 | /// ``` |
| @@ -436,7 +533,7 @@ impl<'a> TestContext<'a> { | |||
| 436 | /// ```no_run | 533 | /// ```no_run |
| 437 | /// # use grasp_audit::*; | 534 | /// # use grasp_audit::*; |
| 438 | /// # async fn example(ctx: &TestContext<'_>) -> anyhow::Result<()> { | 535 | /// # async fn example(ctx: &TestContext<'_>) -> anyhow::Result<()> { |
| 439 | /// let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?; | 536 | /// let repo = ctx.get_fixture(FixtureKind::ValidRepoSent).await?; |
| 440 | /// # Ok(()) | 537 | /// # Ok(()) |
| 441 | /// # } | 538 | /// # } |
| 442 | /// ``` | 539 | /// ``` |
| @@ -517,8 +614,8 @@ impl<'a> TestContext<'a> { | |||
| 517 | /// ```no_run | 614 | /// ```no_run |
| 518 | /// # use grasp_audit::*; | 615 | /// # use grasp_audit::*; |
| 519 | /// # async fn example(ctx: &TestContext<'_>) -> anyhow::Result<()> { | 616 | /// # async fn example(ctx: &TestContext<'_>) -> anyhow::Result<()> { |
| 520 | /// // This ensures ValidRepo exists first, then creates MaintainerState | 617 | /// // This ensures ValidRepoSent exists first, then creates RepoState |
| 521 | /// let state = ctx.ensure_fixture(FixtureKind::MaintainerState).await?; | 618 | /// let state = ctx.ensure_fixture(FixtureKind::RepoState).await?; |
| 522 | /// # Ok(()) | 619 | /// # Ok(()) |
| 523 | /// # } | 620 | /// # } |
| 524 | /// ``` | 621 | /// ``` |
| @@ -625,10 +722,10 @@ impl<'a> TestContext<'a> { | |||
| 625 | /// already-cached dependencies. | 722 | /// already-cached dependencies. |
| 626 | async fn build_fixture_inner(&self, kind: FixtureKind) -> Result<Event> { | 723 | async fn build_fixture_inner(&self, kind: FixtureKind) -> Result<Event> { |
| 627 | match kind { | 724 | match kind { |
| 628 | FixtureKind::ValidRepo => { | 725 | FixtureKind::ValidRepoSent => { |
| 629 | // ValidRepo has no dependencies - create a new repo announcement | 726 | // ValidRepoSent has no dependencies - create a new repo announcement |
| 630 | let test_name = format!( | 727 | let test_name = format!( |
| 631 | "fixture-ValidRepo-{}", | 728 | "fixture-ValidRepoSent-{}", |
| 632 | &uuid::Uuid::new_v4().to_string()[..8] | 729 | &uuid::Uuid::new_v4().to_string()[..8] |
| 633 | ); | 730 | ); |
| 634 | 731 | ||
| @@ -638,9 +735,15 @@ impl<'a> TestContext<'a> { | |||
| 638 | .with_context(|| format!("create_repo_announcement failed for {}", test_name)) | 735 | .with_context(|| format!("create_repo_announcement failed for {}", test_name)) |
| 639 | } | 736 | } |
| 640 | 737 | ||
| 738 | FixtureKind::ValidRepoServed => { | ||
| 739 | // OwnerStateDataPushed is already ensured as a dependency. | ||
| 740 | // The announcement is now promoted (served). Return the cached ValidRepoSent event. | ||
| 741 | self.get_cached_dependency(FixtureKind::ValidRepoSent) | ||
| 742 | } | ||
| 743 | |||
| 641 | FixtureKind::RepoWithIssue => { | 744 | FixtureKind::RepoWithIssue => { |
| 642 | // ValidRepo is ensured by ensure_fixture before this is called | 745 | // ValidRepoServed is ensured by ensure_fixture before this is called |
| 643 | let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; | 746 | let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?; |
| 644 | 747 | ||
| 645 | // Build issue referencing it - caller will send it | 748 | // Build issue referencing it - caller will send it |
| 646 | self.client | 749 | self.client |
| @@ -658,8 +761,8 @@ impl<'a> TestContext<'a> { | |||
| 658 | FixtureKind::RepoState => { | 761 | FixtureKind::RepoState => { |
| 659 | use nostr_sdk::prelude::*; | 762 | use nostr_sdk::prelude::*; |
| 660 | 763 | ||
| 661 | // ValidRepo is ensured by ensure_fixture before this is called | 764 | // ValidRepoSent is ensured by ensure_fixture before this is called |
| 662 | let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; | 765 | let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; |
| 663 | 766 | ||
| 664 | // Extract repo_id from repo announcement | 767 | // Extract repo_id from repo announcement |
| 665 | let repo_id = repo | 768 | let repo_id = repo |
| @@ -692,18 +795,52 @@ impl<'a> TestContext<'a> { | |||
| 692 | .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e)) | 795 | .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e)) |
| 693 | } | 796 | } |
| 694 | 797 | ||
| 798 | FixtureKind::OwnerRepoStateSent => { | ||
| 799 | use nostr_sdk::prelude::*; | ||
| 800 | |||
| 801 | // ValidRepoSent is ensured by ensure_fixture before this is called | ||
| 802 | let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; | ||
| 803 | let repo_id = self.extract_repo_id(&repo)?; | ||
| 804 | |||
| 805 | let base_time = Timestamp::now().as_secs(); | ||
| 806 | let older_timestamp = Timestamp::from(base_time - 10); | ||
| 807 | |||
| 808 | let state_event = self | ||
| 809 | .client | ||
| 810 | .event_builder(Kind::RepoState, "") | ||
| 811 | .tag(Tag::identifier(&repo_id)) | ||
| 812 | .tag(Tag::custom( | ||
| 813 | TagKind::custom("refs/heads/main"), | ||
| 814 | vec![DETERMINISTIC_COMMIT_HASH.to_string()], | ||
| 815 | )) | ||
| 816 | .tag(Tag::custom( | ||
| 817 | TagKind::custom("HEAD"), | ||
| 818 | vec!["ref: refs/heads/main".to_string()], | ||
| 819 | )) | ||
| 820 | .custom_time(older_timestamp) | ||
| 821 | .build(self.client.keys()) | ||
| 822 | .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e))?; | ||
| 823 | |||
| 824 | // Send to relay - event will be accepted but held in purgatory (no git data yet) | ||
| 825 | self.client | ||
| 826 | .send_event_and_note_purgatory(state_event.clone()) | ||
| 827 | .await?; | ||
| 828 | |||
| 829 | Ok(state_event) | ||
| 830 | } | ||
| 831 | |||
| 695 | FixtureKind::PREvent => { | 832 | FixtureKind::PREvent => { |
| 696 | use nostr_sdk::prelude::*; | 833 | use nostr_sdk::prelude::*; |
| 697 | 834 | ||
| 698 | // ValidRepo is ensured by ensure_fixture before this is called | 835 | // ValidRepoServed is ensured by ensure_fixture before this is called |
| 699 | let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; | 836 | let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?; |
| 700 | 837 | ||
| 701 | let repo_id = repo | 838 | let repo_id = repo |
| 702 | .tags | 839 | .tags |
| 703 | .iter() | 840 | .iter() |
| 704 | .find(|t| t.kind() == TagKind::d()) | 841 | .find(|t| t.kind() == TagKind::d()) |
| 705 | .and_then(|t| t.content()) | 842 | .and_then(|t| t.content()) |
| 706 | .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepo fixture"))? | 843 | .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepoServed fixture"))? |
| 707 | .to_string(); | 844 | .to_string(); |
| 708 | 845 | ||
| 709 | // Create PR event 1 second in the past | 846 | // Create PR event 1 second in the past |
| @@ -738,15 +875,15 @@ impl<'a> TestContext<'a> { | |||
| 738 | // This fixture is for "Generated" stage only | 875 | // This fixture is for "Generated" stage only |
| 739 | use nostr_sdk::prelude::*; | 876 | use nostr_sdk::prelude::*; |
| 740 | 877 | ||
| 741 | // ValidRepo is ensured by ensure_fixture before this is called | 878 | // ValidRepoServed is ensured by ensure_fixture before this is called |
| 742 | let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; | 879 | let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?; |
| 743 | 880 | ||
| 744 | let repo_id = repo | 881 | let repo_id = repo |
| 745 | .tags | 882 | .tags |
| 746 | .iter() | 883 | .iter() |
| 747 | .find(|t| t.kind() == TagKind::d()) | 884 | .find(|t| t.kind() == TagKind::d()) |
| 748 | .and_then(|t| t.content()) | 885 | .and_then(|t| t.content()) |
| 749 | .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepo fixture"))? | 886 | .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepoServed fixture"))? |
| 750 | .to_string(); | 887 | .to_string(); |
| 751 | 888 | ||
| 752 | // Create PR event 1 second in the past | 889 | // Create PR event 1 second in the past |
| @@ -784,6 +921,11 @@ impl<'a> TestContext<'a> { | |||
| 784 | self.build_pr_event_sent_after_wrong_push().await | 921 | self.build_pr_event_sent_after_wrong_push().await |
| 785 | } | 922 | } |
| 786 | 923 | ||
| 924 | FixtureKind::PREvent2Generated => self.build_pr_event_2_generated().await, | ||
| 925 | FixtureKind::PREvent2Sent => self.build_pr_event_2_sent().await, | ||
| 926 | FixtureKind::PREvent2GitDataPushed => self.build_pr_event_2_git_data_pushed().await, | ||
| 927 | FixtureKind::PREvent2Served => self.build_pr_event_2_served().await, | ||
| 928 | |||
| 787 | FixtureKind::OwnerStateDataPushed => self.build_owner_state_data_pushed().await, | 929 | FixtureKind::OwnerStateDataPushed => self.build_owner_state_data_pushed().await, |
| 788 | 930 | ||
| 789 | FixtureKind::MaintainerStateDataPushed => { | 931 | FixtureKind::MaintainerStateDataPushed => { |
| @@ -858,55 +1000,26 @@ impl<'a> TestContext<'a> { | |||
| 858 | .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement")) | 1000 | .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement")) |
| 859 | } | 1001 | } |
| 860 | 1002 | ||
| 861 | /// Build OwnerStateDataPushed fixture: full 4-stage fixture for push authorization | 1003 | /// Build OwnerStateDataPushed fixture: git push + purgatory release for owner's state event |
| 862 | /// | 1004 | /// |
| 863 | /// This handles all stages of the fixture: | 1005 | /// `OwnerRepoStateSent` is ensured as a dependency before this is called — the state event |
| 864 | /// 1. **Generated**: Creates RepoState (repo announcement + state event) | 1006 | /// is already on the relay in purgatory. This fixture completes the cycle: |
| 865 | /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message) | 1007 | /// 1. **DataPushed**: Clones repo, creates deterministic commit, pushes to relay |
| 866 | /// 3. **Verify Not Served**: Confirms event is not served by relays | 1008 | /// 2. **Verified**: Confirms state event is released from purgatory and served |
| 867 | /// 4. **DataPushed**: Clones repo, creates deterministic commit, pushes to relay | ||
| 868 | /// 5. **Verified**: Confirms event is served by relay | ||
| 869 | /// | 1009 | /// |
| 870 | /// # Returns | 1010 | /// # Returns |
| 871 | /// The state event (kind 30618) after all stages complete successfully | 1011 | /// The state event (kind 30618) after git data is pushed and purgatory is released |
| 872 | async fn build_owner_state_data_pushed(&self) -> Result<Event> { | 1012 | async fn build_owner_state_data_pushed(&self) -> Result<Event> { |
| 873 | use nostr_sdk::prelude::*; | 1013 | use nostr_sdk::prelude::*; |
| 874 | 1014 | ||
| 875 | // ============================================================ | 1015 | // OwnerRepoStateSent is ensured by ensure_fixture before this is called. |
| 876 | // Stage 1: ValidRepo is ensured by ensure_fixture before this is called | 1016 | // The state event is already on the relay in purgatory - retrieve it from cache. |
| 877 | // ============================================================ | 1017 | let state_event = self.get_cached_dependency(FixtureKind::OwnerRepoStateSent)?; |
| 878 | let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; | 1018 | let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; |
| 879 | let repo_id = self.extract_repo_id(&repo)?; | 1019 | let repo_id = self.extract_repo_id(&repo)?; |
| 880 | 1020 | ||
| 881 | // Build state event | ||
| 882 | let base_time = Timestamp::now().as_secs(); | ||
| 883 | let older_timestamp = Timestamp::from(base_time - 10); // 10 seconds ago | ||
| 884 | |||
| 885 | let state_event = self | ||
| 886 | .client | ||
| 887 | .event_builder(Kind::RepoState, "") | ||
| 888 | .tag(Tag::identifier(&repo_id)) | ||
| 889 | .tag(Tag::custom( | ||
| 890 | TagKind::custom("refs/heads/main"), | ||
| 891 | vec![DETERMINISTIC_COMMIT_HASH.to_string()], | ||
| 892 | )) | ||
| 893 | .tag(Tag::custom( | ||
| 894 | TagKind::custom("HEAD"), | ||
| 895 | vec!["ref: refs/heads/main".to_string()], | ||
| 896 | )) | ||
| 897 | .custom_time(older_timestamp) | ||
| 898 | .build(self.client.keys()) | ||
| 899 | .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e))?; | ||
| 900 | |||
| 901 | // ============================================================ | ||
| 902 | // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served | ||
| 903 | // ============================================================ | ||
| 904 | self.client | ||
| 905 | .send_event_expect_purgatory_not_served(state_event.clone()) | ||
| 906 | .await?; | ||
| 907 | |||
| 908 | // ============================================================ | 1021 | // ============================================================ |
| 909 | // Stage 4: DataPushed - Clone repo, create commit, push | 1022 | // Stage 1: DataPushed - Clone repo, create commit, push |
| 910 | // ============================================================ | 1023 | // ============================================================ |
| 911 | 1024 | ||
| 912 | // Get relay domain from connected relay | 1025 | // Get relay domain from connected relay |
| @@ -1008,7 +1121,7 @@ impl<'a> TestContext<'a> { | |||
| 1008 | } | 1121 | } |
| 1009 | 1122 | ||
| 1010 | // ============================================================ | 1123 | // ============================================================ |
| 1011 | // Stage 5: Verify state event is on relay | 1124 | // Stage 2: Verify state event is released from purgatory |
| 1012 | // ============================================================ | 1125 | // ============================================================ |
| 1013 | 1126 | ||
| 1014 | tokio::time::sleep(Duration::from_millis(200)).await; | 1127 | tokio::time::sleep(Duration::from_millis(200)).await; |
| @@ -1048,8 +1161,8 @@ impl<'a> TestContext<'a> { | |||
| 1048 | // Extract repo_id from owner's state event (same d-tag structure) | 1161 | // Extract repo_id from owner's state event (same d-tag structure) |
| 1049 | let repo_id = self.extract_repo_id(&owner_state)?; | 1162 | let repo_id = self.extract_repo_id(&owner_state)?; |
| 1050 | 1163 | ||
| 1051 | // Get the repo (ValidRepo, also cached) for the owner's npub | 1164 | // Get the repo (ValidRepoSent, also cached) for the owner's npub |
| 1052 | let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; | 1165 | let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; |
| 1053 | 1166 | ||
| 1054 | // Build maintainer's state event (state event ONLY - no announcement) | 1167 | // Build maintainer's state event (state event ONLY - no announcement) |
| 1055 | let base_time = Timestamp::now().as_secs(); | 1168 | let base_time = Timestamp::now().as_secs(); |
| @@ -1074,9 +1187,11 @@ impl<'a> TestContext<'a> { | |||
| 1074 | // ============================================================ | 1187 | // ============================================================ |
| 1075 | // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served | 1188 | // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served |
| 1076 | // ============================================================ | 1189 | // ============================================================ |
| 1077 | self.client | 1190 | let (_, _in_purgatory) = self |
| 1078 | .send_event_expect_purgatory_not_served(maintainer_state_event.clone()) | 1191 | .client |
| 1192 | .send_event_and_note_purgatory(maintainer_state_event.clone()) | ||
| 1079 | .await?; | 1193 | .await?; |
| 1194 | // Note: We don't fail if purgatory wasn't observed - the fixture proceeds regardless | ||
| 1080 | 1195 | ||
| 1081 | // ============================================================ | 1196 | // ============================================================ |
| 1082 | // Stage 4: DataPushed - Clone repo, create maintainer commit, push | 1197 | // Stage 4: DataPushed - Clone repo, create maintainer commit, push |
| @@ -1194,7 +1309,7 @@ impl<'a> TestContext<'a> { | |||
| 1194 | /// recursive maintainer force-pushes their commit on top. | 1309 | /// recursive maintainer force-pushes their commit on top. |
| 1195 | /// | 1310 | /// |
| 1196 | /// This handles all stages of the fixture: | 1311 | /// This handles all stages of the fixture: |
| 1197 | /// 1. **Generated**: (MaintainerStateDataPushed dependency includes ValidRepo + OwnerStateDataPushed) | 1312 | /// 1. **Generated**: (MaintainerStateDataPushed dependency includes ValidRepoSent + OwnerStateDataPushed) |
| 1198 | /// Creates MaintainerAnnouncement + RecursiveMaintainerState | 1313 | /// Creates MaintainerAnnouncement + RecursiveMaintainerState |
| 1199 | /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message) | 1314 | /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message) |
| 1200 | /// 3. **Verify Not Served**: Confirms event is not served by relays | 1315 | /// 3. **Verify Not Served**: Confirms event is not served by relays |
| @@ -1215,8 +1330,8 @@ impl<'a> TestContext<'a> { | |||
| 1215 | // Extract repo_id from maintainer's state event (same d-tag structure) | 1330 | // Extract repo_id from maintainer's state event (same d-tag structure) |
| 1216 | let repo_id = self.extract_repo_id(&maintainer_state)?; | 1331 | let repo_id = self.extract_repo_id(&maintainer_state)?; |
| 1217 | 1332 | ||
| 1218 | // Get the repo (ValidRepo, also cached) for the owner's npub | 1333 | // Get the repo (ValidRepoSent, also cached) for the owner's npub |
| 1219 | let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; | 1334 | let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; |
| 1220 | 1335 | ||
| 1221 | // ============================================================ | 1336 | // ============================================================ |
| 1222 | // Stage 1 (continued): Generate MaintainerAnnouncement and RecursiveMaintainerState | 1337 | // Stage 1 (continued): Generate MaintainerAnnouncement and RecursiveMaintainerState |
| @@ -1249,9 +1364,11 @@ impl<'a> TestContext<'a> { | |||
| 1249 | // ============================================================ | 1364 | // ============================================================ |
| 1250 | // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served | 1365 | // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served |
| 1251 | // ============================================================ | 1366 | // ============================================================ |
| 1252 | self.client | 1367 | let (_, _in_purgatory) = self |
| 1253 | .send_event_expect_purgatory_not_served(recursive_maintainer_state_event.clone()) | 1368 | .client |
| 1369 | .send_event_and_note_purgatory(recursive_maintainer_state_event.clone()) | ||
| 1254 | .await?; | 1370 | .await?; |
| 1371 | // Note: We don't fail if purgatory wasn't observed - the fixture proceeds regardless | ||
| 1255 | 1372 | ||
| 1256 | // ============================================================ | 1373 | // ============================================================ |
| 1257 | // Stage 4: DataPushed - Clone repo, create recursive maintainer commit, push | 1374 | // Stage 4: DataPushed - Clone repo, create recursive maintainer commit, push |
| @@ -1428,7 +1545,7 @@ impl<'a> TestContext<'a> { | |||
| 1428 | /// 3. A wrong commit is pushed to refs/nostr/<pr-event-id> | 1545 | /// 3. A wrong commit is pushed to refs/nostr/<pr-event-id> |
| 1429 | /// | 1546 | /// |
| 1430 | /// Server state after: | 1547 | /// Server state after: |
| 1431 | /// - ValidRepo announcement on relay | 1548 | /// - ValidRepoSent announcement on relay |
| 1432 | /// - refs/nostr/<pr-event-id> on git server pointing to DETERMINISTIC_COMMIT_HASH (wrong) | 1549 | /// - refs/nostr/<pr-event-id> on git server pointing to DETERMINISTIC_COMMIT_HASH (wrong) |
| 1433 | /// - NO PR event on relay | 1550 | /// - NO PR event on relay |
| 1434 | /// | 1551 | /// |
| @@ -1440,8 +1557,8 @@ impl<'a> TestContext<'a> { | |||
| 1440 | let pr_event = self.get_cached_dependency(FixtureKind::PREventGenerated)?; | 1557 | let pr_event = self.get_cached_dependency(FixtureKind::PREventGenerated)?; |
| 1441 | let pr_event_id = pr_event.id.to_hex(); | 1558 | let pr_event_id = pr_event.id.to_hex(); |
| 1442 | 1559 | ||
| 1443 | // Get the ValidRepo to extract repo info | 1560 | // Get the ValidRepoServed to extract repo info |
| 1444 | let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; | 1561 | let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?; |
| 1445 | let repo_id = self.extract_repo_id(&repo)?; | 1562 | let repo_id = self.extract_repo_id(&repo)?; |
| 1446 | 1563 | ||
| 1447 | // Get relay domain for cloning | 1564 | // Get relay domain for cloning |
| @@ -1462,10 +1579,14 @@ impl<'a> TestContext<'a> { | |||
| 1462 | let _ = fs::remove_dir_all(path); | 1579 | let _ = fs::remove_dir_all(path); |
| 1463 | }; | 1580 | }; |
| 1464 | 1581 | ||
| 1465 | // Create a WRONG commit (Owner variant, not PRTestCommit) | 1582 | // Create a WRONG commit using a unique file (not PRTestCommit) |
| 1466 | // This commit hash will NOT match what's in the PR event's `c` tag | 1583 | // We use create_commit (non-deterministic) so it always succeeds even if the |
| 1584 | // repo already has a commit (e.g. from OwnerStateDataPushed) with the same | ||
| 1585 | // deterministic content. The only requirement is that the hash differs from | ||
| 1586 | // PR_TEST_COMMIT_HASH, which is guaranteed since PR_TEST_COMMIT_HASH is a | ||
| 1587 | // deterministic root-commit with specific content and dates. | ||
| 1467 | let wrong_commit_hash = | 1588 | let wrong_commit_hash = |
| 1468 | match create_deterministic_commit_with_variant(&clone_path, CommitVariant::Owner) { | 1589 | match create_commit(&clone_path, "wrong commit - not the PR test commit") { |
| 1469 | Ok(h) => h, | 1590 | Ok(h) => h, |
| 1470 | Err(e) => { | 1591 | Err(e) => { |
| 1471 | cleanup(&clone_path); | 1592 | cleanup(&clone_path); |
| @@ -1520,7 +1641,7 @@ impl<'a> TestContext<'a> { | |||
| 1520 | /// | 1641 | /// |
| 1521 | /// This fixture builds on PRWrongCommitPushedBeforeEvent by sending the PR event. | 1642 | /// This fixture builds on PRWrongCommitPushedBeforeEvent by sending the PR event. |
| 1522 | /// After this fixture, the relay has: | 1643 | /// After this fixture, the relay has: |
| 1523 | /// - ValidRepo announcement | 1644 | /// - ValidRepoServed announcement |
| 1524 | /// - PR event | 1645 | /// - PR event |
| 1525 | /// - refs/nostr/<pr-event-id> may have been cleaned up (that's what tests verify) | 1646 | /// - refs/nostr/<pr-event-id> may have been cleaned up (that's what tests verify) |
| 1526 | /// | 1647 | /// |
| @@ -1539,6 +1660,173 @@ impl<'a> TestContext<'a> { | |||
| 1539 | Ok(pr_event) | 1660 | Ok(pr_event) |
| 1540 | } | 1661 | } |
| 1541 | 1662 | ||
| 1663 | /// Build PREvent2Generated fixture | ||
| 1664 | /// | ||
| 1665 | /// Creates a PR event with `c` tag pointing to PR_TEST_COMMIT_HASH_2. | ||
| 1666 | /// The event is NOT sent to the relay. | ||
| 1667 | async fn build_pr_event_2_generated(&self) -> Result<Event> { | ||
| 1668 | use nostr_sdk::prelude::*; | ||
| 1669 | |||
| 1670 | let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?; | ||
| 1671 | let repo_id = self.extract_repo_id(&repo)?; | ||
| 1672 | |||
| 1673 | let base_time = Timestamp::now().as_secs(); | ||
| 1674 | let pr_timestamp = Timestamp::from(base_time - 1); | ||
| 1675 | |||
| 1676 | self.client | ||
| 1677 | .event_builder(Kind::GitPullRequest, "Test PR 2 for GRASP validation") | ||
| 1678 | .tag(Tag::custom( | ||
| 1679 | TagKind::custom("a"), | ||
| 1680 | vec![format!( | ||
| 1681 | "30617:{}:{}", | ||
| 1682 | self.client.public_key().to_hex(), | ||
| 1683 | repo_id | ||
| 1684 | )], | ||
| 1685 | )) | ||
| 1686 | .tag(Tag::custom( | ||
| 1687 | TagKind::custom("c"), | ||
| 1688 | vec![PR_TEST_COMMIT_HASH_2.to_string()], | ||
| 1689 | )) | ||
| 1690 | .custom_time(pr_timestamp) | ||
| 1691 | .build(self.client.pr_author_keys()) | ||
| 1692 | .map_err(|e| anyhow::anyhow!("Failed to build PR event 2: {}", e)) | ||
| 1693 | } | ||
| 1694 | |||
| 1695 | /// Build PREvent2Sent fixture | ||
| 1696 | /// | ||
| 1697 | /// Sends the PR event to relay. Event should enter purgatory. | ||
| 1698 | async fn build_pr_event_2_sent(&self) -> Result<Event> { | ||
| 1699 | let pr_event = self.get_cached_dependency(FixtureKind::PREvent2Generated)?; | ||
| 1700 | |||
| 1701 | let (_, in_purgatory) = self | ||
| 1702 | .client | ||
| 1703 | .send_event_and_note_purgatory(pr_event.clone()) | ||
| 1704 | .await?; | ||
| 1705 | |||
| 1706 | if !in_purgatory { | ||
| 1707 | return Err(anyhow::anyhow!( | ||
| 1708 | "PR event 2 was served immediately - purgatory not implemented" | ||
| 1709 | )); | ||
| 1710 | } | ||
| 1711 | |||
| 1712 | Ok(pr_event) | ||
| 1713 | } | ||
| 1714 | |||
| 1715 | /// Build PREvent2GitDataPushed fixture | ||
| 1716 | /// | ||
| 1717 | /// Pushes correct commit to refs/nostr/<pr-event-id> after event was sent. | ||
| 1718 | async fn build_pr_event_2_git_data_pushed(&self) -> Result<Event> { | ||
| 1719 | use nostr_sdk::prelude::*; | ||
| 1720 | |||
| 1721 | let pr_event = self.get_cached_dependency(FixtureKind::PREvent2Sent)?; | ||
| 1722 | let pr_event_id = pr_event.id.to_hex(); | ||
| 1723 | |||
| 1724 | let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?; | ||
| 1725 | let repo_id = self.extract_repo_id(&repo)?; | ||
| 1726 | |||
| 1727 | let relay_domain = self.get_relay_domain().await?; | ||
| 1728 | |||
| 1729 | let npub = repo | ||
| 1730 | .pubkey | ||
| 1731 | .to_bech32() | ||
| 1732 | .map_err(|e| anyhow::anyhow!("Failed to convert pubkey: {}", e))?; | ||
| 1733 | |||
| 1734 | let clone_path = clone_repo(&relay_domain, &npub, &repo_id) | ||
| 1735 | .map_err(|e| anyhow::anyhow!("Failed to clone repo: {}", e))?; | ||
| 1736 | |||
| 1737 | let cleanup = |path: &PathBuf| { | ||
| 1738 | let _ = fs::remove_dir_all(path); | ||
| 1739 | }; | ||
| 1740 | |||
| 1741 | // Reset to orphan state and create deterministic root commit | ||
| 1742 | // Step 1: Create orphan branch (removes all history) | ||
| 1743 | let _ = Command::new("git") | ||
| 1744 | .args(["checkout", "--orphan", "pr-branch"]) | ||
| 1745 | .current_dir(&clone_path) | ||
| 1746 | .output(); | ||
| 1747 | |||
| 1748 | // Step 2: Clear staged files (orphan keeps files staged from previous branch) | ||
| 1749 | let _ = Command::new("git") | ||
| 1750 | .args(["rm", "-rf", "--cached", "."]) | ||
| 1751 | .current_dir(&clone_path) | ||
| 1752 | .output(); | ||
| 1753 | |||
| 1754 | // Step 3: Remove all working directory files for clean state (except .git) | ||
| 1755 | for entry in | ||
| 1756 | fs::read_dir(&clone_path).map_err(|e| anyhow::anyhow!("Failed to read dir: {}", e))? | ||
| 1757 | { | ||
| 1758 | if let Ok(entry) = entry { | ||
| 1759 | let path = entry.path(); | ||
| 1760 | if path.file_name() != Some(std::ffi::OsStr::new(".git")) { | ||
| 1761 | let _ = fs::remove_file(&path).or_else(|_| fs::remove_dir_all(&path)); | ||
| 1762 | } | ||
| 1763 | } | ||
| 1764 | } | ||
| 1765 | |||
| 1766 | let commit_hash = match create_deterministic_commit_with_variant( | ||
| 1767 | &clone_path, | ||
| 1768 | CommitVariant::PRTestCommit2, | ||
| 1769 | ) { | ||
| 1770 | Ok(h) => h, | ||
| 1771 | Err(e) => { | ||
| 1772 | cleanup(&clone_path); | ||
| 1773 | return Err(anyhow::anyhow!("Failed to create PR test commit 2: {}", e)); | ||
| 1774 | } | ||
| 1775 | }; | ||
| 1776 | |||
| 1777 | if commit_hash != PR_TEST_COMMIT_HASH_2 { | ||
| 1778 | cleanup(&clone_path); | ||
| 1779 | return Err(anyhow::anyhow!( | ||
| 1780 | "PR test commit 2 hash mismatch: got {}, expected {}", | ||
| 1781 | commit_hash, | ||
| 1782 | PR_TEST_COMMIT_HASH_2 | ||
| 1783 | )); | ||
| 1784 | } | ||
| 1785 | |||
| 1786 | let push_output = Command::new("git") | ||
| 1787 | .args([ | ||
| 1788 | "push", | ||
| 1789 | "origin", | ||
| 1790 | &format!("pr-branch:refs/nostr/{}", pr_event_id), | ||
| 1791 | ]) | ||
| 1792 | .current_dir(&clone_path) | ||
| 1793 | .output() | ||
| 1794 | .map_err(|e| { | ||
| 1795 | cleanup(&clone_path); | ||
| 1796 | anyhow::anyhow!("Failed to execute git push: {}", e) | ||
| 1797 | })?; | ||
| 1798 | |||
| 1799 | cleanup(&clone_path); | ||
| 1800 | |||
| 1801 | if !push_output.status.success() { | ||
| 1802 | let stderr = String::from_utf8_lossy(&push_output.stderr); | ||
| 1803 | return Err(anyhow::anyhow!( | ||
| 1804 | "Push to refs/nostr/{} failed: {}", | ||
| 1805 | pr_event_id, | ||
| 1806 | stderr | ||
| 1807 | )); | ||
| 1808 | } | ||
| 1809 | |||
| 1810 | tokio::time::sleep(std::time::Duration::from_millis(500)).await; | ||
| 1811 | |||
| 1812 | Ok(pr_event) | ||
| 1813 | } | ||
| 1814 | |||
| 1815 | /// Build PREvent2Served fixture | ||
| 1816 | /// | ||
| 1817 | /// Full fixture: event sent, git pushed, event now served. | ||
| 1818 | async fn build_pr_event_2_served(&self) -> Result<Event> { | ||
| 1819 | let pr_event = self.get_cached_dependency(FixtureKind::PREvent2GitDataPushed)?; | ||
| 1820 | |||
| 1821 | if !self.client.is_event_on_relay(pr_event.id).await? { | ||
| 1822 | return Err(anyhow::anyhow!( | ||
| 1823 | "PR event 2 not released from purgatory after git push" | ||
| 1824 | )); | ||
| 1825 | } | ||
| 1826 | |||
| 1827 | Ok(pr_event) | ||
| 1828 | } | ||
| 1829 | |||
| 1542 | /// Get relay domain (host:port) from the connected relay | 1830 | /// Get relay domain (host:port) from the connected relay |
| 1543 | /// | 1831 | /// |
| 1544 | /// Extracts the domain from the relay URL for git HTTP operations. | 1832 | /// Extracts the domain from the relay URL for git HTTP operations. |
| @@ -1845,16 +2133,19 @@ pub enum CommitVariant { | |||
| 1845 | RecursiveMaintainer, | 2133 | RecursiveMaintainer, |
| 1846 | /// PR test commit variant - for PR event tests | 2134 | /// PR test commit variant - for PR event tests |
| 1847 | PRTestCommit, | 2135 | PRTestCommit, |
| 2136 | /// Second PR test commit variant - for second PR event tests | ||
| 2137 | PRTestCommit2, | ||
| 1848 | } | 2138 | } |
| 1849 | 2139 | ||
| 1850 | impl CommitVariant { | 2140 | impl CommitVariant { |
| 1851 | /// Get the file content for this variant | 2141 | /// Get the file content for this variant |
| 1852 | pub fn file_content(&self) -> &'static str { | 2142 | pub fn file_content(&self) -> &'static str { |
| 1853 | match self { | 2143 | match self { |
| 1854 | CommitVariant::Owner => "Initial commit", | 2144 | CommitVariant::Owner => "Initial commit\n", |
| 1855 | CommitVariant::Maintainer => "Maintainer initial commit", | 2145 | CommitVariant::Maintainer => "Maintainer initial commit\n", |
| 1856 | CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit", | 2146 | CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit\n", |
| 1857 | CommitVariant::PRTestCommit => "PR test deterministic commit", | 2147 | CommitVariant::PRTestCommit => "PR test deterministic commit\n", |
| 2148 | CommitVariant::PRTestCommit2 => "PR test deterministic commit 2\n", | ||
| 1858 | } | 2149 | } |
| 1859 | } | 2150 | } |
| 1860 | 2151 | ||
| @@ -1865,6 +2156,7 @@ impl CommitVariant { | |||
| 1865 | CommitVariant::Maintainer => "Maintainer initial commit", | 2156 | CommitVariant::Maintainer => "Maintainer initial commit", |
| 1866 | CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit", | 2157 | CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit", |
| 1867 | CommitVariant::PRTestCommit => "PR test deterministic commit", | 2158 | CommitVariant::PRTestCommit => "PR test deterministic commit", |
| 2159 | CommitVariant::PRTestCommit2 => "PR test deterministic commit 2", | ||
| 1868 | } | 2160 | } |
| 1869 | } | 2161 | } |
| 1870 | } | 2162 | } |
| @@ -2040,10 +2332,10 @@ mod tests { | |||
| 2040 | use std::collections::HashSet; | 2332 | use std::collections::HashSet; |
| 2041 | 2333 | ||
| 2042 | let mut set = HashSet::new(); | 2334 | let mut set = HashSet::new(); |
| 2043 | set.insert(FixtureKind::ValidRepo); | 2335 | set.insert(FixtureKind::ValidRepoSent); |
| 2044 | set.insert(FixtureKind::RepoWithIssue); | 2336 | set.insert(FixtureKind::RepoWithIssue); |
| 2045 | 2337 | ||
| 2046 | assert!(set.contains(&FixtureKind::ValidRepo)); | 2338 | assert!(set.contains(&FixtureKind::ValidRepoSent)); |
| 2047 | assert!(!set.contains(&FixtureKind::RepoWithComment)); | 2339 | assert!(!set.contains(&FixtureKind::RepoWithComment)); |
| 2048 | } | 2340 | } |
| 2049 | 2341 | ||
diff --git a/grasp-audit/src/result.rs b/grasp-audit/src/result.rs index ae3ef26..0c3ec08 100644 --- a/grasp-audit/src/result.rs +++ b/grasp-audit/src/result.rs | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | //! Test result types | 1 | //! Test result types |
| 2 | 2 | ||
| 3 | use crate::specs::grasp01::{get_sections, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID}; | 3 | use crate::specs::grasp01::{get_sections, SpecRef, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID}; |
| 4 | use std::collections::BTreeMap; | 4 | use std::collections::BTreeMap; |
| 5 | use std::time::{Duration, Instant}; | 5 | use std::time::{Duration, Instant}; |
| 6 | 6 | ||
| @@ -68,10 +68,16 @@ pub struct TestResult { | |||
| 68 | 68 | ||
| 69 | impl TestResult { | 69 | impl TestResult { |
| 70 | /// Create a new test result | 70 | /// Create a new test result |
| 71 | pub fn new(name: &str, spec_ref: &str, requirement: &str) -> Self { | 71 | /// |
| 72 | /// # Arguments | ||
| 73 | /// * `name` - Test name identifier | ||
| 74 | /// * `spec_ref` - Reference to the spec requirement being tested | ||
| 75 | /// * `requirement` - Human-readable description of what this test validates | ||
| 76 | /// (can be more specific than the general spec text) | ||
| 77 | pub fn new(name: &str, spec_ref: SpecRef, requirement: &str) -> Self { | ||
| 72 | TestResult { | 78 | TestResult { |
| 73 | name: name.to_string(), | 79 | name: name.to_string(), |
| 74 | spec_ref: spec_ref.to_string(), | 80 | spec_ref: spec_ref.spec_ref_string().to_string(), |
| 75 | requirement: requirement.to_string(), | 81 | requirement: requirement.to_string(), |
| 76 | passed: false, | 82 | passed: false, |
| 77 | error: None, | 83 | error: None, |
| @@ -293,9 +299,13 @@ mod tests { | |||
| 293 | 299 | ||
| 294 | #[tokio::test] | 300 | #[tokio::test] |
| 295 | async fn test_result_pass() { | 301 | async fn test_result_pass() { |
| 296 | let result = TestResult::new("test", "SPEC:1", "Must work") | 302 | let result = TestResult::new( |
| 297 | .run(|| async { Ok(()) }) | 303 | "test", |
| 298 | .await; | 304 | SpecRef::NostrRelayNip01Compliant, |
| 305 | "Test requirement", | ||
| 306 | ) | ||
| 307 | .run(|| async { Ok(()) }) | ||
| 308 | .await; | ||
| 299 | 309 | ||
| 300 | assert!(result.passed); | 310 | assert!(result.passed); |
| 301 | assert!(result.error.is_none()); | 311 | assert!(result.error.is_none()); |
| @@ -303,9 +313,13 @@ mod tests { | |||
| 303 | 313 | ||
| 304 | #[tokio::test] | 314 | #[tokio::test] |
| 305 | async fn test_result_fail() { | 315 | async fn test_result_fail() { |
| 306 | let result = TestResult::new("test", "SPEC:1", "Must work") | 316 | let result = TestResult::new( |
| 307 | .run(|| async { Err("Failed".to_string()) }) | 317 | "test", |
| 308 | .await; | 318 | SpecRef::NostrRelayNip01Compliant, |
| 319 | "Test requirement", | ||
| 320 | ) | ||
| 321 | .run(|| async { Err("Failed".to_string()) }) | ||
| 322 | .await; | ||
| 309 | 323 | ||
| 310 | assert!(!result.passed); | 324 | assert!(!result.passed); |
| 311 | assert_eq!(result.error, Some("Failed".to_string())); | 325 | assert_eq!(result.error, Some("Failed".to_string())); |
| @@ -315,8 +329,15 @@ mod tests { | |||
| 315 | fn test_audit_result() { | 329 | fn test_audit_result() { |
| 316 | let mut audit = AuditResult::new("Test Spec"); | 330 | let mut audit = AuditResult::new("Test Spec"); |
| 317 | 331 | ||
| 318 | audit.add(TestResult::new("test1", "SPEC:1", "Req1").pass()); | 332 | audit.add(TestResult::new("test1", SpecRef::NostrRelayNip01Compliant, "Test 1").pass()); |
| 319 | audit.add(TestResult::new("test2", "SPEC:2", "Req2").fail("Error")); | 333 | audit.add( |
| 334 | TestResult::new( | ||
| 335 | "test2", | ||
| 336 | SpecRef::NostrRelayRejectMissingCloneRelays, | ||
| 337 | "Test 2", | ||
| 338 | ) | ||
| 339 | .fail("Error"), | ||
| 340 | ); | ||
| 320 | 341 | ||
| 321 | assert_eq!(audit.total_count(), 2); | 342 | assert_eq!(audit.total_count(), 2); |
| 322 | assert_eq!(audit.passed_count(), 1); | 343 | assert_eq!(audit.passed_count(), 1); |
diff --git a/grasp-audit/src/specs/grasp01/cors.rs b/grasp-audit/src/specs/grasp01/cors.rs index f8b5f3b..e5d9a27 100644 --- a/grasp-audit/src/specs/grasp01/cors.rs +++ b/grasp-audit/src/specs/grasp01/cors.rs | |||
| @@ -14,6 +14,7 @@ | |||
| 14 | //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test | 14 | //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test |
| 15 | //! ``` | 15 | //! ``` |
| 16 | 16 | ||
| 17 | use crate::specs::grasp01::SpecRef; | ||
| 17 | use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; | 18 | use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; |
| 18 | use nostr_sdk::prelude::*; | 19 | use nostr_sdk::prelude::*; |
| 19 | 20 | ||
| @@ -44,7 +45,7 @@ impl CorsTests { | |||
| 44 | pub async fn test_cors_allow_origin(_client: &AuditClient, relay_domain: &str) -> TestResult { | 45 | pub async fn test_cors_allow_origin(_client: &AuditClient, relay_domain: &str) -> TestResult { |
| 45 | TestResult::new( | 46 | TestResult::new( |
| 46 | "cors_allow_origin", | 47 | "cors_allow_origin", |
| 47 | "GRASP-01:git-http:cors:50", | 48 | SpecRef::CorsAllowOrigin, |
| 48 | "Access-Control-Allow-Origin: * on all responses", | 49 | "Access-Control-Allow-Origin: * on all responses", |
| 49 | ) | 50 | ) |
| 50 | .run(|| { | 51 | .run(|| { |
| @@ -90,7 +91,7 @@ impl CorsTests { | |||
| 90 | pub async fn test_cors_allow_methods(_client: &AuditClient, relay_domain: &str) -> TestResult { | 91 | pub async fn test_cors_allow_methods(_client: &AuditClient, relay_domain: &str) -> TestResult { |
| 91 | TestResult::new( | 92 | TestResult::new( |
| 92 | "cors_allow_methods", | 93 | "cors_allow_methods", |
| 93 | "GRASP-01:git-http:cors:51", | 94 | SpecRef::CorsAllowMethods, |
| 94 | "Access-Control-Allow-Methods: GET, POST on all responses", | 95 | "Access-Control-Allow-Methods: GET, POST on all responses", |
| 95 | ) | 96 | ) |
| 96 | .run(|| { | 97 | .run(|| { |
| @@ -134,7 +135,7 @@ impl CorsTests { | |||
| 134 | pub async fn test_cors_allow_headers(_client: &AuditClient, relay_domain: &str) -> TestResult { | 135 | pub async fn test_cors_allow_headers(_client: &AuditClient, relay_domain: &str) -> TestResult { |
| 135 | TestResult::new( | 136 | TestResult::new( |
| 136 | "cors_allow_headers", | 137 | "cors_allow_headers", |
| 137 | "GRASP-01:git-http:cors:52", | 138 | SpecRef::CorsAllowHeaders, |
| 138 | "Access-Control-Allow-Headers: Content-Type on all responses", | 139 | "Access-Control-Allow-Headers: Content-Type on all responses", |
| 139 | ) | 140 | ) |
| 140 | .run(|| { | 141 | .run(|| { |
| @@ -181,8 +182,8 @@ impl CorsTests { | |||
| 181 | ) -> TestResult { | 182 | ) -> TestResult { |
| 182 | TestResult::new( | 183 | TestResult::new( |
| 183 | "cors_options_preflight", | 184 | "cors_options_preflight", |
| 184 | "GRASP-01:git-http:cors:53", | 185 | SpecRef::CorsOptionsResponse, |
| 185 | "OPTIONS requests return 204 No Content", | 186 | "OPTIONS requests return 204 No Content with CORS headers", |
| 186 | ) | 187 | ) |
| 187 | .run(|| { | 188 | .run(|| { |
| 188 | let relay_domain = relay_domain.to_string(); | 189 | let relay_domain = relay_domain.to_string(); |
| @@ -245,13 +246,13 @@ impl CorsTests { | |||
| 245 | let ctx = TestContext::new(client); | 246 | let ctx = TestContext::new(client); |
| 246 | 247 | ||
| 247 | // Create repository announcement to get a real repo path | 248 | // Create repository announcement to get a real repo path |
| 248 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 249 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { |
| 249 | Ok(r) => r, | 250 | Ok(r) => r, |
| 250 | Err(e) => { | 251 | Err(e) => { |
| 251 | return TestResult::new( | 252 | return TestResult::new( |
| 252 | test_name, | 253 | test_name, |
| 253 | "GRASP-01", | 254 | SpecRef::CorsAllowOrigin, |
| 254 | "CORS headers on real repository endpoint", | 255 | "CORS headers on real repository endpoints", |
| 255 | ) | 256 | ) |
| 256 | .fail(format!("Failed to create repo fixture: {}", e)) | 257 | .fail(format!("Failed to create repo fixture: {}", e)) |
| 257 | } | 258 | } |
| @@ -271,8 +272,8 @@ impl CorsTests { | |||
| 271 | None => { | 272 | None => { |
| 272 | return TestResult::new( | 273 | return TestResult::new( |
| 273 | test_name, | 274 | test_name, |
| 274 | "GRASP-01", | 275 | SpecRef::CorsAllowOrigin, |
| 275 | "CORS headers on real repository endpoint", | 276 | "CORS headers on real repository endpoints", |
| 276 | ) | 277 | ) |
| 277 | .fail("Repository announcement missing d tag") | 278 | .fail("Repository announcement missing d tag") |
| 278 | } | 279 | } |
| @@ -283,8 +284,8 @@ impl CorsTests { | |||
| 283 | Err(e) => { | 284 | Err(e) => { |
| 284 | return TestResult::new( | 285 | return TestResult::new( |
| 285 | test_name, | 286 | test_name, |
| 286 | "GRASP-01", | 287 | SpecRef::CorsAllowOrigin, |
| 287 | "CORS headers on real repository endpoint", | 288 | "CORS headers on real repository endpoints", |
| 288 | ) | 289 | ) |
| 289 | .fail(format!("Failed to convert pubkey to npub: {}", e)) | 290 | .fail(format!("Failed to convert pubkey to npub: {}", e)) |
| 290 | } | 291 | } |
| @@ -302,8 +303,8 @@ impl CorsTests { | |||
| 302 | Err(e) => { | 303 | Err(e) => { |
| 303 | return TestResult::new( | 304 | return TestResult::new( |
| 304 | test_name, | 305 | test_name, |
| 305 | "GRASP-01", | 306 | SpecRef::CorsAllowOrigin, |
| 306 | "CORS headers on real repository endpoint", | 307 | "CORS headers on real repository endpoints", |
| 307 | ) | 308 | ) |
| 308 | .fail(format!("Failed to GET info/refs: {}", e)) | 309 | .fail(format!("Failed to GET info/refs: {}", e)) |
| 309 | } | 310 | } |
| @@ -313,8 +314,8 @@ impl CorsTests { | |||
| 313 | if let Err(e) = check_cors_allow_origin(&response, "info/refs") { | 314 | if let Err(e) = check_cors_allow_origin(&response, "info/refs") { |
| 314 | return TestResult::new( | 315 | return TestResult::new( |
| 315 | test_name, | 316 | test_name, |
| 316 | "GRASP-01", | 317 | SpecRef::CorsAllowOrigin, |
| 317 | "CORS headers on real repository endpoint", | 318 | "CORS headers on real repository endpoints", |
| 318 | ) | 319 | ) |
| 319 | .fail(&e); | 320 | .fail(&e); |
| 320 | } | 321 | } |
| @@ -322,8 +323,8 @@ impl CorsTests { | |||
| 322 | if let Err(e) = check_cors_allow_methods(&response, "info/refs") { | 323 | if let Err(e) = check_cors_allow_methods(&response, "info/refs") { |
| 323 | return TestResult::new( | 324 | return TestResult::new( |
| 324 | test_name, | 325 | test_name, |
| 325 | "GRASP-01", | 326 | SpecRef::CorsAllowMethods, |
| 326 | "CORS headers on real repository endpoint", | 327 | "CORS headers on real repository endpoints", |
| 327 | ) | 328 | ) |
| 328 | .fail(&e); | 329 | .fail(&e); |
| 329 | } | 330 | } |
| @@ -331,16 +332,16 @@ impl CorsTests { | |||
| 331 | if let Err(e) = check_cors_allow_headers(&response, "info/refs") { | 332 | if let Err(e) = check_cors_allow_headers(&response, "info/refs") { |
| 332 | return TestResult::new( | 333 | return TestResult::new( |
| 333 | test_name, | 334 | test_name, |
| 334 | "GRASP-01", | 335 | SpecRef::CorsAllowHeaders, |
| 335 | "CORS headers on real repository endpoint", | 336 | "CORS headers on real repository endpoints", |
| 336 | ) | 337 | ) |
| 337 | .fail(&e); | 338 | .fail(&e); |
| 338 | } | 339 | } |
| 339 | 340 | ||
| 340 | TestResult::new( | 341 | TestResult::new( |
| 341 | test_name, | 342 | test_name, |
| 342 | "GRASP-01", | 343 | SpecRef::CorsAllowOrigin, |
| 343 | "CORS headers on real repository endpoint", | 344 | "CORS headers on real repository endpoints", |
| 344 | ) | 345 | ) |
| 345 | .pass() | 346 | .pass() |
| 346 | } | 347 | } |
diff --git a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs index 5b697d8..3375c4d 100644 --- a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs +++ b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs | |||
| @@ -92,6 +92,7 @@ | |||
| 92 | //! - Transitive tests verify multi-hop acceptance chains | 92 | //! - Transitive tests verify multi-hop acceptance chains |
| 93 | 93 | ||
| 94 | use crate::fixtures::{send_and_verify_accepted, send_and_verify_rejected}; | 94 | use crate::fixtures::{send_and_verify_accepted, send_and_verify_rejected}; |
| 95 | use crate::specs::grasp01::SpecRef; | ||
| 95 | use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; | 96 | use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; |
| 96 | use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp, ToBech32}; | 97 | use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp, ToBech32}; |
| 97 | use std::time::Duration; | 98 | use std::time::Duration; |
| @@ -148,20 +149,23 @@ impl EventAcceptancePolicyTests { | |||
| 148 | pub async fn test_accept_valid_repo_announcement(client: &AuditClient) -> TestResult { | 149 | pub async fn test_accept_valid_repo_announcement(client: &AuditClient) -> TestResult { |
| 149 | TestResult::new( | 150 | TestResult::new( |
| 150 | "accept_valid_repo_announcement", | 151 | "accept_valid_repo_announcement", |
| 151 | "GRASP-01:nostr-relay:7", | 152 | SpecRef::NostrRelayNip01Compliant, |
| 152 | "Accept valid repository announcements with service in clone and relays tags", | 153 | "MUST accept repo announcements listing service in clone & relays tags", |
| 153 | ) | 154 | ) |
| 154 | .run(|| async { | 155 | .run(|| async { |
| 155 | // Create TestContext for mode-aware fixture management | 156 | // Create TestContext for mode-aware fixture management |
| 156 | let ctx = TestContext::new(client); | 157 | let ctx = TestContext::new(client); |
| 157 | 158 | ||
| 158 | // Request repository fixture - behavior depends on mode | 159 | // Request repository fixture - behavior depends on mode |
| 159 | let event = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { | 160 | let event = ctx |
| 160 | format!( | 161 | .get_fixture(FixtureKind::ValidRepoServed) |
| 161 | "Test setup failed: could not get valid repository fixture: {}", | 162 | .await |
| 162 | e | 163 | .map_err(|e| { |
| 163 | ) | 164 | format!( |
| 164 | })?; | 165 | "Test setup failed: could not get valid repository fixture: {}", |
| 166 | e | ||
| 167 | ) | ||
| 168 | })?; | ||
| 165 | 169 | ||
| 166 | // Get relay URL for validation | 170 | // Get relay URL for validation |
| 167 | let relay_url = client | 171 | let relay_url = client |
| @@ -253,8 +257,8 @@ impl EventAcceptancePolicyTests { | |||
| 253 | ) -> TestResult { | 257 | ) -> TestResult { |
| 254 | TestResult::new( | 258 | TestResult::new( |
| 255 | "reject_repo_announcement_missing_clone_tag", | 259 | "reject_repo_announcement_missing_clone_tag", |
| 256 | "GRASP-01:nostr-relay:9", | 260 | SpecRef::NostrRelayRejectMissingCloneRelays, |
| 257 | "Reject repository announcements without service in clone tag", | 261 | "MUST reject announcements not listing service in clone tag", |
| 258 | ) | 262 | ) |
| 259 | .run(|| async { | 263 | .run(|| async { |
| 260 | // Get relay URL from client | 264 | // Get relay URL from client |
| @@ -329,8 +333,8 @@ impl EventAcceptancePolicyTests { | |||
| 329 | ) -> TestResult { | 333 | ) -> TestResult { |
| 330 | TestResult::new( | 334 | TestResult::new( |
| 331 | "reject_repo_announcement_missing_relays_tag", | 335 | "reject_repo_announcement_missing_relays_tag", |
| 332 | "GRASP-01:nostr-relay:9", | 336 | SpecRef::NostrRelayRejectMissingCloneRelays, |
| 333 | "Reject repository announcements without service in relays tag", | 337 | "MUST reject announcements not listing service in relays tag", |
| 334 | ) | 338 | ) |
| 335 | .run(|| async { | 339 | .run(|| async { |
| 336 | // Get relay URL from client | 340 | // Get relay URL from client |
| @@ -425,8 +429,8 @@ impl EventAcceptancePolicyTests { | |||
| 425 | ) -> TestResult { | 429 | ) -> TestResult { |
| 426 | TestResult::new( | 430 | TestResult::new( |
| 427 | "accept_recursive_maintainer_announcement_without_service", | 431 | "accept_recursive_maintainer_announcement_without_service", |
| 428 | "GRASP-01:nostr-relay:9", | 432 | SpecRef::NostrRelayRejectMissingCloneRelays, |
| 429 | "Accept recursive maintainer announcement for chain discovery (even without GRASP server in clone)", | 433 | "MUST accept recursive maintainer announcements for chain discovery", |
| 430 | ) | 434 | ) |
| 431 | .run(|| async { | 435 | .run(|| async { |
| 432 | // Create TestContext for mode-aware fixture management | 436 | // Create TestContext for mode-aware fixture management |
| @@ -593,7 +597,7 @@ impl EventAcceptancePolicyTests { | |||
| 593 | pub async fn test_accept_issue_via_a_tag(client: &AuditClient) -> TestResult { | 597 | pub async fn test_accept_issue_via_a_tag(client: &AuditClient) -> TestResult { |
| 594 | TestResult::new( | 598 | TestResult::new( |
| 595 | "accept_issue_via_a_tag", | 599 | "accept_issue_via_a_tag", |
| 596 | "GRASP-01:nostr-relay:13", | 600 | SpecRef::NostrRelayMustAcceptTaggedEvents, |
| 597 | "Accept issue referencing repo via 'a' tag", | 601 | "Accept issue referencing repo via 'a' tag", |
| 598 | ) | 602 | ) |
| 599 | .run(|| async { | 603 | .run(|| async { |
| @@ -601,12 +605,15 @@ impl EventAcceptancePolicyTests { | |||
| 601 | let ctx = TestContext::new(client); | 605 | let ctx = TestContext::new(client); |
| 602 | 606 | ||
| 603 | // NEW: Get repository fixture (mode-aware) | 607 | // NEW: Get repository fixture (mode-aware) |
| 604 | let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { | 608 | let repo = ctx |
| 605 | format!( | 609 | .get_fixture(FixtureKind::ValidRepoServed) |
| 606 | "Test setup failed: could not get valid repository fixture: {}", | 610 | .await |
| 607 | e | 611 | .map_err(|e| { |
| 608 | ) | 612 | format!( |
| 609 | })?; | 613 | "Test setup failed: could not get valid repository fixture: {}", |
| 614 | e | ||
| 615 | ) | ||
| 616 | })?; | ||
| 610 | 617 | ||
| 611 | // 2. Create issue that references the repo | 618 | // 2. Create issue that references the repo |
| 612 | let issue = Self::create_issue_for_repo(client, &repo, "Test Issue 1")?; | 619 | let issue = Self::create_issue_for_repo(client, &repo, "Test Issue 1")?; |
| @@ -628,7 +635,7 @@ impl EventAcceptancePolicyTests { | |||
| 628 | pub async fn test_accept_comment_via_capital_a_tag(client: &AuditClient) -> TestResult { | 635 | pub async fn test_accept_comment_via_capital_a_tag(client: &AuditClient) -> TestResult { |
| 629 | TestResult::new( | 636 | TestResult::new( |
| 630 | "accept_comment_via_A_tag", | 637 | "accept_comment_via_A_tag", |
| 631 | "GRASP-01:nostr-relay:13", | 638 | SpecRef::NostrRelayMustAcceptTaggedEvents, |
| 632 | "Accept NIP-22 comment with root 'A' tag referencing repo", | 639 | "Accept NIP-22 comment with root 'A' tag referencing repo", |
| 633 | ) | 640 | ) |
| 634 | .run(|| async { | 641 | .run(|| async { |
| @@ -636,12 +643,15 @@ impl EventAcceptancePolicyTests { | |||
| 636 | let ctx = TestContext::new(client); | 643 | let ctx = TestContext::new(client); |
| 637 | 644 | ||
| 638 | // Get repository fixture (mode-aware) | 645 | // Get repository fixture (mode-aware) |
| 639 | let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { | 646 | let repo = ctx |
| 640 | format!( | 647 | .get_fixture(FixtureKind::ValidRepoServed) |
| 641 | "Test setup failed: could not get valid repository fixture: {}", | 648 | .await |
| 642 | e | 649 | .map_err(|e| { |
| 643 | ) | 650 | format!( |
| 644 | })?; | 651 | "Test setup failed: could not get valid repository fixture: {}", |
| 652 | e | ||
| 653 | ) | ||
| 654 | })?; | ||
| 645 | 655 | ||
| 646 | // Extract repo_id and create `A` tag manually | 656 | // Extract repo_id and create `A` tag manually |
| 647 | let repo_id = | 657 | let repo_id = |
| @@ -681,20 +691,23 @@ impl EventAcceptancePolicyTests { | |||
| 681 | pub async fn test_accept_kind1_via_q_tag(client: &AuditClient) -> TestResult { | 691 | pub async fn test_accept_kind1_via_q_tag(client: &AuditClient) -> TestResult { |
| 682 | TestResult::new( | 692 | TestResult::new( |
| 683 | "accept_kind1_via_q_tag", | 693 | "accept_kind1_via_q_tag", |
| 684 | "GRASP-01:nostr-relay:13", | 694 | SpecRef::NostrRelayMustAcceptTaggedEvents, |
| 685 | "Accept kind 1 note quoting repo via 'q' tag", | 695 | "Accept kind 1 text note quoting repo via 'q' tag", |
| 686 | ) | 696 | ) |
| 687 | .run(|| async { | 697 | .run(|| async { |
| 688 | // Create TestContext | 698 | // Create TestContext |
| 689 | let ctx = TestContext::new(client); | 699 | let ctx = TestContext::new(client); |
| 690 | 700 | ||
| 691 | // Get repository fixture (mode-aware) | 701 | // Get repository fixture (mode-aware) |
| 692 | let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { | 702 | let repo = ctx |
| 693 | format!( | 703 | .get_fixture(FixtureKind::ValidRepoServed) |
| 694 | "Test setup failed: could not get valid repository fixture: {}", | 704 | .await |
| 695 | e | 705 | .map_err(|e| { |
| 696 | ) | 706 | format!( |
| 697 | })?; | 707 | "Test setup failed: could not get valid repository fixture: {}", |
| 708 | e | ||
| 709 | ) | ||
| 710 | })?; | ||
| 698 | 711 | ||
| 699 | // Extract repo_id and create `q` tag | 712 | // Extract repo_id and create `q` tag |
| 700 | let repo_id = | 713 | let repo_id = |
| @@ -731,8 +744,8 @@ impl EventAcceptancePolicyTests { | |||
| 731 | pub async fn test_accept_issue_quoting_issue_via_q(client: &AuditClient) -> TestResult { | 744 | pub async fn test_accept_issue_quoting_issue_via_q(client: &AuditClient) -> TestResult { |
| 732 | TestResult::new( | 745 | TestResult::new( |
| 733 | "accept_issue_quoting_issue_via_q", | 746 | "accept_issue_quoting_issue_via_q", |
| 734 | "GRASP-01:nostr-relay:13", | 747 | SpecRef::NostrRelayMustAcceptTaggedEvents, |
| 735 | "Accept issue quoting accepted issue (transitive)", | 748 | "Accept issue quoting another accepted issue (transitive)", |
| 736 | ) | 749 | ) |
| 737 | .run(|| async { | 750 | .run(|| async { |
| 738 | // Create TestContext | 751 | // Create TestContext |
| @@ -777,7 +790,7 @@ impl EventAcceptancePolicyTests { | |||
| 777 | pub async fn test_accept_comment_via_capital_e_tag(client: &AuditClient) -> TestResult { | 790 | pub async fn test_accept_comment_via_capital_e_tag(client: &AuditClient) -> TestResult { |
| 778 | TestResult::new( | 791 | TestResult::new( |
| 779 | "accept_comment_via_E_tag", | 792 | "accept_comment_via_E_tag", |
| 780 | "GRASP-01:nostr-relay:13", | 793 | SpecRef::NostrRelayMustAcceptTaggedEvents, |
| 781 | "Accept NIP-22 comment with root 'E' tag to accepted issue", | 794 | "Accept NIP-22 comment with root 'E' tag to accepted issue", |
| 782 | ) | 795 | ) |
| 783 | .run(|| async { | 796 | .run(|| async { |
| @@ -816,7 +829,7 @@ impl EventAcceptancePolicyTests { | |||
| 816 | pub async fn test_accept_kind1_via_e_tag(client: &AuditClient) -> TestResult { | 829 | pub async fn test_accept_kind1_via_e_tag(client: &AuditClient) -> TestResult { |
| 817 | TestResult::new( | 830 | TestResult::new( |
| 818 | "accept_kind1_via_e_tag", | 831 | "accept_kind1_via_e_tag", |
| 819 | "GRASP-01:nostr-relay:13", | 832 | SpecRef::NostrRelayMustAcceptTaggedEvents, |
| 820 | "Accept kind 1 reply via 'e' tag to accepted kind 1", | 833 | "Accept kind 1 reply via 'e' tag to accepted kind 1", |
| 821 | ) | 834 | ) |
| 822 | .run(|| async { | 835 | .run(|| async { |
| @@ -824,12 +837,15 @@ impl EventAcceptancePolicyTests { | |||
| 824 | let ctx = TestContext::new(client); | 837 | let ctx = TestContext::new(client); |
| 825 | 838 | ||
| 826 | // Get repository fixture (mode-aware) | 839 | // Get repository fixture (mode-aware) |
| 827 | let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { | 840 | let repo = ctx |
| 828 | format!( | 841 | .get_fixture(FixtureKind::ValidRepoServed) |
| 829 | "Test setup failed: could not get valid repository fixture: {}", | 842 | .await |
| 830 | e | 843 | .map_err(|e| { |
| 831 | ) | 844 | format!( |
| 832 | })?; | 845 | "Test setup failed: could not get valid repository fixture: {}", |
| 846 | e | ||
| 847 | ) | ||
| 848 | })?; | ||
| 833 | 849 | ||
| 834 | // Create Kind 1 A that quotes the repo (makes it accepted) | 850 | // Create Kind 1 A that quotes the repo (makes it accepted) |
| 835 | let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?; | 851 | let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?; |
| @@ -872,7 +888,7 @@ impl EventAcceptancePolicyTests { | |||
| 872 | pub async fn test_accept_kind1_referenced_in_issue(client: &AuditClient) -> TestResult { | 888 | pub async fn test_accept_kind1_referenced_in_issue(client: &AuditClient) -> TestResult { |
| 873 | TestResult::new( | 889 | TestResult::new( |
| 874 | "accept_kind1_referenced_in_issue", | 890 | "accept_kind1_referenced_in_issue", |
| 875 | "GRASP-01:nostr-relay:13", | 891 | SpecRef::NostrRelayMustAcceptTaggedEvents, |
| 876 | "Accept kind 1 referenced in accepted issue (forward ref)", | 892 | "Accept kind 1 referenced in accepted issue (forward ref)", |
| 877 | ) | 893 | ) |
| 878 | .run(|| async { | 894 | .run(|| async { |
| @@ -880,12 +896,15 @@ impl EventAcceptancePolicyTests { | |||
| 880 | let ctx = TestContext::new(client); | 896 | let ctx = TestContext::new(client); |
| 881 | 897 | ||
| 882 | // Get repository fixture (mode-aware) | 898 | // Get repository fixture (mode-aware) |
| 883 | let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { | 899 | let repo = ctx |
| 884 | format!( | 900 | .get_fixture(FixtureKind::ValidRepoServed) |
| 885 | "Test setup failed: could not get valid repository fixture: {}", | 901 | .await |
| 886 | e | 902 | .map_err(|e| { |
| 887 | ) | 903 | format!( |
| 888 | })?; | 904 | "Test setup failed: could not get valid repository fixture: {}", |
| 905 | e | ||
| 906 | ) | ||
| 907 | })?; | ||
| 889 | 908 | ||
| 890 | // Verify repo is queryable (ensures it's fully indexed before we reference it) | 909 | // Verify repo is queryable (ensures it's fully indexed before we reference it) |
| 891 | let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?; | 910 | let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?; |
| @@ -964,7 +983,7 @@ impl EventAcceptancePolicyTests { | |||
| 964 | pub async fn test_accept_comment_referenced_in_comment(client: &AuditClient) -> TestResult { | 983 | pub async fn test_accept_comment_referenced_in_comment(client: &AuditClient) -> TestResult { |
| 965 | TestResult::new( | 984 | TestResult::new( |
| 966 | "accept_comment_referenced_in_comment", | 985 | "accept_comment_referenced_in_comment", |
| 967 | "GRASP-01:nostr-relay:13", | 986 | SpecRef::NostrRelayMustAcceptTaggedEvents, |
| 968 | "Accept comment referenced in another accepted comment (forward ref)", | 987 | "Accept comment referenced in another accepted comment (forward ref)", |
| 969 | ) | 988 | ) |
| 970 | .run(|| async { | 989 | .run(|| async { |
| @@ -1025,7 +1044,7 @@ impl EventAcceptancePolicyTests { | |||
| 1025 | pub async fn test_accept_kind1_referenced_in_kind1(client: &AuditClient) -> TestResult { | 1044 | pub async fn test_accept_kind1_referenced_in_kind1(client: &AuditClient) -> TestResult { |
| 1026 | TestResult::new( | 1045 | TestResult::new( |
| 1027 | "accept_kind1_referenced_in_kind1", | 1046 | "accept_kind1_referenced_in_kind1", |
| 1028 | "GRASP-01:nostr-relay:13", | 1047 | SpecRef::NostrRelayMustAcceptTaggedEvents, |
| 1029 | "Accept kind 1 referenced in another accepted kind 1 (forward ref)", | 1048 | "Accept kind 1 referenced in another accepted kind 1 (forward ref)", |
| 1030 | ) | 1049 | ) |
| 1031 | .run(|| async { | 1050 | .run(|| async { |
| @@ -1033,12 +1052,15 @@ impl EventAcceptancePolicyTests { | |||
| 1033 | let ctx = TestContext::new(client); | 1052 | let ctx = TestContext::new(client); |
| 1034 | 1053 | ||
| 1035 | // Get repository fixture (mode-aware) | 1054 | // Get repository fixture (mode-aware) |
| 1036 | let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { | 1055 | let repo = ctx |
| 1037 | format!( | 1056 | .get_fixture(FixtureKind::ValidRepoServed) |
| 1038 | "Test setup failed: could not get valid repository fixture: {}", | 1057 | .await |
| 1039 | e | 1058 | .map_err(|e| { |
| 1040 | ) | 1059 | format!( |
| 1041 | })?; | 1060 | "Test setup failed: could not get valid repository fixture: {}", |
| 1061 | e | ||
| 1062 | ) | ||
| 1063 | })?; | ||
| 1042 | 1064 | ||
| 1043 | // Create Kind 1 A locally but DON'T send it yet | 1065 | // Create Kind 1 A locally but DON'T send it yet |
| 1044 | let kind1_a = client | 1066 | let kind1_a = client |
| @@ -1083,7 +1105,7 @@ impl EventAcceptancePolicyTests { | |||
| 1083 | pub async fn test_reject_orphan_issue(client: &AuditClient) -> TestResult { | 1105 | pub async fn test_reject_orphan_issue(client: &AuditClient) -> TestResult { |
| 1084 | TestResult::new( | 1106 | TestResult::new( |
| 1085 | "reject_orphan_issue", | 1107 | "reject_orphan_issue", |
| 1086 | "GRASP-01:nostr-relay:18", | 1108 | SpecRef::NostrRelayMayRejectSpamCuration, |
| 1087 | "Reject issue referencing unaccepted repo", | 1109 | "Reject issue referencing unaccepted repo", |
| 1088 | ) | 1110 | ) |
| 1089 | .run(|| async { | 1111 | .run(|| async { |
| @@ -1110,7 +1132,7 @@ impl EventAcceptancePolicyTests { | |||
| 1110 | pub async fn test_reject_orphan_kind1(client: &AuditClient) -> TestResult { | 1132 | pub async fn test_reject_orphan_kind1(client: &AuditClient) -> TestResult { |
| 1111 | TestResult::new( | 1133 | TestResult::new( |
| 1112 | "reject_orphan_kind1", | 1134 | "reject_orphan_kind1", |
| 1113 | "GRASP-01:nostr-relay:18", | 1135 | SpecRef::NostrRelayMayRejectSpamCuration, |
| 1114 | "Reject kind 1 with no repo references", | 1136 | "Reject kind 1 with no repo references", |
| 1115 | ) | 1137 | ) |
| 1116 | .run(|| async { | 1138 | .run(|| async { |
| @@ -1139,7 +1161,7 @@ impl EventAcceptancePolicyTests { | |||
| 1139 | pub async fn test_reject_comment_quoting_other_repo(client: &AuditClient) -> TestResult { | 1161 | pub async fn test_reject_comment_quoting_other_repo(client: &AuditClient) -> TestResult { |
| 1140 | TestResult::new( | 1162 | TestResult::new( |
| 1141 | "reject_comment_quoting_other_repo", | 1163 | "reject_comment_quoting_other_repo", |
| 1142 | "GRASP-01:nostr-relay:18", | 1164 | SpecRef::NostrRelayMayRejectSpamCuration, |
| 1143 | "Reject comment quoting unaccepted repo", | 1165 | "Reject comment quoting unaccepted repo", |
| 1144 | ) | 1166 | ) |
| 1145 | .run(|| async { | 1167 | .run(|| async { |
| @@ -1147,12 +1169,15 @@ impl EventAcceptancePolicyTests { | |||
| 1147 | let ctx = TestContext::new(client); | 1169 | let ctx = TestContext::new(client); |
| 1148 | 1170 | ||
| 1149 | // Get accepted repo A fixture (mode-aware) | 1171 | // Get accepted repo A fixture (mode-aware) |
| 1150 | let _repo_a = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { | 1172 | let _repo_a = ctx |
| 1151 | format!( | 1173 | .get_fixture(FixtureKind::ValidRepoServed) |
| 1152 | "Test setup failed: could not get valid repository fixture: {}", | 1174 | .await |
| 1153 | e | 1175 | .map_err(|e| { |
| 1154 | ) | 1176 | format!( |
| 1155 | })?; | 1177 | "Test setup failed: could not get valid repository fixture: {}", |
| 1178 | e | ||
| 1179 | ) | ||
| 1180 | })?; | ||
| 1156 | 1181 | ||
| 1157 | // Create Repo B but DON'T send it (unaccepted) | 1182 | // Create Repo B but DON'T send it (unaccepted) |
| 1158 | let repo_b = Self::create_test_repo(client, "unaccepted-repo-b").await?; | 1183 | let repo_b = Self::create_test_repo(client, "unaccepted-repo-b").await?; |
diff --git a/grasp-audit/src/specs/grasp01/git_clone.rs b/grasp-audit/src/specs/grasp01/git_clone.rs index e162558..0c223f4 100644 --- a/grasp-audit/src/specs/grasp01/git_clone.rs +++ b/grasp-audit/src/specs/grasp01/git_clone.rs | |||
| @@ -15,6 +15,7 @@ | |||
| 15 | //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test | 15 | //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test |
| 16 | //! ``` | 16 | //! ``` |
| 17 | 17 | ||
| 18 | use crate::specs::grasp01::SpecRef; | ||
| 18 | use crate::{AuditClient, FixtureKind, TestContext, TestResult}; | 19 | use crate::{AuditClient, FixtureKind, TestContext, TestResult}; |
| 19 | use nostr_sdk::prelude::*; | 20 | use nostr_sdk::prelude::*; |
| 20 | use std::fs; | 21 | use std::fs; |
| @@ -48,12 +49,12 @@ impl GitCloneTests { | |||
| 48 | let ctx = TestContext::new(client); | 49 | let ctx = TestContext::new(client); |
| 49 | 50 | ||
| 50 | // Create repository announcement | 51 | // Create repository announcement |
| 51 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 52 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { |
| 52 | Ok(r) => r, | 53 | Ok(r) => r, |
| 53 | Err(e) => { | 54 | Err(e) => { |
| 54 | return TestResult::new( | 55 | return TestResult::new( |
| 55 | test_name, | 56 | test_name, |
| 56 | "GRASP-01:git-http:34", | 57 | SpecRef::GitServeRepository, |
| 57 | "Repository must be cloneable via Git HTTP backend", | 58 | "Repository must be cloneable via Git HTTP backend", |
| 58 | ) | 59 | ) |
| 59 | .fail(format!("Failed to create repo fixture: {}", e)) | 60 | .fail(format!("Failed to create repo fixture: {}", e)) |
| @@ -74,7 +75,7 @@ impl GitCloneTests { | |||
| 74 | None => { | 75 | None => { |
| 75 | return TestResult::new( | 76 | return TestResult::new( |
| 76 | test_name, | 77 | test_name, |
| 77 | "GRASP-01", | 78 | SpecRef::GitServeRepository, |
| 78 | "Repository must be cloneable via Git HTTP backend", | 79 | "Repository must be cloneable via Git HTTP backend", |
| 79 | ) | 80 | ) |
| 80 | .fail("Repository announcement missing d tag") | 81 | .fail("Repository announcement missing d tag") |
| @@ -86,7 +87,7 @@ impl GitCloneTests { | |||
| 86 | Err(e) => { | 87 | Err(e) => { |
| 87 | return TestResult::new( | 88 | return TestResult::new( |
| 88 | test_name, | 89 | test_name, |
| 89 | "GRASP-01:git-http:34", | 90 | SpecRef::GitServeRepository, |
| 90 | "Repository must be cloneable via Git HTTP backend", | 91 | "Repository must be cloneable via Git HTTP backend", |
| 91 | ) | 92 | ) |
| 92 | .fail(format!("Failed to convert pubkey to npub: {}", e)) | 93 | .fail(format!("Failed to convert pubkey to npub: {}", e)) |
| @@ -121,7 +122,7 @@ impl GitCloneTests { | |||
| 121 | cleanup(); | 122 | cleanup(); |
| 122 | return TestResult::new( | 123 | return TestResult::new( |
| 123 | test_name, | 124 | test_name, |
| 124 | "GRASP-01:git-http:34", | 125 | SpecRef::GitServeRepository, |
| 125 | "Repository must be cloneable via Git HTTP backend", | 126 | "Repository must be cloneable via Git HTTP backend", |
| 126 | ) | 127 | ) |
| 127 | .fail(format!("Failed to execute git clone: {}", e)); | 128 | .fail(format!("Failed to execute git clone: {}", e)); |
| @@ -133,7 +134,7 @@ impl GitCloneTests { | |||
| 133 | let stderr = String::from_utf8_lossy(&output.stderr); | 134 | let stderr = String::from_utf8_lossy(&output.stderr); |
| 134 | return TestResult::new( | 135 | return TestResult::new( |
| 135 | test_name, | 136 | test_name, |
| 136 | "GRASP-01:git-http:34", | 137 | SpecRef::GitServeRepository, |
| 137 | "Repository must be cloneable via Git HTTP backend", | 138 | "Repository must be cloneable via Git HTTP backend", |
| 138 | ) | 139 | ) |
| 139 | .fail(format!("Git clone failed: {}", stderr)); | 140 | .fail(format!("Git clone failed: {}", stderr)); |
| @@ -144,7 +145,7 @@ impl GitCloneTests { | |||
| 144 | cleanup(); | 145 | cleanup(); |
| 145 | return TestResult::new( | 146 | return TestResult::new( |
| 146 | test_name, | 147 | test_name, |
| 147 | "GRASP-01:git-http:34", | 148 | SpecRef::GitServeRepository, |
| 148 | "Repository must be cloneable via Git HTTP backend", | 149 | "Repository must be cloneable via Git HTTP backend", |
| 149 | ) | 150 | ) |
| 150 | .fail("Cloned repository missing .git directory"); | 151 | .fail("Cloned repository missing .git directory"); |
| @@ -153,7 +154,7 @@ impl GitCloneTests { | |||
| 153 | cleanup(); | 154 | cleanup(); |
| 154 | TestResult::new( | 155 | TestResult::new( |
| 155 | test_name, | 156 | test_name, |
| 156 | "GRASP-01:git-http:34", | 157 | SpecRef::GitServeRepository, |
| 157 | "Repository must be cloneable via Git HTTP backend", | 158 | "Repository must be cloneable via Git HTTP backend", |
| 158 | ) | 159 | ) |
| 159 | .pass() | 160 | .pass() |
| @@ -170,12 +171,12 @@ impl GitCloneTests { | |||
| 170 | let ctx = TestContext::new(client); | 171 | let ctx = TestContext::new(client); |
| 171 | 172 | ||
| 172 | // Create repository announcement | 173 | // Create repository announcement |
| 173 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 174 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { |
| 174 | Ok(r) => r, | 175 | Ok(r) => r, |
| 175 | Err(e) => { | 176 | Err(e) => { |
| 176 | return TestResult::new( | 177 | return TestResult::new( |
| 177 | test_name, | 178 | test_name, |
| 178 | "GRASP-01:git-http:34", | 179 | SpecRef::GitServeRepository, |
| 179 | "Clone URL must follow correct format", | 180 | "Clone URL must follow correct format", |
| 180 | ) | 181 | ) |
| 181 | .fail(format!("Failed to create repo fixture: {}", e)) | 182 | .fail(format!("Failed to create repo fixture: {}", e)) |
| @@ -203,7 +204,7 @@ impl GitCloneTests { | |||
| 203 | if !valid_url.contains(&npub) { | 204 | if !valid_url.contains(&npub) { |
| 204 | return TestResult::new( | 205 | return TestResult::new( |
| 205 | test_name, | 206 | test_name, |
| 206 | "GRASP-01:git-http:34", | 207 | SpecRef::GitServeRepository, |
| 207 | "Clone URL must follow correct format", | 208 | "Clone URL must follow correct format", |
| 208 | ) | 209 | ) |
| 209 | .fail("URL missing npub"); | 210 | .fail("URL missing npub"); |
| @@ -212,7 +213,7 @@ impl GitCloneTests { | |||
| 212 | if !valid_url.contains(&format!("{}.git", repo_id)) { | 213 | if !valid_url.contains(&format!("{}.git", repo_id)) { |
| 213 | return TestResult::new( | 214 | return TestResult::new( |
| 214 | test_name, | 215 | test_name, |
| 215 | "GRASP-01:git-http:34", | 216 | SpecRef::GitServeRepository, |
| 216 | "Clone URL must follow correct format", | 217 | "Clone URL must follow correct format", |
| 217 | ) | 218 | ) |
| 218 | .fail("URL missing repository identifier"); | 219 | .fail("URL missing repository identifier"); |
| @@ -241,7 +242,7 @@ impl GitCloneTests { | |||
| 241 | if output.status.success() { | 242 | if output.status.success() { |
| 242 | return TestResult::new( | 243 | return TestResult::new( |
| 243 | test_name, | 244 | test_name, |
| 244 | "GRASP-01:git-http:34", | 245 | SpecRef::GitServeRepository, |
| 245 | "Clone URL must follow correct format", | 246 | "Clone URL must follow correct format", |
| 246 | ) | 247 | ) |
| 247 | .fail("Invalid URL was accepted (should have been rejected)"); | 248 | .fail("Invalid URL was accepted (should have been rejected)"); |
| @@ -249,7 +250,7 @@ impl GitCloneTests { | |||
| 249 | 250 | ||
| 250 | TestResult::new( | 251 | TestResult::new( |
| 251 | test_name, | 252 | test_name, |
| 252 | "GRASP-01:git-http:34", | 253 | SpecRef::GitServeRepository, |
| 253 | "Clone URL must follow correct format", | 254 | "Clone URL must follow correct format", |
| 254 | ) | 255 | ) |
| 255 | .pass() | 256 | .pass() |
| @@ -273,12 +274,12 @@ impl GitCloneTests { | |||
| 273 | let ctx = TestContext::new(client); | 274 | let ctx = TestContext::new(client); |
| 274 | 275 | ||
| 275 | // Create repository announcement | 276 | // Create repository announcement |
| 276 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 277 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { |
| 277 | Ok(r) => r, | 278 | Ok(r) => r, |
| 278 | Err(e) => { | 279 | Err(e) => { |
| 279 | return TestResult::new( | 280 | return TestResult::new( |
| 280 | test_name, | 281 | test_name, |
| 281 | "GRASP-01:git-http:42", | 282 | SpecRef::GitIncludeAllowSha1InWant, |
| 282 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", | 283 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", |
| 283 | ) | 284 | ) |
| 284 | .fail(format!("Failed to create repo fixture: {}", e)) | 285 | .fail(format!("Failed to create repo fixture: {}", e)) |
| @@ -299,7 +300,7 @@ impl GitCloneTests { | |||
| 299 | None => { | 300 | None => { |
| 300 | return TestResult::new( | 301 | return TestResult::new( |
| 301 | test_name, | 302 | test_name, |
| 302 | "GRASP-01:git-http:42", | 303 | SpecRef::GitIncludeAllowSha1InWant, |
| 303 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", | 304 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", |
| 304 | ) | 305 | ) |
| 305 | .fail("Repository announcement missing d tag") | 306 | .fail("Repository announcement missing d tag") |
| @@ -311,7 +312,7 @@ impl GitCloneTests { | |||
| 311 | Err(e) => { | 312 | Err(e) => { |
| 312 | return TestResult::new( | 313 | return TestResult::new( |
| 313 | test_name, | 314 | test_name, |
| 314 | "GRASP-01:git-http:42", | 315 | SpecRef::GitIncludeAllowSha1InWant, |
| 315 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", | 316 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", |
| 316 | ) | 317 | ) |
| 317 | .fail(format!("Failed to convert pubkey to npub: {}", e)) | 318 | .fail(format!("Failed to convert pubkey to npub: {}", e)) |
| @@ -331,7 +332,7 @@ impl GitCloneTests { | |||
| 331 | Err(e) => { | 332 | Err(e) => { |
| 332 | return TestResult::new( | 333 | return TestResult::new( |
| 333 | test_name, | 334 | test_name, |
| 334 | "GRASP-01:git-http:42", | 335 | SpecRef::GitIncludeAllowSha1InWant, |
| 335 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", | 336 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", |
| 336 | ) | 337 | ) |
| 337 | .fail(format!("HTTP request failed: {}", e)) | 338 | .fail(format!("HTTP request failed: {}", e)) |
| @@ -341,7 +342,7 @@ impl GitCloneTests { | |||
| 341 | if !response.status().is_success() { | 342 | if !response.status().is_success() { |
| 342 | return TestResult::new( | 343 | return TestResult::new( |
| 343 | test_name, | 344 | test_name, |
| 344 | "GRASP-01:git-http:42", | 345 | SpecRef::GitIncludeAllowSha1InWant, |
| 345 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", | 346 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", |
| 346 | ) | 347 | ) |
| 347 | .fail(format!( | 348 | .fail(format!( |
| @@ -356,7 +357,7 @@ impl GitCloneTests { | |||
| 356 | Err(e) => { | 357 | Err(e) => { |
| 357 | return TestResult::new( | 358 | return TestResult::new( |
| 358 | test_name, | 359 | test_name, |
| 359 | "GRASP-01:git-http:42", | 360 | SpecRef::GitIncludeAllowSha1InWant, |
| 360 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", | 361 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", |
| 361 | ) | 362 | ) |
| 362 | .fail(format!("Failed to read response body: {}", e)) | 363 | .fail(format!("Failed to read response body: {}", e)) |
| @@ -370,7 +371,7 @@ impl GitCloneTests { | |||
| 370 | if !has_allow_reachable { | 371 | if !has_allow_reachable { |
| 371 | return TestResult::new( | 372 | return TestResult::new( |
| 372 | test_name, | 373 | test_name, |
| 373 | "GRASP-01:git-http:42", | 374 | SpecRef::GitIncludeAllowSha1InWant, |
| 374 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", | 375 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", |
| 375 | ) | 376 | ) |
| 376 | .fail("Missing capability: allow-reachable-sha1-in-want"); | 377 | .fail("Missing capability: allow-reachable-sha1-in-want"); |
| @@ -379,7 +380,7 @@ impl GitCloneTests { | |||
| 379 | if !has_allow_tip { | 380 | if !has_allow_tip { |
| 380 | return TestResult::new( | 381 | return TestResult::new( |
| 381 | test_name, | 382 | test_name, |
| 382 | "GRASP-01:git-http:42", | 383 | SpecRef::GitIncludeAllowSha1InWant, |
| 383 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", | 384 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", |
| 384 | ) | 385 | ) |
| 385 | .fail("Missing capability: allow-tip-sha1-in-want"); | 386 | .fail("Missing capability: allow-tip-sha1-in-want"); |
| @@ -387,7 +388,7 @@ impl GitCloneTests { | |||
| 387 | 388 | ||
| 388 | TestResult::new( | 389 | TestResult::new( |
| 389 | test_name, | 390 | test_name, |
| 390 | "GRASP-01:git-http:42", | 391 | SpecRef::GitIncludeAllowSha1InWant, |
| 391 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", | 392 | "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", |
| 392 | ) | 393 | ) |
| 393 | .pass() | 394 | .pass() |
diff --git a/grasp-audit/src/specs/grasp01/git_filter.rs b/grasp-audit/src/specs/grasp01/git_filter.rs index 21bab0a..31d86aa 100644 --- a/grasp-audit/src/specs/grasp01/git_filter.rs +++ b/grasp-audit/src/specs/grasp01/git_filter.rs | |||
| @@ -22,6 +22,7 @@ | |||
| 22 | //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test | 22 | //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test |
| 23 | //! ``` | 23 | //! ``` |
| 24 | 24 | ||
| 25 | use crate::specs::grasp01::SpecRef; | ||
| 25 | use crate::{AuditClient, FixtureKind, TestContext, TestResult}; | 26 | use crate::{AuditClient, FixtureKind, TestContext, TestResult}; |
| 26 | use nostr_sdk::prelude::*; | 27 | use nostr_sdk::prelude::*; |
| 27 | use std::fs; | 28 | use std::fs; |
| @@ -61,12 +62,12 @@ impl GitFilterTests { | |||
| 61 | let ctx = TestContext::new(client); | 62 | let ctx = TestContext::new(client); |
| 62 | 63 | ||
| 63 | // Create repository announcement | 64 | // Create repository announcement |
| 64 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 65 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { |
| 65 | Ok(r) => r, | 66 | Ok(r) => r, |
| 66 | Err(e) => { | 67 | Err(e) => { |
| 67 | return TestResult::new( | 68 | return TestResult::new( |
| 68 | test_name, | 69 | test_name, |
| 69 | "GRASP-01:git-http:42", | 70 | SpecRef::GitIncludeAllowSha1InWant, |
| 70 | "MUST include uploadpack.allowFilter in advertisement", | 71 | "MUST include uploadpack.allowFilter in advertisement", |
| 71 | ) | 72 | ) |
| 72 | .fail(format!("Failed to create repo fixture: {}", e)) | 73 | .fail(format!("Failed to create repo fixture: {}", e)) |
| @@ -87,7 +88,7 @@ impl GitFilterTests { | |||
| 87 | None => { | 88 | None => { |
| 88 | return TestResult::new( | 89 | return TestResult::new( |
| 89 | test_name, | 90 | test_name, |
| 90 | "GRASP-01:git-http:42", | 91 | SpecRef::GitIncludeAllowSha1InWant, |
| 91 | "MUST include uploadpack.allowFilter in advertisement", | 92 | "MUST include uploadpack.allowFilter in advertisement", |
| 92 | ) | 93 | ) |
| 93 | .fail("Repository announcement missing d tag") | 94 | .fail("Repository announcement missing d tag") |
| @@ -99,7 +100,7 @@ impl GitFilterTests { | |||
| 99 | Err(e) => { | 100 | Err(e) => { |
| 100 | return TestResult::new( | 101 | return TestResult::new( |
| 101 | test_name, | 102 | test_name, |
| 102 | "GRASP-01:git-http:42", | 103 | SpecRef::GitIncludeAllowSha1InWant, |
| 103 | "MUST include uploadpack.allowFilter in advertisement", | 104 | "MUST include uploadpack.allowFilter in advertisement", |
| 104 | ) | 105 | ) |
| 105 | .fail(format!("Failed to convert pubkey to npub: {}", e)) | 106 | .fail(format!("Failed to convert pubkey to npub: {}", e)) |
| @@ -119,7 +120,7 @@ impl GitFilterTests { | |||
| 119 | Err(e) => { | 120 | Err(e) => { |
| 120 | return TestResult::new( | 121 | return TestResult::new( |
| 121 | test_name, | 122 | test_name, |
| 122 | "GRASP-01:git-http:42", | 123 | SpecRef::GitIncludeAllowSha1InWant, |
| 123 | "MUST include uploadpack.allowFilter in advertisement", | 124 | "MUST include uploadpack.allowFilter in advertisement", |
| 124 | ) | 125 | ) |
| 125 | .fail(format!("HTTP request failed: {}", e)) | 126 | .fail(format!("HTTP request failed: {}", e)) |
| @@ -129,7 +130,7 @@ impl GitFilterTests { | |||
| 129 | if !response.status().is_success() { | 130 | if !response.status().is_success() { |
| 130 | return TestResult::new( | 131 | return TestResult::new( |
| 131 | test_name, | 132 | test_name, |
| 132 | "GRASP-01:git-http:42", | 133 | SpecRef::GitIncludeAllowSha1InWant, |
| 133 | "MUST include uploadpack.allowFilter in advertisement", | 134 | "MUST include uploadpack.allowFilter in advertisement", |
| 134 | ) | 135 | ) |
| 135 | .fail(format!( | 136 | .fail(format!( |
| @@ -144,7 +145,7 @@ impl GitFilterTests { | |||
| 144 | Err(e) => { | 145 | Err(e) => { |
| 145 | return TestResult::new( | 146 | return TestResult::new( |
| 146 | test_name, | 147 | test_name, |
| 147 | "GRASP-01:git-http:42", | 148 | SpecRef::GitIncludeAllowSha1InWant, |
| 148 | "MUST include uploadpack.allowFilter in advertisement", | 149 | "MUST include uploadpack.allowFilter in advertisement", |
| 149 | ) | 150 | ) |
| 150 | .fail(format!("Failed to read response body: {}", e)) | 151 | .fail(format!("Failed to read response body: {}", e)) |
| @@ -155,7 +156,7 @@ impl GitFilterTests { | |||
| 155 | if !body.contains("filter") { | 156 | if !body.contains("filter") { |
| 156 | return TestResult::new( | 157 | return TestResult::new( |
| 157 | test_name, | 158 | test_name, |
| 158 | "GRASP-01:git-http:42", | 159 | SpecRef::GitIncludeAllowSha1InWant, |
| 159 | "MUST include uploadpack.allowFilter in advertisement", | 160 | "MUST include uploadpack.allowFilter in advertisement", |
| 160 | ) | 161 | ) |
| 161 | .fail("Missing capability: filter"); | 162 | .fail("Missing capability: filter"); |
| @@ -163,7 +164,7 @@ impl GitFilterTests { | |||
| 163 | 164 | ||
| 164 | TestResult::new( | 165 | TestResult::new( |
| 165 | test_name, | 166 | test_name, |
| 166 | "GRASP-01:git-http:42", | 167 | SpecRef::GitIncludeAllowSha1InWant, |
| 167 | "MUST include uploadpack.allowFilter in advertisement", | 168 | "MUST include uploadpack.allowFilter in advertisement", |
| 168 | ) | 169 | ) |
| 169 | .pass() | 170 | .pass() |
| @@ -184,12 +185,12 @@ impl GitFilterTests { | |||
| 184 | let ctx = TestContext::new(client); | 185 | let ctx = TestContext::new(client); |
| 185 | 186 | ||
| 186 | // Create repository announcement | 187 | // Create repository announcement |
| 187 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 188 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { |
| 188 | Ok(r) => r, | 189 | Ok(r) => r, |
| 189 | Err(e) => { | 190 | Err(e) => { |
| 190 | return TestResult::new( | 191 | return TestResult::new( |
| 191 | test_name, | 192 | test_name, |
| 192 | "GRASP-01:git-http:42", | 193 | SpecRef::GitIncludeAllowSha1InWant, |
| 193 | "MUST serve filtered clone requests", | 194 | "MUST serve filtered clone requests", |
| 194 | ) | 195 | ) |
| 195 | .fail(format!("Failed to create repo fixture: {}", e)) | 196 | .fail(format!("Failed to create repo fixture: {}", e)) |
| @@ -243,7 +244,7 @@ impl GitFilterTests { | |||
| 243 | cleanup(); | 244 | cleanup(); |
| 244 | return TestResult::new( | 245 | return TestResult::new( |
| 245 | test_name, | 246 | test_name, |
| 246 | "GRASP-01:git-http:42", | 247 | SpecRef::GitIncludeAllowSha1InWant, |
| 247 | "MUST serve filtered clone requests", | 248 | "MUST serve filtered clone requests", |
| 248 | ) | 249 | ) |
| 249 | .fail(format!("Failed to execute git clone: {}", e)); | 250 | .fail(format!("Failed to execute git clone: {}", e)); |
| @@ -255,7 +256,7 @@ impl GitFilterTests { | |||
| 255 | let stderr = String::from_utf8_lossy(&output.stderr); | 256 | let stderr = String::from_utf8_lossy(&output.stderr); |
| 256 | return TestResult::new( | 257 | return TestResult::new( |
| 257 | test_name, | 258 | test_name, |
| 258 | "GRASP-01:git-http:42", | 259 | SpecRef::GitIncludeAllowSha1InWant, |
| 259 | "MUST serve filtered clone requests", | 260 | "MUST serve filtered clone requests", |
| 260 | ) | 261 | ) |
| 261 | .fail(format!("Filtered git clone failed: {}", stderr)); | 262 | .fail(format!("Filtered git clone failed: {}", stderr)); |
| @@ -266,7 +267,7 @@ impl GitFilterTests { | |||
| 266 | cleanup(); | 267 | cleanup(); |
| 267 | return TestResult::new( | 268 | return TestResult::new( |
| 268 | test_name, | 269 | test_name, |
| 269 | "GRASP-01:git-http:42", | 270 | SpecRef::GitIncludeAllowSha1InWant, |
| 270 | "MUST serve filtered clone requests", | 271 | "MUST serve filtered clone requests", |
| 271 | ) | 272 | ) |
| 272 | .fail("Filtered clone missing .git directory"); | 273 | .fail("Filtered clone missing .git directory"); |
| @@ -275,7 +276,7 @@ impl GitFilterTests { | |||
| 275 | cleanup(); | 276 | cleanup(); |
| 276 | TestResult::new( | 277 | TestResult::new( |
| 277 | test_name, | 278 | test_name, |
| 278 | "GRASP-01:git-http:42", | 279 | SpecRef::GitIncludeAllowSha1InWant, |
| 279 | "MUST serve filtered clone requests", | 280 | "MUST serve filtered clone requests", |
| 280 | ) | 281 | ) |
| 281 | .pass() | 282 | .pass() |
| @@ -295,12 +296,12 @@ impl GitFilterTests { | |||
| 295 | let ctx = TestContext::new(client); | 296 | let ctx = TestContext::new(client); |
| 296 | 297 | ||
| 297 | // Create repository announcement | 298 | // Create repository announcement |
| 298 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 299 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { |
| 299 | Ok(r) => r, | 300 | Ok(r) => r, |
| 300 | Err(e) => { | 301 | Err(e) => { |
| 301 | return TestResult::new( | 302 | return TestResult::new( |
| 302 | test_name, | 303 | test_name, |
| 303 | "GRASP-01:git-http:42", | 304 | SpecRef::GitIncludeAllowSha1InWant, |
| 304 | "MUST serve filtered fetch requests", | 305 | "MUST serve filtered fetch requests", |
| 305 | ) | 306 | ) |
| 306 | .fail(format!("Failed to create repo fixture: {}", e)) | 307 | .fail(format!("Failed to create repo fixture: {}", e)) |
| @@ -352,7 +353,7 @@ impl GitFilterTests { | |||
| 352 | cleanup(); | 353 | cleanup(); |
| 353 | return TestResult::new( | 354 | return TestResult::new( |
| 354 | test_name, | 355 | test_name, |
| 355 | "GRASP-01:git-http:42", | 356 | SpecRef::GitIncludeAllowSha1InWant, |
| 356 | "MUST serve filtered fetch requests", | 357 | "MUST serve filtered fetch requests", |
| 357 | ) | 358 | ) |
| 358 | .fail("Failed to create initial shallow clone for fetch test"); | 359 | .fail("Failed to create initial shallow clone for fetch test"); |
| @@ -371,7 +372,7 @@ impl GitFilterTests { | |||
| 371 | cleanup(); | 372 | cleanup(); |
| 372 | return TestResult::new( | 373 | return TestResult::new( |
| 373 | test_name, | 374 | test_name, |
| 374 | "GRASP-01:git-http:42", | 375 | SpecRef::GitIncludeAllowSha1InWant, |
| 375 | "MUST serve filtered fetch requests", | 376 | "MUST serve filtered fetch requests", |
| 376 | ) | 377 | ) |
| 377 | .fail(format!("Failed to execute git fetch: {}", e)); | 378 | .fail(format!("Failed to execute git fetch: {}", e)); |
| @@ -383,7 +384,7 @@ impl GitFilterTests { | |||
| 383 | let stderr = String::from_utf8_lossy(&output.stderr); | 384 | let stderr = String::from_utf8_lossy(&output.stderr); |
| 384 | return TestResult::new( | 385 | return TestResult::new( |
| 385 | test_name, | 386 | test_name, |
| 386 | "GRASP-01:git-http:42", | 387 | SpecRef::GitIncludeAllowSha1InWant, |
| 387 | "MUST serve filtered fetch requests", | 388 | "MUST serve filtered fetch requests", |
| 388 | ) | 389 | ) |
| 389 | .fail(format!("Filtered git fetch failed: {}", stderr)); | 390 | .fail(format!("Filtered git fetch failed: {}", stderr)); |
| @@ -392,7 +393,7 @@ impl GitFilterTests { | |||
| 392 | cleanup(); | 393 | cleanup(); |
| 393 | TestResult::new( | 394 | TestResult::new( |
| 394 | test_name, | 395 | test_name, |
| 395 | "GRASP-01:git-http:42", | 396 | SpecRef::GitIncludeAllowSha1InWant, |
| 396 | "MUST serve filtered fetch requests", | 397 | "MUST serve filtered fetch requests", |
| 397 | ) | 398 | ) |
| 398 | .pass() | 399 | .pass() |
diff --git a/grasp-audit/src/specs/grasp01/mod.rs b/grasp-audit/src/specs/grasp01/mod.rs index 0a819ee..1694f58 100644 --- a/grasp-audit/src/specs/grasp01/mod.rs +++ b/grasp-audit/src/specs/grasp01/mod.rs | |||
| @@ -19,6 +19,7 @@ pub mod git_clone; | |||
| 19 | pub mod git_filter; | 19 | pub mod git_filter; |
| 20 | pub mod nip01_smoke; | 20 | pub mod nip01_smoke; |
| 21 | pub mod nip11_document; | 21 | pub mod nip11_document; |
| 22 | pub mod purgatory; | ||
| 22 | pub mod push_authorization; | 23 | pub mod push_authorization; |
| 23 | pub mod repository_creation; | 24 | pub mod repository_creation; |
| 24 | pub mod spec_requirements; | 25 | pub mod spec_requirements; |
| @@ -29,9 +30,10 @@ pub use git_clone::GitCloneTests; | |||
| 29 | pub use git_filter::GitFilterTests; | 30 | pub use git_filter::GitFilterTests; |
| 30 | pub use nip01_smoke::Nip01SmokeTests; | 31 | pub use nip01_smoke::Nip01SmokeTests; |
| 31 | pub use nip11_document::Nip11DocumentTests; | 32 | pub use nip11_document::Nip11DocumentTests; |
| 33 | pub use purgatory::PurgatoryTests; | ||
| 32 | pub use push_authorization::PushAuthorizationTests; | 34 | pub use push_authorization::PushAuthorizationTests; |
| 33 | pub use repository_creation::RepositoryCreationTests; | 35 | pub use repository_creation::RepositoryCreationTests; |
| 34 | pub use spec_requirements::{ | 36 | pub use spec_requirements::{ |
| 35 | get_requirement, get_requirements_for_section, get_sections, RequirementLevel, SpecRequirement, | 37 | get_requirement, get_requirement_by_ref, get_requirements_for_section, get_sections, |
| 36 | GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID, | 38 | RequirementLevel, SpecRef, SpecRequirement, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID, |
| 37 | }; | 39 | }; |
diff --git a/grasp-audit/src/specs/grasp01/nip01_smoke.rs b/grasp-audit/src/specs/grasp01/nip01_smoke.rs index 4d0b8a4..e3206fc 100644 --- a/grasp-audit/src/specs/grasp01/nip01_smoke.rs +++ b/grasp-audit/src/specs/grasp01/nip01_smoke.rs | |||
| @@ -4,6 +4,7 @@ | |||
| 4 | //! We don't comprehensively test NIP-01 because rust-nostr already has 1000+ tests. | 4 | //! We don't comprehensively test NIP-01 because rust-nostr already has 1000+ tests. |
| 5 | //! These are just smoke tests to ensure the relay is working at all. | 5 | //! These are just smoke tests to ensure the relay is working at all. |
| 6 | 6 | ||
| 7 | use crate::specs::grasp01::SpecRef; | ||
| 7 | use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; | 8 | use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; |
| 8 | use nostr_sdk::prelude::*; | 9 | use nostr_sdk::prelude::*; |
| 9 | 10 | ||
| @@ -32,8 +33,8 @@ impl Nip01SmokeTests { | |||
| 32 | pub async fn test_websocket_connection(client: &AuditClient) -> TestResult { | 33 | pub async fn test_websocket_connection(client: &AuditClient) -> TestResult { |
| 33 | TestResult::new( | 34 | TestResult::new( |
| 34 | "websocket_connection", | 35 | "websocket_connection", |
| 35 | "GRASP-01:nostr-relay:7", | 36 | SpecRef::NostrRelayNip01Compliant, |
| 36 | "Can establish WebSocket connection to /", | 37 | "MUST serve a relay at / via WebSocket", |
| 37 | ) | 38 | ) |
| 38 | .run(|| async { | 39 | .run(|| async { |
| 39 | if !client.is_connected().await { | 40 | if !client.is_connected().await { |
| @@ -61,16 +62,16 @@ impl Nip01SmokeTests { | |||
| 61 | pub async fn test_send_receive_event(client: &AuditClient) -> TestResult { | 62 | pub async fn test_send_receive_event(client: &AuditClient) -> TestResult { |
| 62 | TestResult::new( | 63 | TestResult::new( |
| 63 | "send_receive_event", | 64 | "send_receive_event", |
| 64 | "GRASP-01:nostr-relay:7", | 65 | SpecRef::NostrRelayNip01Compliant, |
| 65 | "Can send EVENT and receive OK response", | 66 | "MUST accept valid EVENT messages", |
| 66 | ) | 67 | ) |
| 67 | .run(|| async { | 68 | .run(|| async { |
| 68 | // Step 1: GENERATE - Create TestContext and get ValidRepo fixture | 69 | // Step 1: GENERATE - Create TestContext and get ValidRepoServed fixture |
| 69 | let ctx = TestContext::new(client); | 70 | let ctx = TestContext::new(client); |
| 70 | let event = ctx | 71 | let event = ctx |
| 71 | .get_fixture(FixtureKind::ValidRepo) | 72 | .get_fixture(FixtureKind::ValidRepoServed) |
| 72 | .await | 73 | .await |
| 73 | .map_err(|e| format!("Failed to create ValidRepo fixture: {}", e))?; | 74 | .map_err(|e| format!("Failed to create ValidRepoServed fixture: {}", e))?; |
| 74 | 75 | ||
| 75 | let event_id = event.id; | 76 | let event_id = event.id; |
| 76 | 77 | ||
| @@ -121,22 +122,22 @@ impl Nip01SmokeTests { | |||
| 121 | /// | 122 | /// |
| 122 | /// ## Fixture-First Pattern | 123 | /// ## Fixture-First Pattern |
| 123 | /// | 124 | /// |
| 124 | /// 1. **Generate**: Create TestContext and get ValidRepo fixture | 125 | /// 1. **Generate**: Create TestContext and get ValidRepoServed fixture |
| 125 | /// 2. **Send**: Fixture already sends the event to relay | 126 | /// 2. **Send**: Fixture already sends the event to relay |
| 126 | /// 3. **Verify**: Subscribe and verify we receive the event | 127 | /// 3. **Verify**: Subscribe and verify we receive the event |
| 127 | pub async fn test_create_subscription(client: &AuditClient) -> TestResult { | 128 | pub async fn test_create_subscription(client: &AuditClient) -> TestResult { |
| 128 | TestResult::new( | 129 | TestResult::new( |
| 129 | "create_subscription", | 130 | "create_subscription", |
| 130 | "GRASP-01:nostr-relay:7", | 131 | SpecRef::NostrRelayNip01Compliant, |
| 131 | "Can create subscription with REQ and receive EOSE", | 132 | "MUST support REQ subscriptions", |
| 132 | ) | 133 | ) |
| 133 | .run(|| async { | 134 | .run(|| async { |
| 134 | // Step 1: GENERATE - Create TestContext and get ValidRepo fixture | 135 | // Step 1: GENERATE - Create TestContext and get ValidRepoServed fixture |
| 135 | let ctx = TestContext::new(client); | 136 | let ctx = TestContext::new(client); |
| 136 | let _event = ctx | 137 | let _event = ctx |
| 137 | .get_fixture(FixtureKind::ValidRepo) | 138 | .get_fixture(FixtureKind::ValidRepoServed) |
| 138 | .await | 139 | .await |
| 139 | .map_err(|e| format!("Failed to create ValidRepo fixture: {}", e))?; | 140 | .map_err(|e| format!("Failed to create ValidRepoServed fixture: {}", e))?; |
| 140 | 141 | ||
| 141 | // Step 2: VERIFY - Subscribe to NIP-34 announcements from this author | 142 | // Step 2: VERIFY - Subscribe to NIP-34 announcements from this author |
| 142 | let filter = Filter::new() | 143 | let filter = Filter::new() |
| @@ -165,8 +166,8 @@ impl Nip01SmokeTests { | |||
| 165 | pub async fn test_close_subscription(client: &AuditClient) -> TestResult { | 166 | pub async fn test_close_subscription(client: &AuditClient) -> TestResult { |
| 166 | TestResult::new( | 167 | TestResult::new( |
| 167 | "close_subscription", | 168 | "close_subscription", |
| 168 | "GRASP-01:nostr-relay:7", | 169 | SpecRef::NostrRelayNip01Compliant, |
| 169 | "Can close subscriptions", | 170 | "MUST support CLOSE to end subscriptions", |
| 170 | ) | 171 | ) |
| 171 | .run(|| async { | 172 | .run(|| async { |
| 172 | // For now, we just verify we can query events | 173 | // For now, we just verify we can query events |
| @@ -193,8 +194,8 @@ impl Nip01SmokeTests { | |||
| 193 | pub async fn test_reject_invalid_signature(client: &AuditClient) -> TestResult { | 194 | pub async fn test_reject_invalid_signature(client: &AuditClient) -> TestResult { |
| 194 | TestResult::new( | 195 | TestResult::new( |
| 195 | "reject_invalid_signature", | 196 | "reject_invalid_signature", |
| 196 | "GRASP-01:nostr-relay:7", | 197 | SpecRef::NostrRelayNip01Compliant, |
| 197 | "Rejects events with invalid signatures", | 198 | "MUST reject events with invalid signatures", |
| 198 | ) | 199 | ) |
| 199 | .run(|| async { | 200 | .run(|| async { |
| 200 | // Create a valid event | 201 | // Create a valid event |
| @@ -247,8 +248,8 @@ impl Nip01SmokeTests { | |||
| 247 | pub async fn test_reject_invalid_event_id(client: &AuditClient) -> TestResult { | 248 | pub async fn test_reject_invalid_event_id(client: &AuditClient) -> TestResult { |
| 248 | TestResult::new( | 249 | TestResult::new( |
| 249 | "reject_invalid_event_id", | 250 | "reject_invalid_event_id", |
| 250 | "GRASP-01:nostr-relay:7", | 251 | SpecRef::NostrRelayNip01Compliant, |
| 251 | "Rejects events with invalid event IDs", | 252 | "MUST reject events where ID doesn't match hash", |
| 252 | ) | 253 | ) |
| 253 | .run(|| async { | 254 | .run(|| async { |
| 254 | // Create a valid event | 255 | // Create a valid event |
diff --git a/grasp-audit/src/specs/grasp01/nip11_document.rs b/grasp-audit/src/specs/grasp01/nip11_document.rs index 19ceace..5bf53bd 100644 --- a/grasp-audit/src/specs/grasp01/nip11_document.rs +++ b/grasp-audit/src/specs/grasp01/nip11_document.rs | |||
| @@ -8,6 +8,7 @@ | |||
| 8 | //! - Includes repo_acceptance_criteria field describing acceptance policy | 8 | //! - Includes repo_acceptance_criteria field describing acceptance policy |
| 9 | //! - Handles curation field correctly (present if curated, absent otherwise) | 9 | //! - Handles curation field correctly (present if curated, absent otherwise) |
| 10 | 10 | ||
| 11 | use crate::specs::grasp01::SpecRef; | ||
| 11 | use crate::{AuditClient, AuditResult, TestResult}; | 12 | use crate::{AuditClient, AuditResult, TestResult}; |
| 12 | 13 | ||
| 13 | pub struct Nip11DocumentTests; | 14 | pub struct Nip11DocumentTests; |
| @@ -37,8 +38,8 @@ impl Nip11DocumentTests { | |||
| 37 | pub async fn test_nip11_document_exists(client: &AuditClient) -> TestResult { | 38 | pub async fn test_nip11_document_exists(client: &AuditClient) -> TestResult { |
| 38 | TestResult::new( | 39 | TestResult::new( |
| 39 | "nip11_document_exists", | 40 | "nip11_document_exists", |
| 40 | "GRASP-01:nostr-relay:26", | 41 | SpecRef::Nip11ServeDocument, |
| 41 | "Serve NIP-11 relay information document", | 42 | "MUST serve NIP-11 document", |
| 42 | ) | 43 | ) |
| 43 | .run(|| async { | 44 | .run(|| async { |
| 44 | // 1. Extract HTTP(S) URL from client's WebSocket URL | 45 | // 1. Extract HTTP(S) URL from client's WebSocket URL |
| @@ -96,8 +97,8 @@ impl Nip11DocumentTests { | |||
| 96 | pub async fn test_nip11_supported_grasps_field(client: &AuditClient) -> TestResult { | 97 | pub async fn test_nip11_supported_grasps_field(client: &AuditClient) -> TestResult { |
| 97 | TestResult::new( | 98 | TestResult::new( |
| 98 | "nip11_supported_grasps_field", | 99 | "nip11_supported_grasps_field", |
| 99 | "GRASP-01:nostr-relay:28", | 100 | SpecRef::Nip11ListSupportedGrasps, |
| 100 | "NIP-11 document includes supported_grasps field with GRASP-01", | 101 | "MUST list supported GRASPs as string array", |
| 101 | ) | 102 | ) |
| 102 | .run(|| async { | 103 | .run(|| async { |
| 103 | // 1. Fetch NIP-11 document | 104 | // 1. Fetch NIP-11 document |
| @@ -172,8 +173,8 @@ impl Nip11DocumentTests { | |||
| 172 | pub async fn test_nip11_repo_acceptance_criteria_field(client: &AuditClient) -> TestResult { | 173 | pub async fn test_nip11_repo_acceptance_criteria_field(client: &AuditClient) -> TestResult { |
| 173 | TestResult::new( | 174 | TestResult::new( |
| 174 | "nip11_repo_acceptance_criteria_field", | 175 | "nip11_repo_acceptance_criteria_field", |
| 175 | "GRASP-01:nostr-relay:29", | 176 | SpecRef::Nip11ListRepoAcceptanceCriteria, |
| 176 | "NIP-11 document includes repo_acceptance_criteria field", | 177 | "MUST list repository acceptance criteria", |
| 177 | ) | 178 | ) |
| 178 | .run(|| async { | 179 | .run(|| async { |
| 179 | // 1. Fetch NIP-11 document | 180 | // 1. Fetch NIP-11 document |
| @@ -227,8 +228,8 @@ impl Nip11DocumentTests { | |||
| 227 | pub async fn test_nip11_curation_field(client: &AuditClient) -> TestResult { | 228 | pub async fn test_nip11_curation_field(client: &AuditClient) -> TestResult { |
| 228 | TestResult::new( | 229 | TestResult::new( |
| 229 | "nip11_curation_field", | 230 | "nip11_curation_field", |
| 230 | "GRASP-01:nostr-relay:30", | 231 | SpecRef::Nip11ListCurationPolicy, |
| 231 | "NIP-11 curation field present if curated, absent otherwise", | 232 | "MUST include curation if curated, omit otherwise", |
| 232 | ) | 233 | ) |
| 233 | .run(|| async { | 234 | .run(|| async { |
| 234 | // 1. Fetch NIP-11 document | 235 | // 1. Fetch NIP-11 document |
diff --git a/grasp-audit/src/specs/grasp01/purgatory.rs b/grasp-audit/src/specs/grasp01/purgatory.rs new file mode 100644 index 0000000..29eabad --- /dev/null +++ b/grasp-audit/src/specs/grasp01/purgatory.rs | |||
| @@ -0,0 +1,983 @@ | |||
| 1 | //! GRASP-01 Purgatory Tests | ||
| 2 | //! | ||
| 3 | //! Tests for the GRASP-01 purgatory mechanism where events are accepted but not | ||
| 4 | //! served until corresponding git data arrives. | ||
| 5 | //! | ||
| 6 | //! ## Purgatory Behavior (GRASP-01 Line 22) | ||
| 7 | //! | ||
| 8 | //! "New repository announcements, repo state announcements, PRs and PR Updates | ||
| 9 | //! SHOULD be accepted with message 'purgatory: won't be served until git data arrives' | ||
| 10 | //! and kept in purgatory (not served) until the related git data arrives and otherwise | ||
| 11 | //! discarded after 30 minutes." | ||
| 12 | //! | ||
| 13 | //! ## Test Categories | ||
| 14 | //! | ||
| 15 | //! ### Announcement Purgatory (feature not yet implemented) | ||
| 16 | //! - `test_announcement_not_served_before_git_data` | ||
| 17 | //! - `test_announcement_served_after_git_push` | ||
| 18 | //! - `test_bare_repo_exists_for_purgatory_announcement` | ||
| 19 | //! - `test_state_event_accepted_for_purgatory_announcement` | ||
| 20 | //! | ||
| 21 | //! ### State Event Purgatory (already implemented) | ||
| 22 | //! - `test_state_event_not_served_before_git_data` | ||
| 23 | //! - `test_state_event_served_after_git_push` | ||
| 24 | //! | ||
| 25 | //! ### PR Purgatory (already implemented) | ||
| 26 | //! - `test_pr_event_accepted_into_purgatory` - Event accepted, not queryable | ||
| 27 | //! - `test_pr_event_in_purgatory_git_push_accepted` - Git push to refs/nostr/<event-id> succeeds | ||
| 28 | //! - `test_pr_event_served_after_git_push` - Event becomes queryable after git data | ||
| 29 | |||
| 30 | use crate::fixtures::{clone_repo, create_commit, try_push}; | ||
| 31 | use crate::specs::grasp01::SpecRef; | ||
| 32 | use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; | ||
| 33 | use nostr_sdk::prelude::*; | ||
| 34 | use std::fs; | ||
| 35 | use std::time::Duration; | ||
| 36 | |||
| 37 | /// Test suite for GRASP-01 purgatory behavior | ||
| 38 | pub struct PurgatoryTests; | ||
| 39 | |||
| 40 | impl PurgatoryTests { | ||
| 41 | /// Run all purgatory tests | ||
| 42 | pub async fn run_all(client: &AuditClient) -> AuditResult { | ||
| 43 | let mut results = AuditResult::new("GRASP-01 Purgatory Tests"); | ||
| 44 | |||
| 45 | // Announcement purgatory tests (feature not yet implemented) | ||
| 46 | results.add(Self::test_announcement_not_served_before_git_data(client).await); | ||
| 47 | results.add(Self::test_announcement_served_after_git_push(client).await); | ||
| 48 | results.add(Self::test_bare_repo_exists_for_purgatory_announcement(client).await); | ||
| 49 | results.add(Self::test_state_event_accepted_for_purgatory_announcement(client).await); | ||
| 50 | |||
| 51 | // Deletion event tests (NIP-09) | ||
| 52 | results.add(Self::test_deletion_by_event_id_removes_purgatory_state_event(client).await); | ||
| 53 | results.add( | ||
| 54 | Self::test_deletion_by_coordinate_removes_purgatory_state_event(client).await, | ||
| 55 | ); | ||
| 56 | |||
| 57 | // State event purgatory tests (already implemented) | ||
| 58 | results.add(Self::test_state_event_not_served_before_git_data(client).await); | ||
| 59 | results.add(Self::test_state_event_served_after_git_push(client).await); | ||
| 60 | |||
| 61 | // PR purgatory tests | ||
| 62 | results.add(Self::test_pr_event_accepted_into_purgatory_and_isnt_served(client).await); | ||
| 63 | results.add(Self::test_pr_event_in_purgatory_git_push_accepted(client).await); | ||
| 64 | results.add(Self::test_pr_event_served_after_git_push(client).await); | ||
| 65 | |||
| 66 | results | ||
| 67 | } | ||
| 68 | |||
| 69 | // ============================================================ | ||
| 70 | // Announcement Purgatory Tests (#[ignore] - feature not yet implemented) | ||
| 71 | // ============================================================ | ||
| 72 | |||
| 73 | /// Test: Repository announcement not served before git data arrives | ||
| 74 | /// | ||
| 75 | /// Spec: GRASP-01 Line 22 | ||
| 76 | /// "New repository announcements... SHOULD be accepted with message | ||
| 77 | /// 'purgatory: won't be served until git data arrives' and kept in purgatory | ||
| 78 | /// (not served) until the related git data arrives" | ||
| 79 | /// | ||
| 80 | /// This test verifies: | ||
| 81 | /// 1. Send a valid repository announcement | ||
| 82 | /// 2. Event is accepted (OK response) | ||
| 83 | /// 3. Event is NOT queryable from the relay (in purgatory) | ||
| 84 | /// | ||
| 85 | /// NOTE: Announcement purgatory feature not yet implemented - test may fail | ||
| 86 | pub async fn test_announcement_not_served_before_git_data(client: &AuditClient) -> TestResult { | ||
| 87 | TestResult::new( | ||
| 88 | "announcement_not_served_before_git_data", | ||
| 89 | SpecRef::PurgatoryAcceptUntilGitData, | ||
| 90 | "Repository announcements SHOULD be accepted but not served until git data arrives", | ||
| 91 | ) | ||
| 92 | .run(|| async { | ||
| 93 | let ctx = TestContext::new(client); | ||
| 94 | |||
| 95 | // Create a fresh repo announcement (not the served variant) | ||
| 96 | let repo = ctx | ||
| 97 | .get_fixture(FixtureKind::ValidRepoSent) | ||
| 98 | .await | ||
| 99 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; | ||
| 100 | |||
| 101 | let repo_id = repo | ||
| 102 | .tags | ||
| 103 | .iter() | ||
| 104 | .find(|t| t.kind() == TagKind::d()) | ||
| 105 | .and_then(|t| t.content()) | ||
| 106 | .ok_or("Missing d tag in repo announcement")? | ||
| 107 | .to_string(); | ||
| 108 | |||
| 109 | // Query for the announcement - should NOT be served | ||
| 110 | let filter = Filter::new() | ||
| 111 | .kind(Kind::GitRepoAnnouncement) | ||
| 112 | .author(client.public_key()) | ||
| 113 | .identifier(&repo_id); | ||
| 114 | |||
| 115 | tokio::time::sleep(Duration::from_millis(300)).await; | ||
| 116 | |||
| 117 | let events = client | ||
| 118 | .query(filter) | ||
| 119 | .await | ||
| 120 | .map_err(|e| format!("Failed to query relay: {}", e))?; | ||
| 121 | |||
| 122 | if events.iter().any(|e| e.id == repo.id) { | ||
| 123 | return Err(format!( | ||
| 124 | "Announcement was served immediately - purgatory not implemented. \ | ||
| 125 | Event ID: {} should NOT be queryable until git data arrives", | ||
| 126 | repo.id | ||
| 127 | )); | ||
| 128 | } | ||
| 129 | |||
| 130 | Ok(()) | ||
| 131 | }) | ||
| 132 | .await | ||
| 133 | } | ||
| 134 | |||
| 135 | /// Test: Repository announcement served after git push | ||
| 136 | /// | ||
| 137 | /// Spec: GRASP-01 Line 22 | ||
| 138 | /// "...kept in purgatory (not served) until the related git data arrives" | ||
| 139 | /// | ||
| 140 | /// This test verifies the full lifecycle: | ||
| 141 | /// 1. Send repository announcement (enters purgatory) | ||
| 142 | /// 2. Send state event (enters purgatory) | ||
| 143 | /// 3. Push git data matching state event | ||
| 144 | /// 4. Both announcement and state event are now served | ||
| 145 | /// | ||
| 146 | /// NOTE: Announcement purgatory feature not yet implemented - test may fail | ||
| 147 | pub async fn test_announcement_served_after_git_push(client: &AuditClient) -> TestResult { | ||
| 148 | TestResult::new( | ||
| 149 | "announcement_served_after_git_push", | ||
| 150 | SpecRef::PurgatoryAcceptUntilGitData, | ||
| 151 | "Repository announcements SHOULD be served after git data arrives", | ||
| 152 | ) | ||
| 153 | .run(|| async { | ||
| 154 | let ctx = TestContext::new(client); | ||
| 155 | |||
| 156 | // OwnerStateDataPushed fixture handles the full lifecycle: | ||
| 157 | // 1. Creates repo announcement (purgatory) | ||
| 158 | // 2. Creates state event (purgatory) | ||
| 159 | // 3. Pushes git data | ||
| 160 | // 4. Verifies events are served | ||
| 161 | let state_event = ctx | ||
| 162 | .get_fixture(FixtureKind::OwnerStateDataPushed) | ||
| 163 | .await | ||
| 164 | .map_err(|e| format!("Failed to complete full lifecycle: {}", e))?; | ||
| 165 | |||
| 166 | // Extract repo_id from state event | ||
| 167 | let repo_id = state_event | ||
| 168 | .tags | ||
| 169 | .iter() | ||
| 170 | .find(|t| t.kind() == TagKind::d()) | ||
| 171 | .and_then(|t| t.content()) | ||
| 172 | .ok_or("Missing d tag in state event")? | ||
| 173 | .to_string(); | ||
| 174 | |||
| 175 | // Verify announcement is now served | ||
| 176 | let announcement_filter = Filter::new() | ||
| 177 | .kind(Kind::GitRepoAnnouncement) | ||
| 178 | .author(client.public_key()) | ||
| 179 | .identifier(&repo_id); | ||
| 180 | |||
| 181 | let announcements = client | ||
| 182 | .query(announcement_filter) | ||
| 183 | .await | ||
| 184 | .map_err(|e| format!("Failed to query announcements: {}", e))?; | ||
| 185 | |||
| 186 | if announcements.is_empty() { | ||
| 187 | return Err(format!( | ||
| 188 | "Announcement not served after git push. Repo ID: {}", | ||
| 189 | repo_id | ||
| 190 | )); | ||
| 191 | } | ||
| 192 | |||
| 193 | // Verify state event is served | ||
| 194 | let state_filter = Filter::new() | ||
| 195 | .kind(Kind::RepoState) | ||
| 196 | .author(client.public_key()) | ||
| 197 | .identifier(&repo_id); | ||
| 198 | |||
| 199 | let state_events = client | ||
| 200 | .query(state_filter) | ||
| 201 | .await | ||
| 202 | .map_err(|e| format!("Failed to query state events: {}", e))?; | ||
| 203 | |||
| 204 | if !state_events.iter().any(|e| e.id == state_event.id) { | ||
| 205 | return Err(format!( | ||
| 206 | "State event not served after git push. Event ID: {}", | ||
| 207 | state_event.id | ||
| 208 | )); | ||
| 209 | } | ||
| 210 | |||
| 211 | Ok(()) | ||
| 212 | }) | ||
| 213 | .await | ||
| 214 | } | ||
| 215 | |||
| 216 | /// Test: Bare repository exists for purgatory announcement | ||
| 217 | /// | ||
| 218 | /// Spec: GRASP-01 Line 34 | ||
| 219 | /// "MUST serve a git repository via an unauthenticated git smart http service | ||
| 220 | /// at `/<npub>/<identifier>.git` for each git repository announcement the relay | ||
| 221 | /// serves or has in purgatory." | ||
| 222 | /// | ||
| 223 | /// This test verifies that git HTTP service works even for repos in purgatory. | ||
| 224 | /// | ||
| 225 | /// NOTE: Announcement purgatory feature not yet implemented - test may fail | ||
| 226 | pub async fn test_bare_repo_exists_for_purgatory_announcement( | ||
| 227 | client: &AuditClient, | ||
| 228 | ) -> TestResult { | ||
| 229 | TestResult::new( | ||
| 230 | "bare_repo_exists_for_purgatory_announcement", | ||
| 231 | SpecRef::GitServeRepository, | ||
| 232 | "Git HTTP service MUST work for repos in purgatory", | ||
| 233 | ) | ||
| 234 | .run(|| async { | ||
| 235 | let ctx = TestContext::new(client); | ||
| 236 | |||
| 237 | // Get a repo announcement (in purgatory, no git data yet) | ||
| 238 | let repo = ctx | ||
| 239 | .get_fixture(FixtureKind::ValidRepoSent) | ||
| 240 | .await | ||
| 241 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; | ||
| 242 | |||
| 243 | let repo_id = repo | ||
| 244 | .tags | ||
| 245 | .iter() | ||
| 246 | .find(|t| t.kind() == TagKind::d()) | ||
| 247 | .and_then(|t| t.content()) | ||
| 248 | .ok_or("Missing d tag in repo announcement")? | ||
| 249 | .to_string(); | ||
| 250 | |||
| 251 | let npub = client | ||
| 252 | .public_key() | ||
| 253 | .to_bech32() | ||
| 254 | .map_err(|e| format!("Failed to convert pubkey: {}", e))?; | ||
| 255 | |||
| 256 | // Get relay domain | ||
| 257 | let relay_url = client | ||
| 258 | .client() | ||
| 259 | .relays() | ||
| 260 | .await | ||
| 261 | .keys() | ||
| 262 | .next() | ||
| 263 | .ok_or("No relay connected")? | ||
| 264 | .to_string(); | ||
| 265 | let relay_domain = relay_url | ||
| 266 | .replace("ws://", "") | ||
| 267 | .replace("wss://", "") | ||
| 268 | .replace(":8080", ""); | ||
| 269 | |||
| 270 | // Check git HTTP service is available | ||
| 271 | let info_refs_url = format!( | ||
| 272 | "http://{}/{}/{}.git/info/refs?service=git-upload-pack", | ||
| 273 | relay_domain, npub, repo_id | ||
| 274 | ); | ||
| 275 | |||
| 276 | let http_client = reqwest::Client::new(); | ||
| 277 | let response = http_client | ||
| 278 | .get(&info_refs_url) | ||
| 279 | .send() | ||
| 280 | .await | ||
| 281 | .map_err(|e| format!("HTTP request failed: {}", e))?; | ||
| 282 | |||
| 283 | if !response.status().is_success() { | ||
| 284 | return Err(format!( | ||
| 285 | "Git HTTP service not available for purgatory repo. \ | ||
| 286 | URL: {}, Status: {}", | ||
| 287 | info_refs_url, | ||
| 288 | response.status() | ||
| 289 | )); | ||
| 290 | } | ||
| 291 | |||
| 292 | Ok(()) | ||
| 293 | }) | ||
| 294 | .await | ||
| 295 | } | ||
| 296 | |||
| 297 | /// Test: State event accepted for purgatory announcement | ||
| 298 | /// | ||
| 299 | /// Spec: GRASP-01 Line 22 | ||
| 300 | /// "New repository announcements, repo state announcements... SHOULD be accepted" | ||
| 301 | /// | ||
| 302 | /// This test verifies that state events are accepted even when the repo | ||
| 303 | /// announcement is in purgatory (no git data yet). | ||
| 304 | /// | ||
| 305 | /// NOTE: Announcement purgatory feature not yet implemented - test may fail | ||
| 306 | pub async fn test_state_event_accepted_for_purgatory_announcement( | ||
| 307 | client: &AuditClient, | ||
| 308 | ) -> TestResult { | ||
| 309 | TestResult::new( | ||
| 310 | "state_event_accepted_for_purgatory_announcement", | ||
| 311 | SpecRef::PurgatoryAcceptUntilGitData, | ||
| 312 | "State events SHOULD be accepted for repos in purgatory", | ||
| 313 | ) | ||
| 314 | .run(|| async { | ||
| 315 | let ctx = TestContext::new(client); | ||
| 316 | |||
| 317 | // Get a repo announcement (in purgatory) | ||
| 318 | let repo = ctx | ||
| 319 | .get_fixture(FixtureKind::ValidRepoSent) | ||
| 320 | .await | ||
| 321 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; | ||
| 322 | |||
| 323 | // Build a state event for this repo | ||
| 324 | let repo_id = repo | ||
| 325 | .tags | ||
| 326 | .iter() | ||
| 327 | .find(|t| t.kind() == TagKind::d()) | ||
| 328 | .and_then(|t| t.content()) | ||
| 329 | .ok_or("Missing d tag in repo announcement")? | ||
| 330 | .to_string(); | ||
| 331 | |||
| 332 | let state_event = client | ||
| 333 | .event_builder(Kind::RepoState, "") | ||
| 334 | .tag(Tag::identifier(&repo_id)) | ||
| 335 | .tag(Tag::custom( | ||
| 336 | TagKind::custom("refs/heads/main"), | ||
| 337 | vec!["abc123".to_string()], | ||
| 338 | )) | ||
| 339 | .tag(Tag::custom( | ||
| 340 | TagKind::custom("HEAD"), | ||
| 341 | vec!["ref: refs/heads/main".to_string()], | ||
| 342 | )) | ||
| 343 | .build(client.keys()) | ||
| 344 | .map_err(|e| format!("Failed to build state event: {}", e))?; | ||
| 345 | |||
| 346 | // Send state event - should be accepted (even though repo is in purgatory) | ||
| 347 | let (_, in_purgatory) = client | ||
| 348 | .send_event_and_note_purgatory(state_event.clone()) | ||
| 349 | .await | ||
| 350 | .map_err(|e| format!("Failed to send state event: {}", e))?; | ||
| 351 | |||
| 352 | // Event should be accepted (either in purgatory or served) | ||
| 353 | // We just verify it wasn't rejected | ||
| 354 | if !in_purgatory { | ||
| 355 | // Check if it's actually on the relay (might be served immediately) | ||
| 356 | let filter = Filter::new() | ||
| 357 | .kind(Kind::RepoState) | ||
| 358 | .author(client.public_key()) | ||
| 359 | .identifier(&repo_id); | ||
| 360 | |||
| 361 | let events = client | ||
| 362 | .query(filter) | ||
| 363 | .await | ||
| 364 | .map_err(|e| format!("Failed to query: {}", e))?; | ||
| 365 | |||
| 366 | if events.iter().any(|e| e.id == state_event.id) { | ||
| 367 | return Err(format!( | ||
| 368 | "State event was served immediately - repo announcement purgatory not implemented. \ | ||
| 369 | Event ID: {} should NOT be queryable until git data arrives", | ||
| 370 | state_event.id | ||
| 371 | )); | ||
| 372 | } | ||
| 373 | |||
| 374 | return Err(format!( | ||
| 375 | "State event was neither in purgatory nor served. \ | ||
| 376 | Event ID: {}", | ||
| 377 | state_event.id | ||
| 378 | )); | ||
| 379 | } | ||
| 380 | |||
| 381 | // Feature IS implemented - state event in purgatory as expected | ||
| 382 | Ok(()) | ||
| 383 | }) | ||
| 384 | .await | ||
| 385 | } | ||
| 386 | |||
| 387 | // ============================================================ | ||
| 388 | // State Event Purgatory Tests (non-ignored - already implemented) | ||
| 389 | // ============================================================ | ||
| 390 | |||
| 391 | /// Test: State event not served before git data arrives | ||
| 392 | /// | ||
| 393 | /// Spec: GRASP-01 Line 22 | ||
| 394 | /// "repo state announcements... SHOULD be accepted with message | ||
| 395 | /// 'purgatory: won't be served until git data arrives'" | ||
| 396 | /// | ||
| 397 | /// This test verifies: | ||
| 398 | /// 1. Send state event for a repo with git data | ||
| 399 | /// 2. State event points to a different commit than what's pushed | ||
| 400 | /// 3. State event is NOT queryable (in purgatory) | ||
| 401 | pub async fn test_state_event_not_served_before_git_data(client: &AuditClient) -> TestResult { | ||
| 402 | TestResult::new( | ||
| 403 | "state_event_not_served_before_git_data", | ||
| 404 | SpecRef::PurgatoryAcceptUntilGitData, | ||
| 405 | "State events SHOULD be accepted but not served until git data arrives", | ||
| 406 | ) | ||
| 407 | .run(|| async { | ||
| 408 | let ctx = TestContext::new(client); | ||
| 409 | |||
| 410 | // Get a repo with git data already pushed | ||
| 411 | let existing_state = ctx | ||
| 412 | .get_fixture(FixtureKind::OwnerStateDataPushed) | ||
| 413 | .await | ||
| 414 | .map_err(|e| format!("Failed to get existing repo: {}", e))?; | ||
| 415 | |||
| 416 | let repo_id = existing_state | ||
| 417 | .tags | ||
| 418 | .iter() | ||
| 419 | .find(|t| t.kind() == TagKind::d()) | ||
| 420 | .and_then(|t| t.content()) | ||
| 421 | .ok_or("Missing d tag in state event")? | ||
| 422 | .to_string(); | ||
| 423 | |||
| 424 | // Create a NEW state event pointing to a DIFFERENT commit | ||
| 425 | // This should enter purgatory since the commit doesn't exist | ||
| 426 | let new_state = client | ||
| 427 | .event_builder(Kind::RepoState, "") | ||
| 428 | .tag(Tag::identifier(&repo_id)) | ||
| 429 | .tag(Tag::custom( | ||
| 430 | TagKind::custom("refs/heads/main"), | ||
| 431 | vec!["deadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_string()], | ||
| 432 | )) | ||
| 433 | .tag(Tag::custom( | ||
| 434 | TagKind::custom("HEAD"), | ||
| 435 | vec!["ref: refs/heads/main".to_string()], | ||
| 436 | )) | ||
| 437 | .build(client.keys()) | ||
| 438 | .map_err(|e| format!("Failed to build state event: {}", e))?; | ||
| 439 | |||
| 440 | // Send the state event | ||
| 441 | let (_, in_purgatory) = client | ||
| 442 | .send_event_and_note_purgatory(new_state.clone()) | ||
| 443 | .await | ||
| 444 | .map_err(|e| format!("Failed to send state event: {}", e))?; | ||
| 445 | |||
| 446 | if !in_purgatory { | ||
| 447 | return Err(format!( | ||
| 448 | "State event was served immediately despite pointing to \ | ||
| 449 | non-existent commit. Event ID: {}", | ||
| 450 | new_state.id | ||
| 451 | )); | ||
| 452 | } | ||
| 453 | |||
| 454 | Ok(()) | ||
| 455 | }) | ||
| 456 | .await | ||
| 457 | } | ||
| 458 | |||
| 459 | /// Test: State event served after git push | ||
| 460 | /// | ||
| 461 | /// Spec: GRASP-01 Line 22 | ||
| 462 | /// "...kept in purgatory (not served) until the related git data arrives" | ||
| 463 | /// | ||
| 464 | /// This test verifies the full lifecycle using OwnerStateDataPushed fixture: | ||
| 465 | /// 1. State event is sent (enters purgatory) | ||
| 466 | /// 2. Git data is pushed matching the state event | ||
| 467 | /// 3. State event is now served | ||
| 468 | pub async fn test_state_event_served_after_git_push(client: &AuditClient) -> TestResult { | ||
| 469 | TestResult::new( | ||
| 470 | "state_event_served_after_git_push", | ||
| 471 | SpecRef::PurgatoryAcceptUntilGitData, | ||
| 472 | "State events SHOULD be served after matching git data arrives", | ||
| 473 | ) | ||
| 474 | .run(|| async { | ||
| 475 | let ctx = TestContext::new(client); | ||
| 476 | |||
| 477 | // OwnerStateDataPushed handles the full lifecycle | ||
| 478 | let state_event = ctx | ||
| 479 | .get_fixture(FixtureKind::OwnerStateDataPushed) | ||
| 480 | .await | ||
| 481 | .map_err(|e| format!("Failed to complete full lifecycle: {}", e))?; | ||
| 482 | |||
| 483 | // Verify state event is now served | ||
| 484 | let repo_id = state_event | ||
| 485 | .tags | ||
| 486 | .iter() | ||
| 487 | .find(|t| t.kind() == TagKind::d()) | ||
| 488 | .and_then(|t| t.content()) | ||
| 489 | .ok_or("Missing d tag in state event")? | ||
| 490 | .to_string(); | ||
| 491 | |||
| 492 | let filter = Filter::new() | ||
| 493 | .kind(Kind::RepoState) | ||
| 494 | .author(client.public_key()) | ||
| 495 | .identifier(&repo_id); | ||
| 496 | |||
| 497 | let events = client | ||
| 498 | .query(filter) | ||
| 499 | .await | ||
| 500 | .map_err(|e| format!("Failed to query state events: {}", e))?; | ||
| 501 | |||
| 502 | if !events.iter().any(|e| e.id == state_event.id) { | ||
| 503 | return Err(format!( | ||
| 504 | "State event not served after git push. Event ID: {}", | ||
| 505 | state_event.id | ||
| 506 | )); | ||
| 507 | } | ||
| 508 | |||
| 509 | Ok(()) | ||
| 510 | }) | ||
| 511 | .await | ||
| 512 | } | ||
| 513 | |||
| 514 | // ============================================================ | ||
| 515 | // PR Purgatory Tests | ||
| 516 | // ============================================================ | ||
| 517 | |||
| 518 | /// Test: PR event accepted into purgatory (not served before git data) | ||
| 519 | /// | ||
| 520 | /// Spec: GRASP-01 Line 22 | ||
| 521 | /// "PRs and PR Updates SHOULD be accepted with message | ||
| 522 | /// 'purgatory: won't be served until git data arrives'" | ||
| 523 | /// | ||
| 524 | /// This test verifies: | ||
| 525 | /// 1. PR event is sent and relay responds OK (accepted) | ||
| 526 | /// 2. PR event is NOT queryable (in purgatory, not served) | ||
| 527 | /// | ||
| 528 | /// PASS means: Relay accepted the event and is holding it in purgatory | ||
| 529 | /// FAIL means: Either event was rejected, or served immediately (purgatory not implemented) | ||
| 530 | /// | ||
| 531 | /// Note: This test cannot distinguish between "event in purgatory" and | ||
| 532 | /// "event accepted but never stored" - both result in event not being queryable. | ||
| 533 | /// The fixture verifies the relay responded OK, which is the best we can do | ||
| 534 | /// with black-box testing. | ||
| 535 | pub async fn test_pr_event_accepted_into_purgatory_and_isnt_served( | ||
| 536 | client: &AuditClient, | ||
| 537 | ) -> TestResult { | ||
| 538 | TestResult::new( | ||
| 539 | "pr_event_accepted_into_purgatory", | ||
| 540 | SpecRef::PurgatoryAcceptUntilGitData, | ||
| 541 | "PR event SHOULD be accepted but not served until git data arrives", | ||
| 542 | ) | ||
| 543 | .run(|| async { | ||
| 544 | let ctx = TestContext::new(client); | ||
| 545 | |||
| 546 | // PREvent2Sent fixture: | ||
| 547 | // 1. Sends PR event | ||
| 548 | // 2. Verifies relay responded OK (not rejected) | ||
| 549 | // 3. Verifies event is NOT queryable (in purgatory) | ||
| 550 | let pr_event = ctx | ||
| 551 | .get_fixture(FixtureKind::PREvent2Sent) | ||
| 552 | .await | ||
| 553 | .map_err(|e| format!("Failed to send PR event: {}", e))?; | ||
| 554 | |||
| 555 | // Double-check: event should not be queryable | ||
| 556 | let filter = Filter::new() | ||
| 557 | .kind(Kind::GitPullRequest) | ||
| 558 | .author(client.pr_author_keys().public_key()) | ||
| 559 | .id(pr_event.id); | ||
| 560 | |||
| 561 | tokio::time::sleep(Duration::from_millis(300)).await; | ||
| 562 | |||
| 563 | let events = client | ||
| 564 | .query(filter) | ||
| 565 | .await | ||
| 566 | .map_err(|e| format!("Failed to query PR events: {}", e))?; | ||
| 567 | |||
| 568 | if !events.is_empty() { | ||
| 569 | return Err(format!( | ||
| 570 | "PR event was served immediately - purgatory not implemented. Event ID: {}", | ||
| 571 | pr_event.id | ||
| 572 | )); | ||
| 573 | } | ||
| 574 | |||
| 575 | Ok(()) | ||
| 576 | }) | ||
| 577 | .await | ||
| 578 | } | ||
| 579 | |||
| 580 | /// Test: Git push to refs/nostr/<pr-event-id> is accepted | ||
| 581 | /// | ||
| 582 | /// This test verifies that pushing git data for a PR event in purgatory | ||
| 583 | /// is accepted by the relay. | ||
| 584 | /// | ||
| 585 | /// PASS means: Git push succeeded, relay accepted the git data | ||
| 586 | /// FAIL means: Git push was rejected (wrong ref, permissions, etc.) | ||
| 587 | pub async fn test_pr_event_in_purgatory_git_push_accepted(client: &AuditClient) -> TestResult { | ||
| 588 | TestResult::new( | ||
| 589 | "pr_event_in_purgatory_git_push_accepted", | ||
| 590 | SpecRef::PurgatoryAcceptUntilGitData, | ||
| 591 | "Git push for PR event SHOULD be accepted", | ||
| 592 | ) | ||
| 593 | .run(|| async { | ||
| 594 | let ctx = TestContext::new(client); | ||
| 595 | |||
| 596 | // PREvent2GitDataPushed fixture: | ||
| 597 | // 1. Gets PR event in purgatory (PREvent2Sent) | ||
| 598 | // 2. Pushes commit to refs/nostr/<pr-event-id> | ||
| 599 | // 3. Verifies push succeeded | ||
| 600 | let _pr_event = ctx | ||
| 601 | .get_fixture(FixtureKind::PREvent2GitDataPushed) | ||
| 602 | .await | ||
| 603 | .map_err(|e| format!("Failed to push git data for PR event: {}", e))?; | ||
| 604 | |||
| 605 | Ok(()) | ||
| 606 | }) | ||
| 607 | .await | ||
| 608 | } | ||
| 609 | |||
| 610 | /// Test: PR event served after git data arrives | ||
| 611 | /// | ||
| 612 | /// This test verifies the full purgatory release mechanism: | ||
| 613 | /// after git data is pushed to refs/nostr/<pr-event-id>, the event | ||
| 614 | /// becomes queryable. | ||
| 615 | /// | ||
| 616 | /// PASS means: Event was released from purgatory and is now served | ||
| 617 | /// FAIL means: Event still not queryable after git push (purgatory release broken) | ||
| 618 | pub async fn test_pr_event_served_after_git_push(client: &AuditClient) -> TestResult { | ||
| 619 | TestResult::new( | ||
| 620 | "pr_event_served_after_git_push", | ||
| 621 | SpecRef::PurgatoryAcceptUntilGitData, | ||
| 622 | "PR event SHOULD be served after matching git data arrives", | ||
| 623 | ) | ||
| 624 | .run(|| async { | ||
| 625 | let ctx = TestContext::new(client); | ||
| 626 | |||
| 627 | // PREvent2Served fixture: | ||
| 628 | // 1. Gets PR event with git data pushed (PREvent2GitDataPushed) | ||
| 629 | // 2. Verifies event is now queryable | ||
| 630 | let pr_event = ctx | ||
| 631 | .get_fixture(FixtureKind::PREvent2Served) | ||
| 632 | .await | ||
| 633 | .map_err(|e| format!("Failed to complete purgatory release: {}", e))?; | ||
| 634 | |||
| 635 | // Double-check: event should be queryable now | ||
| 636 | let filter = Filter::new() | ||
| 637 | .kind(Kind::GitPullRequest) | ||
| 638 | .author(client.pr_author_keys().public_key()) | ||
| 639 | .id(pr_event.id); | ||
| 640 | |||
| 641 | let events = client | ||
| 642 | .query(filter) | ||
| 643 | .await | ||
| 644 | .map_err(|e| format!("Failed to query PR events: {}", e))?; | ||
| 645 | |||
| 646 | if events.is_empty() { | ||
| 647 | return Err(format!( | ||
| 648 | "PR event not served after git push. Event ID: {} should be queryable", | ||
| 649 | pr_event.id | ||
| 650 | )); | ||
| 651 | } | ||
| 652 | |||
| 653 | Ok(()) | ||
| 654 | }) | ||
| 655 | .await | ||
| 656 | } | ||
| 657 | // ============================================================ | ||
| 658 | // Deletion Event Tests (NIP-09) | ||
| 659 | // ============================================================ | ||
| 660 | |||
| 661 | /// Test: Kind 5 deletion event by event ID removes a purgatory state event | ||
| 662 | /// | ||
| 663 | /// Spec: NIP-09 | ||
| 664 | /// "A special event with kind 5... having a list of one or more `e` or `a` tags, | ||
| 665 | /// each referencing an event the author is requesting to be deleted." | ||
| 666 | /// | ||
| 667 | /// This test verifies: | ||
| 668 | /// 1. Get a promoted repo (OwnerStateDataPushed) so git pushes are possible | ||
| 669 | /// 2. Clone the repo and create a unique commit (not yet pushed) | ||
| 670 | /// 3. Submit a state event pointing to that unique commit (enters purgatory) | ||
| 671 | /// 4. Send a kind 5 deletion event referencing the state event by event ID | ||
| 672 | /// 5. Attempt to push the unique commit — MUST be rejected (no authorized state event) | ||
| 673 | pub async fn test_deletion_by_event_id_removes_purgatory_state_event( | ||
| 674 | client: &AuditClient, | ||
| 675 | ) -> TestResult { | ||
| 676 | TestResult::new( | ||
| 677 | "deletion_by_event_id_removes_purgatory_state_event", | ||
| 678 | SpecRef::PurgatoryAcceptUntilGitData, | ||
| 679 | "Kind 5 deletion by event ID SHOULD remove a purgatory state event, causing push rejection", | ||
| 680 | ) | ||
| 681 | .run(|| async { | ||
| 682 | let ctx = TestContext::new(client); | ||
| 683 | |||
| 684 | // Stage 1: get a promoted repo with git data already on the relay | ||
| 685 | let existing_state = ctx | ||
| 686 | .get_fixture(FixtureKind::OwnerStateDataPushed) | ||
| 687 | .await | ||
| 688 | .map_err(|e| format!("Failed to get promoted repo: {}", e))?; | ||
| 689 | |||
| 690 | let repo_id = existing_state | ||
| 691 | .tags | ||
| 692 | .iter() | ||
| 693 | .find(|t| t.kind() == TagKind::d()) | ||
| 694 | .and_then(|t| t.content()) | ||
| 695 | .ok_or("Missing d tag in state event")? | ||
| 696 | .to_string(); | ||
| 697 | |||
| 698 | let relay_domain = client | ||
| 699 | .relay_url() | ||
| 700 | .await | ||
| 701 | .map_err(|e| e.to_string())? | ||
| 702 | .trim_start_matches("ws://") | ||
| 703 | .trim_start_matches("wss://") | ||
| 704 | .to_string(); | ||
| 705 | |||
| 706 | let npub = client | ||
| 707 | .public_key() | ||
| 708 | .to_bech32() | ||
| 709 | .map_err(|e| e.to_string())?; | ||
| 710 | |||
| 711 | // Stage 2: clone the repo and create a unique commit (not pushed yet) | ||
| 712 | let clone_path = clone_repo(&relay_domain, &npub, &repo_id) | ||
| 713 | .map_err(|e| format!("Failed to clone repo: {}", e))?; | ||
| 714 | |||
| 715 | let cleanup = || { let _ = fs::remove_dir_all(&clone_path); }; | ||
| 716 | |||
| 717 | let unique_commit = match create_commit(&clone_path, "deletion test unique commit") { | ||
| 718 | Ok(h) => h, | ||
| 719 | Err(e) => { cleanup(); return Err(format!("Failed to create commit: {}", e)); } | ||
| 720 | }; | ||
| 721 | |||
| 722 | // Stage 3: submit a state event pointing to the unique commit (enters purgatory) | ||
| 723 | let state_event = client | ||
| 724 | .event_builder(Kind::RepoState, "") | ||
| 725 | .tag(Tag::identifier(&repo_id)) | ||
| 726 | .tag(Tag::custom( | ||
| 727 | TagKind::custom("refs/heads/main"), | ||
| 728 | vec![unique_commit.clone()], | ||
| 729 | )) | ||
| 730 | .tag(Tag::custom( | ||
| 731 | TagKind::custom("HEAD"), | ||
| 732 | vec!["ref: refs/heads/main".to_string()], | ||
| 733 | )) | ||
| 734 | .build(client.keys()) | ||
| 735 | .map_err(|e| { cleanup(); format!("Failed to build state event: {}", e) })?; | ||
| 736 | |||
| 737 | let (_, in_purgatory) = client | ||
| 738 | .send_event_and_note_purgatory(state_event.clone()) | ||
| 739 | .await | ||
| 740 | .map_err(|e| { cleanup(); format!("Failed to send state event: {}", e) })?; | ||
| 741 | |||
| 742 | if !in_purgatory { | ||
| 743 | cleanup(); | ||
| 744 | return Err(format!( | ||
| 745 | "State event was served immediately (not in purgatory). \ | ||
| 746 | Commit {} may already exist on relay.", | ||
| 747 | unique_commit | ||
| 748 | )); | ||
| 749 | } | ||
| 750 | |||
| 751 | // Stage 4: send kind 5 deletion event referencing the state event by event ID | ||
| 752 | let deletion = client | ||
| 753 | .event_builder(Kind::EventDeletion, "") | ||
| 754 | .tag(Tag::event(state_event.id)) | ||
| 755 | .tag(Tag::custom(TagKind::custom("k"), vec!["30618"])) | ||
| 756 | .build(client.keys()) | ||
| 757 | .map_err(|e| { cleanup(); format!("Failed to build deletion event: {}", e) })?; | ||
| 758 | |||
| 759 | client | ||
| 760 | .send_event(deletion) | ||
| 761 | .await | ||
| 762 | .map_err(|e| { cleanup(); format!("Relay rejected deletion event: {}", e) })?; | ||
| 763 | |||
| 764 | tokio::time::sleep(Duration::from_millis(300)).await; | ||
| 765 | |||
| 766 | // Stage 5: attempt to push the unique commit — must be rejected | ||
| 767 | let push_result = try_push(&clone_path); | ||
| 768 | cleanup(); | ||
| 769 | |||
| 770 | match push_result { | ||
| 771 | Ok(false) => Ok(()), // push rejected as expected | ||
| 772 | Ok(true) => Err(format!( | ||
| 773 | "Push was accepted but should have been rejected. \ | ||
| 774 | The state event (id={}) was deleted, so commit {} \ | ||
| 775 | should not be authorized.", | ||
| 776 | state_event.id, unique_commit | ||
| 777 | )), | ||
| 778 | Err(e) => Err(format!("Git push error: {}", e)), | ||
| 779 | } | ||
| 780 | }) | ||
| 781 | .await | ||
| 782 | } | ||
| 783 | |||
| 784 | /// Test: Kind 5 deletion event by `a` tag coordinate removes a purgatory state event | ||
| 785 | /// | ||
| 786 | /// Spec: NIP-09 | ||
| 787 | /// "When an `a` tag is used, relays SHOULD delete all versions of the replaceable | ||
| 788 | /// event up to the `created_at` timestamp of the deletion request event." | ||
| 789 | /// | ||
| 790 | /// This test verifies: | ||
| 791 | /// 1. Get a promoted repo (OwnerStateDataPushed) so git pushes are possible | ||
| 792 | /// 2. Generate a fresh keypair for a new maintainer | ||
| 793 | /// 3. Send a replacement owner announcement adding the new maintainer (goes to DB) | ||
| 794 | /// 4. Send a state event signed by the new maintainer pointing to a unique commit | ||
| 795 | /// (enters purgatory — maintainer is authorized but commit doesn't exist yet) | ||
| 796 | /// 5. Delete by coordinate `30618:<new_maintainer_pubkey>:<identifier>` | ||
| 797 | /// 6. Clone repo, create that unique commit, attempt to push — MUST be rejected | ||
| 798 | /// (the state event was deleted, so the commit is no longer authorized) | ||
| 799 | pub async fn test_deletion_by_coordinate_removes_purgatory_state_event( | ||
| 800 | client: &AuditClient, | ||
| 801 | ) -> TestResult { | ||
| 802 | TestResult::new( | ||
| 803 | "deletion_by_coordinate_removes_purgatory_state_event", | ||
| 804 | SpecRef::PurgatoryAcceptUntilGitData, | ||
| 805 | "Kind 5 deletion by `a` coordinate SHOULD remove a purgatory state event, causing push rejection", | ||
| 806 | ) | ||
| 807 | .run(|| async { | ||
| 808 | let ctx = TestContext::new(client); | ||
| 809 | |||
| 810 | // Stage 1: get a promoted repo with git data already on the relay | ||
| 811 | let existing_state = ctx | ||
| 812 | .get_fixture(FixtureKind::OwnerStateDataPushed) | ||
| 813 | .await | ||
| 814 | .map_err(|e| format!("Failed to get promoted repo: {}", e))?; | ||
| 815 | |||
| 816 | let repo_id = existing_state | ||
| 817 | .tags | ||
| 818 | .iter() | ||
| 819 | .find(|t| t.kind() == TagKind::d()) | ||
| 820 | .and_then(|t| t.content()) | ||
| 821 | .ok_or("Missing d tag in state event")? | ||
| 822 | .to_string(); | ||
| 823 | |||
| 824 | // Stage 2: generate a fresh keypair for a new maintainer | ||
| 825 | let new_maintainer_keys = Keys::generate(); | ||
| 826 | let new_maintainer_hex = new_maintainer_keys.public_key().to_hex(); | ||
| 827 | |||
| 828 | // Stage 3: send a replacement owner announcement that adds the new maintainer. | ||
| 829 | // This is a replacement (same pubkey + identifier already in DB) so it goes | ||
| 830 | // straight to the database without entering purgatory. | ||
| 831 | let relay_url = client | ||
| 832 | .relay_url() | ||
| 833 | .await | ||
| 834 | .map_err(|e| e.to_string())?; | ||
| 835 | let http_url = relay_url | ||
| 836 | .replace("ws://", "http://") | ||
| 837 | .replace("wss://", "https://"); | ||
| 838 | let npub = client | ||
| 839 | .public_key() | ||
| 840 | .to_bech32() | ||
| 841 | .map_err(|e| e.to_string())?; | ||
| 842 | |||
| 843 | let replacement_announcement = client | ||
| 844 | .event_builder(Kind::GitRepoAnnouncement, "") | ||
| 845 | .tag(Tag::identifier(&repo_id)) | ||
| 846 | .tag(Tag::custom( | ||
| 847 | TagKind::custom("clone"), | ||
| 848 | vec![format!("{}/{}/{}.git", http_url, npub, repo_id)], | ||
| 849 | )) | ||
| 850 | .tag(Tag::custom( | ||
| 851 | TagKind::custom("relays"), | ||
| 852 | vec![relay_url.clone()], | ||
| 853 | )) | ||
| 854 | .tag(Tag::custom( | ||
| 855 | TagKind::custom("maintainers"), | ||
| 856 | vec![new_maintainer_hex.clone()], | ||
| 857 | )) | ||
| 858 | .build(client.keys()) | ||
| 859 | .map_err(|e| format!("Failed to build replacement announcement: {}", e))?; | ||
| 860 | |||
| 861 | client | ||
| 862 | .send_event(replacement_announcement) | ||
| 863 | .await | ||
| 864 | .map_err(|e| format!("Relay rejected replacement announcement: {}", e))?; | ||
| 865 | |||
| 866 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 867 | |||
| 868 | // Stage 4: clone the repo and create a unique commit (not pushed yet) | ||
| 869 | let relay_domain = relay_url | ||
| 870 | .trim_start_matches("ws://") | ||
| 871 | .trim_start_matches("wss://") | ||
| 872 | .to_string(); | ||
| 873 | |||
| 874 | let clone_path = clone_repo(&relay_domain, &npub, &repo_id) | ||
| 875 | .map_err(|e| format!("Failed to clone repo: {}", e))?; | ||
| 876 | |||
| 877 | let cleanup = || { let _ = fs::remove_dir_all(&clone_path); }; | ||
| 878 | |||
| 879 | let unique_commit = match create_commit(&clone_path, "deletion coordinate test unique commit") { | ||
| 880 | Ok(h) => h, | ||
| 881 | Err(e) => { cleanup(); return Err(format!("Failed to create commit: {}", e)); } | ||
| 882 | }; | ||
| 883 | |||
| 884 | // Stage 5: submit a state event signed by the new maintainer pointing to the | ||
| 885 | // unique commit. The new maintainer is now authorized (listed in the replacement | ||
| 886 | // announcement), so the state event should enter purgatory (commit doesn't exist). | ||
| 887 | let state_event = client | ||
| 888 | .event_builder(Kind::RepoState, "") | ||
| 889 | .tag(Tag::identifier(&repo_id)) | ||
| 890 | .tag(Tag::custom( | ||
| 891 | TagKind::custom("refs/heads/main"), | ||
| 892 | vec![unique_commit.clone()], | ||
| 893 | )) | ||
| 894 | .tag(Tag::custom( | ||
| 895 | TagKind::custom("HEAD"), | ||
| 896 | vec!["ref: refs/heads/main".to_string()], | ||
| 897 | )) | ||
| 898 | .build(&new_maintainer_keys) | ||
| 899 | .map_err(|e| { cleanup(); format!("Failed to build state event: {}", e) })?; | ||
| 900 | |||
| 901 | let (_, in_purgatory) = client | ||
| 902 | .send_event_and_note_purgatory(state_event.clone()) | ||
| 903 | .await | ||
| 904 | .map_err(|e| { cleanup(); format!("Failed to send state event: {}", e) })?; | ||
| 905 | |||
| 906 | if !in_purgatory { | ||
| 907 | cleanup(); | ||
| 908 | return Err(format!( | ||
| 909 | "State event was served immediately (not in purgatory). \ | ||
| 910 | Commit {} may already exist on relay.", | ||
| 911 | unique_commit | ||
| 912 | )); | ||
| 913 | } | ||
| 914 | |||
| 915 | // Stage 6: send kind 5 deletion event signed by the new maintainer, | ||
| 916 | // referencing their state event by coordinate `30618:<pubkey>:<identifier>` | ||
| 917 | let coord = format!("30618:{}:{}", new_maintainer_hex, repo_id); | ||
| 918 | |||
| 919 | let deletion = client | ||
| 920 | .event_builder(Kind::EventDeletion, "") | ||
| 921 | .tag(Tag::custom(TagKind::custom("a"), vec![coord])) | ||
| 922 | .tag(Tag::custom(TagKind::custom("k"), vec!["30618"])) | ||
| 923 | .build(&new_maintainer_keys) | ||
| 924 | .map_err(|e| { cleanup(); format!("Failed to build deletion event: {}", e) })?; | ||
| 925 | |||
| 926 | client | ||
| 927 | .send_event(deletion) | ||
| 928 | .await | ||
| 929 | .map_err(|e| { cleanup(); format!("Relay rejected deletion event: {}", e) })?; | ||
| 930 | |||
| 931 | tokio::time::sleep(Duration::from_millis(300)).await; | ||
| 932 | |||
| 933 | // Stage 7: attempt to push the unique commit — must be rejected because | ||
| 934 | // the new maintainer's state event was deleted from purgatory | ||
| 935 | let push_result = try_push(&clone_path); | ||
| 936 | cleanup(); | ||
| 937 | |||
| 938 | match push_result { | ||
| 939 | Ok(false) => Ok(()), // push rejected as expected | ||
| 940 | Ok(true) => Err(format!( | ||
| 941 | "Push was accepted but should have been rejected. \ | ||
| 942 | The new maintainer's state event (id={}) was deleted by coordinate, \ | ||
| 943 | so commit {} should not be authorized.", | ||
| 944 | state_event.id, unique_commit | ||
| 945 | )), | ||
| 946 | Err(e) => Err(format!("Git push error: {}", e)), | ||
| 947 | } | ||
| 948 | }) | ||
| 949 | .await | ||
| 950 | } | ||
| 951 | } | ||
| 952 | |||
| 953 | #[cfg(test)] | ||
| 954 | mod tests { | ||
| 955 | use super::*; | ||
| 956 | use crate::AuditConfig; | ||
| 957 | |||
| 958 | #[tokio::test] | ||
| 959 | #[ignore] // Requires running relay | ||
| 960 | async fn test_grasp01_purgatory_against_relay() { | ||
| 961 | let relay_url = std::env::var("RELAY_URL").expect( | ||
| 962 | "RELAY_URL environment variable must be set. Example: RELAY_URL=ws://localhost:18081", | ||
| 963 | ); | ||
| 964 | |||
| 965 | let config = AuditConfig::isolated(); | ||
| 966 | let client = AuditClient::new(&relay_url, config) | ||
| 967 | .await | ||
| 968 | .unwrap_or_else(|_| { | ||
| 969 | panic!( | ||
| 970 | "Failed to connect to relay at {}. Ensure relay is running and accessible.", | ||
| 971 | relay_url | ||
| 972 | ) | ||
| 973 | }); | ||
| 974 | |||
| 975 | let results = PurgatoryTests::run_all(&client).await; | ||
| 976 | results.print_report(); | ||
| 977 | |||
| 978 | assert!( | ||
| 979 | results.all_passed(), | ||
| 980 | "Some purgatory tests failed. See report above." | ||
| 981 | ); | ||
| 982 | } | ||
| 983 | } | ||
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index c1003b9..73cbe1f 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs | |||
| @@ -19,7 +19,7 @@ | |||
| 19 | /// Expected hash for PR test deterministic commit | 19 | /// Expected hash for PR test deterministic commit |
| 20 | /// | 20 | /// |
| 21 | /// This hash is produced by creating a commit with: | 21 | /// This hash is produced by creating a commit with: |
| 22 | /// - File: test.txt containing "PR test deterministic commit" | 22 | /// - File: test.txt containing "PR test deterministic commit\n" (with trailing newline) |
| 23 | /// - Message: "PR test deterministic commit" | 23 | /// - Message: "PR test deterministic commit" |
| 24 | /// - Author: "GRASP Audit Test <test@grasp-audit.local>" | 24 | /// - Author: "GRASP Audit Test <test@grasp-audit.local>" |
| 25 | /// - Author date: 2024-01-01T00:00:00Z | 25 | /// - Author date: 2024-01-01T00:00:00Z |
| @@ -29,8 +29,9 @@ | |||
| 29 | /// | 29 | /// |
| 30 | /// Run `test_pr_test_commit_hash_discovery` to discover/verify this value. | 30 | /// Run `test_pr_test_commit_hash_discovery` to discover/verify this value. |
| 31 | #[allow(dead_code)] | 31 | #[allow(dead_code)] |
| 32 | const PR_TEST_COMMIT_HASH: &str = "5d40fb1555a0c28bf4d650515a73aaa54d4d9bfb"; | 32 | const PR_TEST_COMMIT_HASH: &str = "5a51b30e4615b572dcd5b9e487861b58605a5c21"; |
| 33 | 33 | ||
| 34 | use crate::specs::grasp01::SpecRef; | ||
| 34 | use crate::{ | 35 | use crate::{ |
| 35 | clone_repo, create_commit, create_deterministic_commit_with_variant, try_push, try_push_to_ref, | 36 | clone_repo, create_commit, create_deterministic_commit_with_variant, try_push, try_push_to_ref, |
| 36 | AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, | 37 | AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, |
| @@ -207,7 +208,7 @@ async fn setup_pr_test_repo( | |||
| 207 | ) -> Result<(PathBuf, String, String, String), String> { | 208 | ) -> Result<(PathBuf, String, String, String), String> { |
| 208 | // Get fixtures | 209 | // Get fixtures |
| 209 | let repo_event = ctx | 210 | let repo_event = ctx |
| 210 | .get_fixture(FixtureKind::ValidRepo) | 211 | .get_fixture(FixtureKind::ValidRepoServed) |
| 211 | .await | 212 | .await |
| 212 | .map_err(|e| format!("Failed to get repo announcement: {}", e))?; | 213 | .map_err(|e| format!("Failed to get repo announcement: {}", e))?; |
| 213 | 214 | ||
| @@ -406,12 +407,12 @@ impl PushAuthorizationTests { | |||
| 406 | let ctx = TestContext::new(client); | 407 | let ctx = TestContext::new(client); |
| 407 | 408 | ||
| 408 | // Create repository (no state event) | 409 | // Create repository (no state event) |
| 409 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 410 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { |
| 410 | Ok(r) => r, | 411 | Ok(r) => r, |
| 411 | Err(e) => { | 412 | Err(e) => { |
| 412 | return TestResult::new( | 413 | return TestResult::new( |
| 413 | test_name, | 414 | test_name, |
| 414 | "GRASP-01:git-http:36", | 415 | SpecRef::GitAcceptPushesAlignState, |
| 415 | "Push rejected without state event", | 416 | "Push rejected without state event", |
| 416 | ) | 417 | ) |
| 417 | .fail(format!("Failed to create repo: {}", e)) | 418 | .fail(format!("Failed to create repo: {}", e)) |
| @@ -435,7 +436,7 @@ impl PushAuthorizationTests { | |||
| 435 | Err(e) => { | 436 | Err(e) => { |
| 436 | return TestResult::new( | 437 | return TestResult::new( |
| 437 | test_name, | 438 | test_name, |
| 438 | "GRASP-01:git-http:36", | 439 | SpecRef::GitAcceptPushesAlignState, |
| 439 | "Push rejected without state event", | 440 | "Push rejected without state event", |
| 440 | ) | 441 | ) |
| 441 | .fail(&e) | 442 | .fail(&e) |
| @@ -449,7 +450,7 @@ impl PushAuthorizationTests { | |||
| 449 | cleanup(); | 450 | cleanup(); |
| 450 | return TestResult::new( | 451 | return TestResult::new( |
| 451 | test_name, | 452 | test_name, |
| 452 | "GRASP-01:git-http:36", | 453 | SpecRef::GitAcceptPushesAlignState, |
| 453 | "Push rejected without state event", | 454 | "Push rejected without state event", |
| 454 | ) | 455 | ) |
| 455 | .fail(&e); | 456 | .fail(&e); |
| @@ -462,19 +463,19 @@ impl PushAuthorizationTests { | |||
| 462 | match push_result { | 463 | match push_result { |
| 463 | Ok(false) => TestResult::new( | 464 | Ok(false) => TestResult::new( |
| 464 | test_name, | 465 | test_name, |
| 465 | "GRASP-01:git-http:36", | 466 | SpecRef::GitAcceptPushesAlignState, |
| 466 | "Push rejected without state event", | 467 | "Push rejected without state event", |
| 467 | ) | 468 | ) |
| 468 | .pass(), | 469 | .pass(), |
| 469 | Ok(true) => TestResult::new( | 470 | Ok(true) => TestResult::new( |
| 470 | test_name, | 471 | test_name, |
| 471 | "GRASP-01:git-http:36", | 472 | SpecRef::GitAcceptPushesAlignState, |
| 472 | "Push rejected without state event", | 473 | "Push rejected without state event", |
| 473 | ) | 474 | ) |
| 474 | .fail("Push accepted but should be rejected"), | 475 | .fail("Push accepted but should be rejected"), |
| 475 | Err(e) => TestResult::new( | 476 | Err(e) => TestResult::new( |
| 476 | test_name, | 477 | test_name, |
| 477 | "GRASP-01:git-http:36", | 478 | SpecRef::GitAcceptPushesAlignState, |
| 478 | "Push rejected without state event", | 479 | "Push rejected without state event", |
| 479 | ) | 480 | ) |
| 480 | .fail(&e), | 481 | .fail(&e), |
| @@ -507,13 +508,13 @@ impl PushAuthorizationTests { | |||
| 507 | match ctx.get_fixture(FixtureKind::OwnerStateDataPushed).await { | 508 | match ctx.get_fixture(FixtureKind::OwnerStateDataPushed).await { |
| 508 | Ok(_state_event) => TestResult::new( | 509 | Ok(_state_event) => TestResult::new( |
| 509 | test_name, | 510 | test_name, |
| 510 | "GRASP-01:git-http:36", // TODO do we add purgatory line here? | 511 | SpecRef::GitAcceptPushesAlignState, |
| 511 | "Push authorized with matching state", | 512 | "Push authorized with matching state", |
| 512 | ) | 513 | ) |
| 513 | .pass(), | 514 | .pass(), |
| 514 | Err(e) => TestResult::new( | 515 | Err(e) => TestResult::new( |
| 515 | test_name, | 516 | test_name, |
| 516 | "GRASP-01:git-http:36", | 517 | SpecRef::GitAcceptPushesAlignState, |
| 517 | "Push authorized with matching state", | 518 | "Push authorized with matching state", |
| 518 | ) | 519 | ) |
| 519 | .fail(format!("{}", e)), | 520 | .fail(format!("{}", e)), |
| @@ -555,7 +556,7 @@ impl PushAuthorizationTests { | |||
| 555 | Err(e) => { | 556 | Err(e) => { |
| 556 | return TestResult::new( | 557 | return TestResult::new( |
| 557 | test_name, | 558 | test_name, |
| 558 | "GRASP-01:git-http:36", | 559 | SpecRef::GitAcceptPushesAlignState, |
| 559 | "Push rejected when commit not in state event", | 560 | "Push rejected when commit not in state event", |
| 560 | ) | 561 | ) |
| 561 | .fail(format!("Failed to create RepoState fixture: {}", e)); | 562 | .fail(format!("Failed to create RepoState fixture: {}", e)); |
| @@ -575,7 +576,7 @@ impl PushAuthorizationTests { | |||
| 575 | None => { | 576 | None => { |
| 576 | return TestResult::new( | 577 | return TestResult::new( |
| 577 | test_name, | 578 | test_name, |
| 578 | "GRASP-01:git-http:36", | 579 | SpecRef::GitAcceptPushesAlignState, |
| 579 | "Push rejected when commit not in state event", | 580 | "Push rejected when commit not in state event", |
| 580 | ) | 581 | ) |
| 581 | .fail("Missing repo_id in state event"); | 582 | .fail("Missing repo_id in state event"); |
| @@ -587,7 +588,7 @@ impl PushAuthorizationTests { | |||
| 587 | Err(e) => { | 588 | Err(e) => { |
| 588 | return TestResult::new( | 589 | return TestResult::new( |
| 589 | test_name, | 590 | test_name, |
| 590 | "GRASP-01:git-http:36", | 591 | SpecRef::GitAcceptPushesAlignState, |
| 591 | "Push rejected when commit not in state event", | 592 | "Push rejected when commit not in state event", |
| 592 | ) | 593 | ) |
| 593 | .fail(format!("Failed to convert pubkey to bech32: {}", e)); | 594 | .fail(format!("Failed to convert pubkey to bech32: {}", e)); |
| @@ -603,7 +604,7 @@ impl PushAuthorizationTests { | |||
| 603 | Err(e) => { | 604 | Err(e) => { |
| 604 | return TestResult::new( | 605 | return TestResult::new( |
| 605 | test_name, | 606 | test_name, |
| 606 | "GRASP-01:git-http:36", | 607 | SpecRef::GitAcceptPushesAlignState, |
| 607 | "Push rejected when commit not in state event", | 608 | "Push rejected when commit not in state event", |
| 608 | ) | 609 | ) |
| 609 | .fail(format!("Failed to clone repo: {}", e)); | 610 | .fail(format!("Failed to clone repo: {}", e)); |
| @@ -626,7 +627,7 @@ impl PushAuthorizationTests { | |||
| 626 | cleanup(); | 627 | cleanup(); |
| 627 | return TestResult::new( | 628 | return TestResult::new( |
| 628 | test_name, | 629 | test_name, |
| 629 | "GRASP-01:git-http:36", | 630 | SpecRef::GitAcceptPushesAlignState, |
| 630 | "Push rejected when commit not in state event", | 631 | "Push rejected when commit not in state event", |
| 631 | ) | 632 | ) |
| 632 | .fail(format!("Failed to create/checkout main branch: {}", e)); | 633 | .fail(format!("Failed to create/checkout main branch: {}", e)); |
| @@ -635,7 +636,7 @@ impl PushAuthorizationTests { | |||
| 635 | cleanup(); | 636 | cleanup(); |
| 636 | return TestResult::new( | 637 | return TestResult::new( |
| 637 | test_name, | 638 | test_name, |
| 638 | "GRASP-01:git-http:36", | 639 | SpecRef::GitAcceptPushesAlignState, |
| 639 | "Push rejected when commit not in state event", | 640 | "Push rejected when commit not in state event", |
| 640 | ) | 641 | ) |
| 641 | .fail(format!( | 642 | .fail(format!( |
| @@ -652,7 +653,7 @@ impl PushAuthorizationTests { | |||
| 652 | cleanup(); | 653 | cleanup(); |
| 653 | return TestResult::new( | 654 | return TestResult::new( |
| 654 | test_name, | 655 | test_name, |
| 655 | "GRASP-01:git-http:36", | 656 | SpecRef::GitAcceptPushesAlignState, |
| 656 | "Push rejected when commit not in state event", | 657 | "Push rejected when commit not in state event", |
| 657 | ) | 658 | ) |
| 658 | .fail(format!("Failed to create wrong commit: {}", e)); | 659 | .fail(format!("Failed to create wrong commit: {}", e)); |
| @@ -666,10 +667,10 @@ impl PushAuthorizationTests { | |||
| 666 | cleanup(); | 667 | cleanup(); |
| 667 | 668 | ||
| 668 | match push_result { | 669 | match push_result { |
| 669 | Ok(false) => TestResult::new(test_name, "GRASP-01:git-http:36", "Push rejected when commit not in state event").pass(), | 670 | Ok(false) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event").pass(), |
| 670 | Ok(true) => TestResult::new(test_name, "GRASP-01:git-http:36", "Push rejected when commit not in state event") | 671 | Ok(true) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event") |
| 671 | .fail("Push accepted but should be rejected. The pushed commit is not in the state event."), | 672 | .fail("Push accepted but should be rejected. The pushed commit is not in the state event."), |
| 672 | Err(e) => TestResult::new(test_name, "GRASP-01:git-http:36", "Push rejected when commit not in state event").fail(&e), | 673 | Err(e) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event").fail(&e), |
| 673 | } | 674 | } |
| 674 | } | 675 | } |
| 675 | 676 | ||
| @@ -704,13 +705,13 @@ impl PushAuthorizationTests { | |||
| 704 | { | 705 | { |
| 705 | Ok(_maintainer_state_event) => TestResult::new( | 706 | Ok(_maintainer_state_event) => TestResult::new( |
| 706 | test_name, | 707 | test_name, |
| 707 | "GRASP-01:git-http:36", | 708 | SpecRef::GitAcceptPushesAlignState, |
| 708 | "Push authorized by maintainer state event only (no announcement)", | 709 | "Push authorized by maintainer state event only (no announcement)", |
| 709 | ) | 710 | ) |
| 710 | .pass(), | 711 | .pass(), |
| 711 | Err(e) => TestResult::new( | 712 | Err(e) => TestResult::new( |
| 712 | test_name, | 713 | test_name, |
| 713 | "GRASP-01:git-http:36", | 714 | SpecRef::GitAcceptPushesAlignState, |
| 714 | "Push authorized by maintainer state event only (no announcement)", | 715 | "Push authorized by maintainer state event only (no announcement)", |
| 715 | ) | 716 | ) |
| 716 | .fail(format!("{}", e)), | 717 | .fail(format!("{}", e)), |
| @@ -747,13 +748,13 @@ impl PushAuthorizationTests { | |||
| 747 | { | 748 | { |
| 748 | Ok(_recursive_maintainer_state_event) => TestResult::new( | 749 | Ok(_recursive_maintainer_state_event) => TestResult::new( |
| 749 | test_name, | 750 | test_name, |
| 750 | "GRASP-01:git-http:36", | 751 | SpecRef::GitAcceptPushesAlignState, |
| 751 | "Push authorized by recursive maintainer state event", | 752 | "Push authorized by recursive maintainer state event", |
| 752 | ) | 753 | ) |
| 753 | .pass(), | 754 | .pass(), |
| 754 | Err(e) => TestResult::new( | 755 | Err(e) => TestResult::new( |
| 755 | test_name, | 756 | test_name, |
| 756 | "GRASP-01:git-http:36", | 757 | SpecRef::GitAcceptPushesAlignState, |
| 757 | "Push authorized by recursive maintainer state event", | 758 | "Push authorized by recursive maintainer state event", |
| 758 | ) | 759 | ) |
| 759 | .fail(format!("{}", e)), | 760 | .fail(format!("{}", e)), |
| @@ -797,7 +798,7 @@ impl PushAuthorizationTests { | |||
| 797 | Err(e) => { | 798 | Err(e) => { |
| 798 | return TestResult::new( | 799 | return TestResult::new( |
| 799 | test_name, | 800 | test_name, |
| 800 | "GRASP-01:git-http:36", | 801 | SpecRef::GitAcceptPushesAlignState, |
| 801 | "Non-maintainer state events ignored", | 802 | "Non-maintainer state events ignored", |
| 802 | ) | 803 | ) |
| 803 | .fail(format!("Failed to get OwnerStateDataPushed fixture: {}", e)); | 804 | .fail(format!("Failed to get OwnerStateDataPushed fixture: {}", e)); |
| @@ -815,7 +816,7 @@ impl PushAuthorizationTests { | |||
| 815 | None => { | 816 | None => { |
| 816 | return TestResult::new( | 817 | return TestResult::new( |
| 817 | test_name, | 818 | test_name, |
| 818 | "GRASP-01:git-http:36", | 819 | SpecRef::GitAcceptPushesAlignState, |
| 819 | "Non-maintainer state events ignored", | 820 | "Non-maintainer state events ignored", |
| 820 | ) | 821 | ) |
| 821 | .fail("Missing repo_id in state event"); | 822 | .fail("Missing repo_id in state event"); |
| @@ -827,7 +828,7 @@ impl PushAuthorizationTests { | |||
| 827 | Err(e) => { | 828 | Err(e) => { |
| 828 | return TestResult::new( | 829 | return TestResult::new( |
| 829 | test_name, | 830 | test_name, |
| 830 | "GRASP-01:git-http:36", | 831 | SpecRef::GitAcceptPushesAlignState, |
| 831 | "Non-maintainer state events ignored", | 832 | "Non-maintainer state events ignored", |
| 832 | ) | 833 | ) |
| 833 | .fail(format!("Failed to convert pubkey to bech32: {}", e)); | 834 | .fail(format!("Failed to convert pubkey to bech32: {}", e)); |
| @@ -842,7 +843,7 @@ impl PushAuthorizationTests { | |||
| 842 | Err(e) => { | 843 | Err(e) => { |
| 843 | return TestResult::new( | 844 | return TestResult::new( |
| 844 | test_name, | 845 | test_name, |
| 845 | "GRASP-01:git-http:36", | 846 | SpecRef::GitAcceptPushesAlignState, |
| 846 | "Non-maintainer state events ignored", | 847 | "Non-maintainer state events ignored", |
| 847 | ) | 848 | ) |
| 848 | .fail(format!("Failed to clone repo: {}", e)); | 849 | .fail(format!("Failed to clone repo: {}", e)); |
| @@ -864,7 +865,7 @@ impl PushAuthorizationTests { | |||
| 864 | cleanup(); | 865 | cleanup(); |
| 865 | return TestResult::new( | 866 | return TestResult::new( |
| 866 | test_name, | 867 | test_name, |
| 867 | "GRASP-01:git-http:36", | 868 | SpecRef::GitAcceptPushesAlignState, |
| 868 | "Non-maintainer state events ignored", | 869 | "Non-maintainer state events ignored", |
| 869 | ) | 870 | ) |
| 870 | .fail(format!("Failed to create commit: {}", e)); | 871 | .fail(format!("Failed to create commit: {}", e)); |
| @@ -890,7 +891,7 @@ impl PushAuthorizationTests { | |||
| 890 | cleanup(); | 891 | cleanup(); |
| 891 | return TestResult::new( | 892 | return TestResult::new( |
| 892 | test_name, | 893 | test_name, |
| 893 | "GRASP-01:git-http:36", | 894 | SpecRef::GitAcceptPushesAlignState, |
| 894 | "Non-maintainer state events ignored", | 895 | "Non-maintainer state events ignored", |
| 895 | ) | 896 | ) |
| 896 | .fail(format!("Failed to build rogue state event: {}", e)); | 897 | .fail(format!("Failed to build rogue state event: {}", e)); |
| @@ -902,7 +903,7 @@ impl PushAuthorizationTests { | |||
| 902 | cleanup(); | 903 | cleanup(); |
| 903 | return TestResult::new( | 904 | return TestResult::new( |
| 904 | test_name, | 905 | test_name, |
| 905 | "GRASP-01:git-http:36", | 906 | SpecRef::GitAcceptPushesAlignState, |
| 906 | "Non-maintainer state events ignored", | 907 | "Non-maintainer state events ignored", |
| 907 | ) | 908 | ) |
| 908 | .fail(format!("Failed to send rogue state event: {}", e)); | 909 | .fail(format!("Failed to send rogue state event: {}", e)); |
| @@ -919,8 +920,8 @@ impl PushAuthorizationTests { | |||
| 919 | cleanup(); | 920 | cleanup(); |
| 920 | 921 | ||
| 921 | match push_result { | 922 | match push_result { |
| 922 | Ok(false) => TestResult::new(test_name, "GRASP-01:git-http:36", "Non-maintainer state events ignored").pass(), | 923 | Ok(false) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored").pass(), |
| 923 | Ok(true) => TestResult::new(test_name, "GRASP-01:git-http:36", "Non-maintainer state events ignored") | 924 | Ok(true) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored") |
| 924 | .fail(format!( | 925 | .fail(format!( |
| 925 | "Push accepted but should be rejected. A non-maintainer (pubkey: {}) published \ | 926 | "Push accepted but should be rejected. A non-maintainer (pubkey: {}) published \ |
| 926 | a state event announcing commit {}, but the push was accepted. The relay should \ | 927 | a state event announcing commit {}, but the push was accepted. The relay should \ |
| @@ -929,7 +930,7 @@ impl PushAuthorizationTests { | |||
| 929 | new_commit, | 930 | new_commit, |
| 930 | client.public_key() | 931 | client.public_key() |
| 931 | )), | 932 | )), |
| 932 | Err(e) => TestResult::new(test_name, "GRASP-01:git-http:36", "Non-maintainer state events ignored").fail(&e), | 933 | Err(e) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored").fail(&e), |
| 933 | } | 934 | } |
| 934 | } | 935 | } |
| 935 | 936 | ||
| @@ -955,12 +956,12 @@ impl PushAuthorizationTests { | |||
| 955 | // ============================================================ | 956 | // ============================================================ |
| 956 | let ctx = TestContext::new(client); | 957 | let ctx = TestContext::new(client); |
| 957 | 958 | ||
| 958 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 959 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { |
| 959 | Ok(r) => r, | 960 | Ok(r) => r, |
| 960 | Err(e) => { | 961 | Err(e) => { |
| 961 | return TestResult::new( | 962 | return TestResult::new( |
| 962 | test_name, | 963 | test_name, |
| 963 | "GRASP-01:git-http:40", | 964 | SpecRef::GitAcceptRefsNostrEventId, |
| 964 | "Push to refs/nostr/<invalid-event-id> rejected", | 965 | "Push to refs/nostr/<invalid-event-id> rejected", |
| 965 | ) | 966 | ) |
| 966 | .fail(format!("Failed to create repo: {}", e)); | 967 | .fail(format!("Failed to create repo: {}", e)); |
| @@ -986,7 +987,7 @@ impl PushAuthorizationTests { | |||
| 986 | Err(e) => { | 987 | Err(e) => { |
| 987 | return TestResult::new( | 988 | return TestResult::new( |
| 988 | test_name, | 989 | test_name, |
| 989 | "GRASP-01:git-http:40", | 990 | SpecRef::GitAcceptRefsNostrEventId, |
| 990 | "Push to refs/nostr/<invalid-event-id> rejected", | 991 | "Push to refs/nostr/<invalid-event-id> rejected", |
| 991 | ) | 992 | ) |
| 992 | .fail(&e); | 993 | .fail(&e); |
| @@ -1001,7 +1002,7 @@ impl PushAuthorizationTests { | |||
| 1001 | cleanup(); | 1002 | cleanup(); |
| 1002 | return TestResult::new( | 1003 | return TestResult::new( |
| 1003 | test_name, | 1004 | test_name, |
| 1004 | "GRASP-01:git-http:40", | 1005 | SpecRef::GitAcceptRefsNostrEventId, |
| 1005 | "Push to refs/nostr/<invalid-event-id> rejected", | 1006 | "Push to refs/nostr/<invalid-event-id> rejected", |
| 1006 | ) | 1007 | ) |
| 1007 | .fail(&e); | 1008 | .fail(&e); |
| @@ -1020,13 +1021,13 @@ impl PushAuthorizationTests { | |||
| 1020 | match push_result { | 1021 | match push_result { |
| 1021 | Ok(false) => TestResult::new( | 1022 | Ok(false) => TestResult::new( |
| 1022 | test_name, | 1023 | test_name, |
| 1023 | "GRASP-01:git-http:40", | 1024 | SpecRef::GitAcceptRefsNostrEventId, |
| 1024 | "Push to refs/nostr/<invalid-event-id> rejected", | 1025 | "Push to refs/nostr/<invalid-event-id> rejected", |
| 1025 | ) | 1026 | ) |
| 1026 | .pass(), | 1027 | .pass(), |
| 1027 | Ok(true) => TestResult::new( | 1028 | Ok(true) => TestResult::new( |
| 1028 | test_name, | 1029 | test_name, |
| 1029 | "GRASP-01:git-http:40", | 1030 | SpecRef::GitAcceptRefsNostrEventId, |
| 1030 | "Push to refs/nostr/<invalid-event-id> rejected", | 1031 | "Push to refs/nostr/<invalid-event-id> rejected", |
| 1031 | ) | 1032 | ) |
| 1032 | .fail(format!( | 1033 | .fail(format!( |
| @@ -1037,7 +1038,7 @@ impl PushAuthorizationTests { | |||
| 1037 | )), | 1038 | )), |
| 1038 | Err(e) => TestResult::new( | 1039 | Err(e) => TestResult::new( |
| 1039 | test_name, | 1040 | test_name, |
| 1040 | "GRASP-01:git-http:40", | 1041 | SpecRef::GitAcceptRefsNostrEventId, |
| 1041 | "Push to refs/nostr/<invalid-event-id> rejected", | 1042 | "Push to refs/nostr/<invalid-event-id> rejected", |
| 1042 | ) | 1043 | ) |
| 1043 | .fail(format!("Push error: {}", e)), | 1044 | .fail(format!("Push error: {}", e)), |
| @@ -1071,10 +1072,11 @@ impl PushAuthorizationTests { | |||
| 1071 | .get_fixture(FixtureKind::PRWrongCommitPushedBeforeEvent) | 1072 | .get_fixture(FixtureKind::PRWrongCommitPushedBeforeEvent) |
| 1072 | .await | 1073 | .await |
| 1073 | { | 1074 | { |
| 1074 | Ok(_pr_event) => TestResult::new(test_name, "GRASP-01:git-http:40", desc).pass(), | 1075 | Ok(_pr_event) => { |
| 1075 | Err(e) => { | 1076 | TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).pass() |
| 1076 | TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(format!("{}", e)) | ||
| 1077 | } | 1077 | } |
| 1078 | Err(e) => TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) | ||
| 1079 | .fail(format!("{}", e)), | ||
| 1078 | } | 1080 | } |
| 1079 | } | 1081 | } |
| 1080 | 1082 | ||
| @@ -1100,7 +1102,7 @@ impl PushAuthorizationTests { | |||
| 1100 | { | 1102 | { |
| 1101 | Ok(e) => e, | 1103 | Ok(e) => e, |
| 1102 | Err(e) => { | 1104 | Err(e) => { |
| 1103 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1105 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1104 | .fail(format!("{}", e)); | 1106 | .fail(format!("{}", e)); |
| 1105 | } | 1107 | } |
| 1106 | }; | 1108 | }; |
| @@ -1108,10 +1110,10 @@ impl PushAuthorizationTests { | |||
| 1108 | let pr_event_id = pr_event.id.to_hex(); | 1110 | let pr_event_id = pr_event.id.to_hex(); |
| 1109 | 1111 | ||
| 1110 | // Get repo info for cloning (fresh clone for verification) | 1112 | // Get repo info for cloning (fresh clone for verification) |
| 1111 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 1113 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoServed).await { |
| 1112 | Ok(r) => r, | 1114 | Ok(r) => r, |
| 1113 | Err(e) => { | 1115 | Err(e) => { |
| 1114 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1116 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1115 | .fail(format!("{}", e)); | 1117 | .fail(format!("{}", e)); |
| 1116 | } | 1118 | } |
| 1117 | }; | 1119 | }; |
| @@ -1127,7 +1129,7 @@ impl PushAuthorizationTests { | |||
| 1127 | let owner_npub = match repo.pubkey.to_bech32() { | 1129 | let owner_npub = match repo.pubkey.to_bech32() { |
| 1128 | Ok(n) => n, | 1130 | Ok(n) => n, |
| 1129 | Err(e) => { | 1131 | Err(e) => { |
| 1130 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1132 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1131 | .fail(format!("Failed to get owner npub: {}", e)); | 1133 | .fail(format!("Failed to get owner npub: {}", e)); |
| 1132 | } | 1134 | } |
| 1133 | }; | 1135 | }; |
| @@ -1136,7 +1138,8 @@ impl PushAuthorizationTests { | |||
| 1136 | let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { | 1138 | let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { |
| 1137 | Ok(p) => p, | 1139 | Ok(p) => p, |
| 1138 | Err(e) => { | 1140 | Err(e) => { |
| 1139 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); | 1141 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1142 | .fail(&e); | ||
| 1140 | } | 1143 | } |
| 1141 | }; | 1144 | }; |
| 1142 | 1145 | ||
| @@ -1146,7 +1149,8 @@ impl PushAuthorizationTests { | |||
| 1146 | Ok(exists) => exists, | 1149 | Ok(exists) => exists, |
| 1147 | Err(e) => { | 1150 | Err(e) => { |
| 1148 | let _ = fs::remove_dir_all(&clone_path); | 1151 | let _ = fs::remove_dir_all(&clone_path); |
| 1149 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); | 1152 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1153 | .fail(&e); | ||
| 1150 | } | 1154 | } |
| 1151 | }; | 1155 | }; |
| 1152 | 1156 | ||
| @@ -1154,13 +1158,13 @@ impl PushAuthorizationTests { | |||
| 1154 | 1158 | ||
| 1155 | // Ref should be deleted since the pushed commit doesn't match the PR event's `c` tag | 1159 | // Ref should be deleted since the pushed commit doesn't match the PR event's `c` tag |
| 1156 | if refs_exist { | 1160 | if refs_exist { |
| 1157 | TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(format!( | 1161 | TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).fail(format!( |
| 1158 | "Expected refs/nostr/{} to be deleted when PR event published with non-matching commit, \ | 1162 | "Expected refs/nostr/{} to be deleted when PR event published with non-matching commit, \ |
| 1159 | but the ref still exists. The relay should delete refs that don't match the event's `c` tag.", | 1163 | but the ref still exists. The relay should delete refs that don't match the event's `c` tag.", |
| 1160 | pr_event_id | 1164 | pr_event_id |
| 1161 | )) | 1165 | )) |
| 1162 | } else { | 1166 | } else { |
| 1163 | TestResult::new(test_name, "GRASP-01:git-http:40", desc).pass() | 1167 | TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).pass() |
| 1164 | } | 1168 | } |
| 1165 | } | 1169 | } |
| 1166 | 1170 | ||
| @@ -1186,7 +1190,7 @@ impl PushAuthorizationTests { | |||
| 1186 | { | 1190 | { |
| 1187 | Ok(e) => e, | 1191 | Ok(e) => e, |
| 1188 | Err(e) => { | 1192 | Err(e) => { |
| 1189 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1193 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1190 | .fail(format!("{}", e)); | 1194 | .fail(format!("{}", e)); |
| 1191 | } | 1195 | } |
| 1192 | }; | 1196 | }; |
| @@ -1194,10 +1198,10 @@ impl PushAuthorizationTests { | |||
| 1194 | let pr_event_id = pr_event.id.to_hex(); | 1198 | let pr_event_id = pr_event.id.to_hex(); |
| 1195 | 1199 | ||
| 1196 | // Get repo info for cloning (fresh clone for this test) | 1200 | // Get repo info for cloning (fresh clone for this test) |
| 1197 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 1201 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoServed).await { |
| 1198 | Ok(r) => r, | 1202 | Ok(r) => r, |
| 1199 | Err(e) => { | 1203 | Err(e) => { |
| 1200 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1204 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1201 | .fail(format!("{}", e)); | 1205 | .fail(format!("{}", e)); |
| 1202 | } | 1206 | } |
| 1203 | }; | 1207 | }; |
| @@ -1213,7 +1217,7 @@ impl PushAuthorizationTests { | |||
| 1213 | let owner_npub = match repo.pubkey.to_bech32() { | 1217 | let owner_npub = match repo.pubkey.to_bech32() { |
| 1214 | Ok(n) => n, | 1218 | Ok(n) => n, |
| 1215 | Err(e) => { | 1219 | Err(e) => { |
| 1216 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1220 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1217 | .fail(format!("Failed to get owner npub: {}", e)); | 1221 | .fail(format!("Failed to get owner npub: {}", e)); |
| 1218 | } | 1222 | } |
| 1219 | }; | 1223 | }; |
| @@ -1222,15 +1226,16 @@ impl PushAuthorizationTests { | |||
| 1222 | let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { | 1226 | let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { |
| 1223 | Ok(p) => p, | 1227 | Ok(p) => p, |
| 1224 | Err(e) => { | 1228 | Err(e) => { |
| 1225 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); | 1229 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1230 | .fail(&e); | ||
| 1226 | } | 1231 | } |
| 1227 | }; | 1232 | }; |
| 1228 | 1233 | ||
| 1229 | // Create a wrong commit (Owner variant, not PRTestCommit) | 1234 | // Create a wrong commit (unique, not PRTestCommit) - use create_commit so it always |
| 1230 | if let Err(e) = create_deterministic_commit_with_variant(&clone_path, CommitVariant::Owner) | 1235 | // succeeds even when the clone already has the Owner deterministic content on disk. |
| 1231 | { | 1236 | if let Err(e) = create_commit(&clone_path, "wrong commit - not the PR test commit") { |
| 1232 | let _ = fs::remove_dir_all(&clone_path); | 1237 | let _ = fs::remove_dir_all(&clone_path); |
| 1233 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); | 1238 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).fail(&e); |
| 1234 | } | 1239 | } |
| 1235 | 1240 | ||
| 1236 | // Try to push with wrong commit (should be rejected since PR event exists) | 1241 | // Try to push with wrong commit (should be rejected since PR event exists) |
| @@ -1238,7 +1243,8 @@ impl PushAuthorizationTests { | |||
| 1238 | Ok(success) => success, | 1243 | Ok(success) => success, |
| 1239 | Err(e) => { | 1244 | Err(e) => { |
| 1240 | let _ = fs::remove_dir_all(&clone_path); | 1245 | let _ = fs::remove_dir_all(&clone_path); |
| 1241 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); | 1246 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1247 | .fail(&e); | ||
| 1242 | } | 1248 | } |
| 1243 | }; | 1249 | }; |
| 1244 | 1250 | ||
| @@ -1246,11 +1252,11 @@ impl PushAuthorizationTests { | |||
| 1246 | 1252 | ||
| 1247 | // Should REJECT - PR event exists with different commit hash | 1253 | // Should REJECT - PR event exists with different commit hash |
| 1248 | if push_succeeded { | 1254 | if push_succeeded { |
| 1249 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1255 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1250 | .fail("Push accepted (expected rejection due to commit hash mismatch)"); | 1256 | .fail("Push accepted (expected rejection due to commit hash mismatch)"); |
| 1251 | } | 1257 | } |
| 1252 | 1258 | ||
| 1253 | TestResult::new(test_name, "GRASP-01:git-http:40", desc).pass() | 1259 | TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).pass() |
| 1254 | } | 1260 | } |
| 1255 | 1261 | ||
| 1256 | /// Test 4: Push correct commit to refs/nostr/<pr-event-id> AFTER PR event exists | 1262 | /// Test 4: Push correct commit to refs/nostr/<pr-event-id> AFTER PR event exists |
| @@ -1275,7 +1281,7 @@ impl PushAuthorizationTests { | |||
| 1275 | { | 1281 | { |
| 1276 | Ok(e) => e, | 1282 | Ok(e) => e, |
| 1277 | Err(e) => { | 1283 | Err(e) => { |
| 1278 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1284 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1279 | .fail(format!("{}", e)); | 1285 | .fail(format!("{}", e)); |
| 1280 | } | 1286 | } |
| 1281 | }; | 1287 | }; |
| @@ -1283,10 +1289,10 @@ impl PushAuthorizationTests { | |||
| 1283 | let pr_event_id = pr_event.id.to_hex(); | 1289 | let pr_event_id = pr_event.id.to_hex(); |
| 1284 | 1290 | ||
| 1285 | // Get repo info for cloning (fresh clone for this test) | 1291 | // Get repo info for cloning (fresh clone for this test) |
| 1286 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 1292 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoServed).await { |
| 1287 | Ok(r) => r, | 1293 | Ok(r) => r, |
| 1288 | Err(e) => { | 1294 | Err(e) => { |
| 1289 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1295 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1290 | .fail(format!("{}", e)); | 1296 | .fail(format!("{}", e)); |
| 1291 | } | 1297 | } |
| 1292 | }; | 1298 | }; |
| @@ -1302,7 +1308,7 @@ impl PushAuthorizationTests { | |||
| 1302 | let owner_npub = match repo.pubkey.to_bech32() { | 1308 | let owner_npub = match repo.pubkey.to_bech32() { |
| 1303 | Ok(n) => n, | 1309 | Ok(n) => n, |
| 1304 | Err(e) => { | 1310 | Err(e) => { |
| 1305 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1311 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1306 | .fail(format!("Failed to get owner npub: {}", e)); | 1312 | .fail(format!("Failed to get owner npub: {}", e)); |
| 1307 | } | 1313 | } |
| 1308 | }; | 1314 | }; |
| @@ -1311,26 +1317,27 @@ impl PushAuthorizationTests { | |||
| 1311 | let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { | 1317 | let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { |
| 1312 | Ok(p) => p, | 1318 | Ok(p) => p, |
| 1313 | Err(e) => { | 1319 | Err(e) => { |
| 1314 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); | 1320 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1321 | .fail(&e); | ||
| 1315 | } | 1322 | } |
| 1316 | }; | 1323 | }; |
| 1317 | 1324 | ||
| 1318 | // Create the CORRECT PR test commit (the one expected by PR event) | 1325 | // Create the CORRECT PR test commit (the one expected by PR event) |
| 1319 | if let Err(e) = reset_to_correct_pr_commit(&clone_path) { | 1326 | if let Err(e) = reset_to_correct_pr_commit(&clone_path) { |
| 1320 | let _ = fs::remove_dir_all(&clone_path); | 1327 | let _ = fs::remove_dir_all(&clone_path); |
| 1321 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); | 1328 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).fail(&e); |
| 1322 | } | 1329 | } |
| 1323 | 1330 | ||
| 1324 | // Check event is not yet served by relay (still in purgatory) | 1331 | // Check event is not yet served by relay (still in purgatory) |
| 1325 | match client.is_event_on_relay(pr_event.id).await { | 1332 | match client.is_event_on_relay(pr_event.id).await { |
| 1326 | Ok(on_relay) => { | 1333 | Ok(on_relay) => { |
| 1327 | if on_relay { | 1334 | if on_relay { |
| 1328 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1335 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1329 | .fail("PR event not in purgatory before correct commit pushed to refs/nostr/<event-id> (the relay serve the PR event)"); | 1336 | .fail("PR event not in purgatory before correct commit pushed to refs/nostr/<event-id> (the relay serve the PR event)"); |
| 1330 | } | 1337 | } |
| 1331 | } | 1338 | } |
| 1332 | Err(_) => { | 1339 | Err(_) => { |
| 1333 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1340 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1334 | .fail("failed to query relay"); | 1341 | .fail("failed to query relay"); |
| 1335 | } | 1342 | } |
| 1336 | } | 1343 | } |
| @@ -1340,7 +1347,8 @@ impl PushAuthorizationTests { | |||
| 1340 | Ok(success) => success, | 1347 | Ok(success) => success, |
| 1341 | Err(e) => { | 1348 | Err(e) => { |
| 1342 | let _ = fs::remove_dir_all(&clone_path); | 1349 | let _ = fs::remove_dir_all(&clone_path); |
| 1343 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); | 1350 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1351 | .fail(&e); | ||
| 1344 | } | 1352 | } |
| 1345 | }; | 1353 | }; |
| 1346 | 1354 | ||
| @@ -1348,7 +1356,7 @@ impl PushAuthorizationTests { | |||
| 1348 | 1356 | ||
| 1349 | // Should ACCEPT - commit matches PR event's c tag | 1357 | // Should ACCEPT - commit matches PR event's c tag |
| 1350 | if !push_succeeded { | 1358 | if !push_succeeded { |
| 1351 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1359 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1352 | .fail("Push rejected (expected acceptance since commit matches PR event)"); | 1360 | .fail("Push rejected (expected acceptance since commit matches PR event)"); |
| 1353 | } | 1361 | } |
| 1354 | 1362 | ||
| @@ -1361,17 +1369,17 @@ impl PushAuthorizationTests { | |||
| 1361 | match client.is_event_on_relay(pr_event.id).await { | 1369 | match client.is_event_on_relay(pr_event.id).await { |
| 1362 | Ok(on_relay) => { | 1370 | Ok(on_relay) => { |
| 1363 | if !on_relay { | 1371 | if !on_relay { |
| 1364 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1372 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1365 | .fail("PR event not served after correct commit at refs/nostr/<event-id>"); | 1373 | .fail("PR event not served after correct commit at refs/nostr/<event-id>"); |
| 1366 | } | 1374 | } |
| 1367 | } | 1375 | } |
| 1368 | Err(_) => { | 1376 | Err(_) => { |
| 1369 | return TestResult::new(test_name, "GRASP-01:git-http:40", desc) | 1377 | return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) |
| 1370 | .fail("failed to query relay"); | 1378 | .fail("failed to query relay"); |
| 1371 | } | 1379 | } |
| 1372 | } | 1380 | } |
| 1373 | 1381 | ||
| 1374 | TestResult::new(test_name, "GRASP-01:git-http:40", desc).pass() | 1382 | TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).pass() |
| 1375 | } | 1383 | } |
| 1376 | 1384 | ||
| 1377 | /// Test that HEAD is set after a state event is published with an existing commit | 1385 | /// Test that HEAD is set after a state event is published with an existing commit |
| @@ -1408,20 +1416,19 @@ impl PushAuthorizationTests { | |||
| 1408 | { | 1416 | { |
| 1409 | Ok(e) => e, | 1417 | Ok(e) => e, |
| 1410 | Err(e) => { | 1418 | Err(e) => { |
| 1411 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc).fail(format!( | 1419 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).fail( |
| 1412 | "Failed to create HeadSetToDevelopBranch fixture: {}", | 1420 | format!("Failed to create HeadSetToDevelopBranch fixture: {}", e), |
| 1413 | e | 1421 | ); |
| 1414 | )); | ||
| 1415 | } | 1422 | } |
| 1416 | }; | 1423 | }; |
| 1417 | 1424 | ||
| 1418 | // ============================================================ | 1425 | // ============================================================ |
| 1419 | // Step 2: Extract repo_id and owner npub from ValidRepo (cached by fixture) | 1426 | // Step 2: Extract repo_id and owner npub from ValidRepo (cached by fixture) |
| 1420 | // ============================================================ | 1427 | // ============================================================ |
| 1421 | let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 1428 | let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { |
| 1422 | Ok(e) => e, | 1429 | Ok(e) => e, |
| 1423 | Err(e) => { | 1430 | Err(e) => { |
| 1424 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1431 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1425 | .fail(format!("Failed to get ValidRepo fixture: {}", e)); | 1432 | .fail(format!("Failed to get ValidRepo fixture: {}", e)); |
| 1426 | } | 1433 | } |
| 1427 | }; | 1434 | }; |
| @@ -1434,7 +1441,7 @@ impl PushAuthorizationTests { | |||
| 1434 | { | 1441 | { |
| 1435 | Some(id) => id.to_string(), | 1442 | Some(id) => id.to_string(), |
| 1436 | None => { | 1443 | None => { |
| 1437 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1444 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1438 | .fail("Missing repo_id in ValidRepo"); | 1445 | .fail("Missing repo_id in ValidRepo"); |
| 1439 | } | 1446 | } |
| 1440 | }; | 1447 | }; |
| @@ -1442,7 +1449,7 @@ impl PushAuthorizationTests { | |||
| 1442 | let npub = match valid_repo.pubkey.to_bech32() { | 1449 | let npub = match valid_repo.pubkey.to_bech32() { |
| 1443 | Ok(n) => n, | 1450 | Ok(n) => n, |
| 1444 | Err(e) => { | 1451 | Err(e) => { |
| 1445 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1452 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1446 | .fail(format!("Failed to convert pubkey to bech32: {}", e)); | 1453 | .fail(format!("Failed to convert pubkey to bech32: {}", e)); |
| 1447 | } | 1454 | } |
| 1448 | }; | 1455 | }; |
| @@ -1454,16 +1461,16 @@ impl PushAuthorizationTests { | |||
| 1454 | match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await { | 1461 | match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await { |
| 1455 | Ok(branch) => branch, | 1462 | Ok(branch) => branch, |
| 1456 | Err(e) => { | 1463 | Err(e) => { |
| 1457 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1464 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1458 | .fail(format!("Failed to get default branch: {}", e)); | 1465 | .fail(format!("Failed to get default branch: {}", e)); |
| 1459 | } | 1466 | } |
| 1460 | }; | 1467 | }; |
| 1461 | 1468 | ||
| 1462 | // Verify HEAD points to refs/heads/develop | 1469 | // Verify HEAD points to refs/heads/develop |
| 1463 | if default_branch == "refs/heads/develop" { | 1470 | if default_branch == "refs/heads/develop" { |
| 1464 | TestResult::new(test_name, "GRASP-01:git-http:38", desc).pass() | 1471 | TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).pass() |
| 1465 | } else { | 1472 | } else { |
| 1466 | TestResult::new(test_name, "GRASP-01:git-http:38", desc).fail(format!( | 1473 | TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).fail(format!( |
| 1467 | "Expected HEAD to point to 'refs/heads/develop' but got '{}'. \ | 1474 | "Expected HEAD to point to 'refs/heads/develop' but got '{}'. \ |
| 1468 | GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \ | 1475 | GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \ |
| 1469 | as soon as the git data related to that branch has been received.'", | 1476 | as soon as the git data related to that branch has been received.'", |
| @@ -1512,20 +1519,19 @@ impl PushAuthorizationTests { | |||
| 1512 | let _develop_state = match ctx.get_fixture(FixtureKind::HeadSetToDevelopBranch).await { | 1519 | let _develop_state = match ctx.get_fixture(FixtureKind::HeadSetToDevelopBranch).await { |
| 1513 | Ok(e) => e, | 1520 | Ok(e) => e, |
| 1514 | Err(e) => { | 1521 | Err(e) => { |
| 1515 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc).fail(format!( | 1522 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).fail( |
| 1516 | "Failed to create HeadSetToDevelopBranch fixture: {}", | 1523 | format!("Failed to create HeadSetToDevelopBranch fixture: {}", e), |
| 1517 | e | 1524 | ); |
| 1518 | )); | ||
| 1519 | } | 1525 | } |
| 1520 | }; | 1526 | }; |
| 1521 | 1527 | ||
| 1522 | // ============================================================ | 1528 | // ============================================================ |
| 1523 | // Step 2: Extract repo_id and owner npub from ValidRepo (cached by fixture) | 1529 | // Step 2: Extract repo_id and owner npub from ValidRepo (cached by fixture) |
| 1524 | // ============================================================ | 1530 | // ============================================================ |
| 1525 | let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 1531 | let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { |
| 1526 | Ok(e) => e, | 1532 | Ok(e) => e, |
| 1527 | Err(e) => { | 1533 | Err(e) => { |
| 1528 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1534 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1529 | .fail(format!("Failed to get ValidRepo fixture: {}", e)); | 1535 | .fail(format!("Failed to get ValidRepo fixture: {}", e)); |
| 1530 | } | 1536 | } |
| 1531 | }; | 1537 | }; |
| @@ -1538,7 +1544,7 @@ impl PushAuthorizationTests { | |||
| 1538 | { | 1544 | { |
| 1539 | Some(id) => id.to_string(), | 1545 | Some(id) => id.to_string(), |
| 1540 | None => { | 1546 | None => { |
| 1541 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1547 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1542 | .fail("Missing repo_id in ValidRepo"); | 1548 | .fail("Missing repo_id in ValidRepo"); |
| 1543 | } | 1549 | } |
| 1544 | }; | 1550 | }; |
| @@ -1546,7 +1552,7 @@ impl PushAuthorizationTests { | |||
| 1546 | let npub = match valid_repo.pubkey.to_bech32() { | 1552 | let npub = match valid_repo.pubkey.to_bech32() { |
| 1547 | Ok(n) => n, | 1553 | Ok(n) => n, |
| 1548 | Err(e) => { | 1554 | Err(e) => { |
| 1549 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1555 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1550 | .fail(format!("Failed to convert pubkey to bech32: {}", e)); | 1556 | .fail(format!("Failed to convert pubkey to bech32: {}", e)); |
| 1551 | } | 1557 | } |
| 1552 | }; | 1558 | }; |
| @@ -1557,7 +1563,7 @@ impl PushAuthorizationTests { | |||
| 1557 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { | 1563 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { |
| 1558 | Ok(path) => path, | 1564 | Ok(path) => path, |
| 1559 | Err(e) => { | 1565 | Err(e) => { |
| 1560 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1566 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1561 | .fail(format!("Failed to clone repo: {}", e)); | 1567 | .fail(format!("Failed to clone repo: {}", e)); |
| 1562 | } | 1568 | } |
| 1563 | }; | 1569 | }; |
| @@ -1572,7 +1578,7 @@ impl PushAuthorizationTests { | |||
| 1572 | 1578 | ||
| 1573 | if let Err(e) = output { | 1579 | if let Err(e) = output { |
| 1574 | let _ = fs::remove_dir_all(&clone_path); | 1580 | let _ = fs::remove_dir_all(&clone_path); |
| 1575 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1581 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1576 | .fail(format!("Failed to create develop1 branch: {}", e)); | 1582 | .fail(format!("Failed to create develop1 branch: {}", e)); |
| 1577 | } | 1583 | } |
| 1578 | 1584 | ||
| @@ -1581,7 +1587,7 @@ impl PushAuthorizationTests { | |||
| 1581 | Ok(hash) => hash, | 1587 | Ok(hash) => hash, |
| 1582 | Err(e) => { | 1588 | Err(e) => { |
| 1583 | let _ = fs::remove_dir_all(&clone_path); | 1589 | let _ = fs::remove_dir_all(&clone_path); |
| 1584 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1590 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1585 | .fail(format!("Failed to create commit: {}", e)); | 1591 | .fail(format!("Failed to create commit: {}", e)); |
| 1586 | } | 1592 | } |
| 1587 | }; | 1593 | }; |
| @@ -1610,7 +1616,7 @@ impl PushAuthorizationTests { | |||
| 1610 | Ok(e) => e, | 1616 | Ok(e) => e, |
| 1611 | Err(e) => { | 1617 | Err(e) => { |
| 1612 | let _ = fs::remove_dir_all(&clone_path); | 1618 | let _ = fs::remove_dir_all(&clone_path); |
| 1613 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1619 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1614 | .fail(format!("Failed to build state event: {}", e)); | 1620 | .fail(format!("Failed to build state event: {}", e)); |
| 1615 | } | 1621 | } |
| 1616 | }; | 1622 | }; |
| @@ -1621,7 +1627,7 @@ impl PushAuthorizationTests { | |||
| 1621 | .await | 1627 | .await |
| 1622 | { | 1628 | { |
| 1623 | let _ = fs::remove_dir_all(&clone_path); | 1629 | let _ = fs::remove_dir_all(&clone_path); |
| 1624 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1630 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1625 | .fail(format!("Failed to send state event: {}", e)); | 1631 | .fail(format!("Failed to send state event: {}", e)); |
| 1626 | } | 1632 | } |
| 1627 | 1633 | ||
| @@ -1634,11 +1640,11 @@ impl PushAuthorizationTests { | |||
| 1634 | match push_result { | 1640 | match push_result { |
| 1635 | Ok(true) => { /* Push succeeded, continue to verify */ } | 1641 | Ok(true) => { /* Push succeeded, continue to verify */ } |
| 1636 | Ok(false) => { | 1642 | Ok(false) => { |
| 1637 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1643 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1638 | .fail("Push to refs/heads/develop1 was rejected"); | 1644 | .fail("Push to refs/heads/develop1 was rejected"); |
| 1639 | } | 1645 | } |
| 1640 | Err(e) => { | 1646 | Err(e) => { |
| 1641 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1647 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1642 | .fail(format!("Failed to push develop1 branch: {}", e)); | 1648 | .fail(format!("Failed to push develop1 branch: {}", e)); |
| 1643 | } | 1649 | } |
| 1644 | } | 1650 | } |
| @@ -1651,16 +1657,16 @@ impl PushAuthorizationTests { | |||
| 1651 | match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await { | 1657 | match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await { |
| 1652 | Ok(branch) => branch, | 1658 | Ok(branch) => branch, |
| 1653 | Err(e) => { | 1659 | Err(e) => { |
| 1654 | return TestResult::new(test_name, "GRASP-01:git-http:38", desc) | 1660 | return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) |
| 1655 | .fail(format!("Failed to get default branch: {}", e)); | 1661 | .fail(format!("Failed to get default branch: {}", e)); |
| 1656 | } | 1662 | } |
| 1657 | }; | 1663 | }; |
| 1658 | 1664 | ||
| 1659 | // Verify HEAD points to refs/heads/develop1 | 1665 | // Verify HEAD points to refs/heads/develop1 |
| 1660 | if default_branch == "refs/heads/develop1" { | 1666 | if default_branch == "refs/heads/develop1" { |
| 1661 | TestResult::new(test_name, "GRASP-01:git-http:38", desc).pass() | 1667 | TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).pass() |
| 1662 | } else { | 1668 | } else { |
| 1663 | TestResult::new(test_name, "GRASP-01:git-http:38", desc).fail(format!( | 1669 | TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).fail(format!( |
| 1664 | "Expected HEAD to point to 'refs/heads/develop1' but got '{}'. \ | 1670 | "Expected HEAD to point to 'refs/heads/develop1' but got '{}'. \ |
| 1665 | GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \ | 1671 | GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \ |
| 1666 | as soon as the git data related to that branch has been received.'", | 1672 | as soon as the git data related to that branch has been received.'", |
| @@ -1701,24 +1707,24 @@ mod tests { | |||
| 1701 | String::from_utf8_lossy(&output.stderr) | 1707 | String::from_utf8_lossy(&output.stderr) |
| 1702 | ); | 1708 | ); |
| 1703 | 1709 | ||
| 1704 | // Configure git user - use PR Test Author identity | 1710 | // Configure git user - use same identity as clone_repo in fixtures.rs |
| 1705 | let output = Command::new("git") | 1711 | let output = Command::new("git") |
| 1706 | .args(["config", "user.email", "pr-test@example.com"]) | 1712 | .args(["config", "user.email", "test@grasp-audit.local"]) |
| 1707 | .current_dir(path) | 1713 | .current_dir(path) |
| 1708 | .output() | 1714 | .output() |
| 1709 | .expect("git config email failed"); | 1715 | .expect("git config email failed"); |
| 1710 | assert!(output.status.success(), "git config email failed"); | 1716 | assert!(output.status.success(), "git config email failed"); |
| 1711 | 1717 | ||
| 1712 | let output = Command::new("git") | 1718 | let output = Command::new("git") |
| 1713 | .args(["config", "user.name", "PR Test Author"]) | 1719 | .args(["config", "user.name", "GRASP Audit Test"]) |
| 1714 | .current_dir(path) | 1720 | .current_dir(path) |
| 1715 | .output() | 1721 | .output() |
| 1716 | .expect("git config name failed"); | 1722 | .expect("git config name failed"); |
| 1717 | assert!(output.status.success(), "git config name failed"); | 1723 | assert!(output.status.success(), "git config name failed"); |
| 1718 | 1724 | ||
| 1719 | // Create the deterministic file content | 1725 | // Create the deterministic file content (must match CommitVariant::PRTestCommit exactly) |
| 1720 | let test_file = path.join("test.txt"); | 1726 | let test_file = path.join("test.txt"); |
| 1721 | fs::write(&test_file, "PR test deterministic commit").expect("Failed to write test file"); | 1727 | fs::write(&test_file, "PR test deterministic commit\n").expect("Failed to write test file"); |
| 1722 | 1728 | ||
| 1723 | // Add the file | 1729 | // Add the file |
| 1724 | let output = Command::new("git") | 1730 | let output = Command::new("git") |
diff --git a/grasp-audit/src/specs/grasp01/repository_creation.rs b/grasp-audit/src/specs/grasp01/repository_creation.rs index 2eddb97..5730f1c 100644 --- a/grasp-audit/src/specs/grasp01/repository_creation.rs +++ b/grasp-audit/src/specs/grasp01/repository_creation.rs | |||
| @@ -15,6 +15,7 @@ | |||
| 15 | //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test | 15 | //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test |
| 16 | //! ``` | 16 | //! ``` |
| 17 | 17 | ||
| 18 | use crate::specs::grasp01::SpecRef; | ||
| 18 | use crate::{AuditClient, FixtureKind, TestContext, TestResult}; | 19 | use crate::{AuditClient, FixtureKind, TestContext, TestResult}; |
| 19 | use nostr_sdk::prelude::*; | 20 | use nostr_sdk::prelude::*; |
| 20 | 21 | ||
| @@ -50,12 +51,12 @@ impl RepositoryCreationTests { | |||
| 50 | let ctx = TestContext::new(client); | 51 | let ctx = TestContext::new(client); |
| 51 | 52 | ||
| 52 | // Use TestContext to create and send repository announcement | 53 | // Use TestContext to create and send repository announcement |
| 53 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 54 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { |
| 54 | Ok(r) => r, | 55 | Ok(r) => r, |
| 55 | Err(e) => { | 56 | Err(e) => { |
| 56 | return TestResult::new( | 57 | return TestResult::new( |
| 57 | test_name, | 58 | test_name, |
| 58 | "GRASP-01:git-http:34", | 59 | SpecRef::GitServeRepository, |
| 59 | "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", | 60 | "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", |
| 60 | ) | 61 | ) |
| 61 | .fail(format!("Failed to create repo fixture: {}", e)) | 62 | .fail(format!("Failed to create repo fixture: {}", e)) |
| @@ -76,7 +77,7 @@ impl RepositoryCreationTests { | |||
| 76 | None => { | 77 | None => { |
| 77 | return TestResult::new( | 78 | return TestResult::new( |
| 78 | test_name, | 79 | test_name, |
| 79 | "GRASP-01:git-http:34", | 80 | SpecRef::GitServeRepository, |
| 80 | "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", | 81 | "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", |
| 81 | ) | 82 | ) |
| 82 | .fail("Repository announcement missing d tag") | 83 | .fail("Repository announcement missing d tag") |
| @@ -88,7 +89,7 @@ impl RepositoryCreationTests { | |||
| 88 | Err(e) => { | 89 | Err(e) => { |
| 89 | return TestResult::new( | 90 | return TestResult::new( |
| 90 | test_name, | 91 | test_name, |
| 91 | "GRASP-01:git-http:34", | 92 | SpecRef::GitServeRepository, |
| 92 | "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", | 93 | "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", |
| 93 | ) | 94 | ) |
| 94 | .fail(format!("Failed to convert pubkey to npub: {}", e)) | 95 | .fail(format!("Failed to convert pubkey to npub: {}", e)) |
| @@ -99,7 +100,7 @@ impl RepositoryCreationTests { | |||
| 99 | if let Err(e) = check_repo_accessible_via_http(relay_domain, &npub, &repo_id).await { | 100 | if let Err(e) = check_repo_accessible_via_http(relay_domain, &npub, &repo_id).await { |
| 100 | return TestResult::new( | 101 | return TestResult::new( |
| 101 | test_name, | 102 | test_name, |
| 102 | "GRASP-01:git-http:34", | 103 | SpecRef::GitServeRepository, |
| 103 | "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", | 104 | "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", |
| 104 | ) | 105 | ) |
| 105 | .fail(format!("Repository not accessible via HTTP: {}", e)); | 106 | .fail(format!("Repository not accessible via HTTP: {}", e)); |
| @@ -107,7 +108,7 @@ impl RepositoryCreationTests { | |||
| 107 | 108 | ||
| 108 | TestResult::new( | 109 | TestResult::new( |
| 109 | test_name, | 110 | test_name, |
| 110 | "GRASP-01:git-http:34", | 111 | SpecRef::GitServeRepository, |
| 111 | "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", | 112 | "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", |
| 112 | ) | 113 | ) |
| 113 | .pass() | 114 | .pass() |
| @@ -130,12 +131,12 @@ impl RepositoryCreationTests { | |||
| 130 | let ctx = TestContext::new(client); | 131 | let ctx = TestContext::new(client); |
| 131 | 132 | ||
| 132 | // Create a repository announcement | 133 | // Create a repository announcement |
| 133 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 134 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { |
| 134 | Ok(r) => r, | 135 | Ok(r) => r, |
| 135 | Err(e) => { | 136 | Err(e) => { |
| 136 | return TestResult::new( | 137 | return TestResult::new( |
| 137 | test_name, | 138 | test_name, |
| 138 | "GRASP-01:git-http:44", | 139 | SpecRef::GitServeWebpage, |
| 139 | "Relay SHOULD serve a webpage for existing repositories", | 140 | "Relay SHOULD serve a webpage for existing repositories", |
| 140 | ) | 141 | ) |
| 141 | .fail(format!("Failed to create repo fixture: {}", e)) | 142 | .fail(format!("Failed to create repo fixture: {}", e)) |
| @@ -156,7 +157,7 @@ impl RepositoryCreationTests { | |||
| 156 | None => { | 157 | None => { |
| 157 | return TestResult::new( | 158 | return TestResult::new( |
| 158 | test_name, | 159 | test_name, |
| 159 | "GRASP-01:git-http:44", | 160 | SpecRef::GitServeWebpage, |
| 160 | "Relay SHOULD serve a webpage for existing repositories", | 161 | "Relay SHOULD serve a webpage for existing repositories", |
| 161 | ) | 162 | ) |
| 162 | .fail("Repository announcement missing d tag") | 163 | .fail("Repository announcement missing d tag") |
| @@ -168,7 +169,7 @@ impl RepositoryCreationTests { | |||
| 168 | Err(e) => { | 169 | Err(e) => { |
| 169 | return TestResult::new( | 170 | return TestResult::new( |
| 170 | test_name, | 171 | test_name, |
| 171 | "GRASP-01:git-http:44", | 172 | SpecRef::GitServeWebpage, |
| 172 | "Relay SHOULD serve a webpage for existing repositories", | 173 | "Relay SHOULD serve a webpage for existing repositories", |
| 173 | ) | 174 | ) |
| 174 | .fail(format!("Failed to convert pubkey to npub: {}", e)) | 175 | .fail(format!("Failed to convert pubkey to npub: {}", e)) |
| @@ -179,7 +180,7 @@ impl RepositoryCreationTests { | |||
| 179 | if let Err(e) = check_webpage_served(relay_domain, &npub, &repo_id).await { | 180 | if let Err(e) = check_webpage_served(relay_domain, &npub, &repo_id).await { |
| 180 | return TestResult::new( | 181 | return TestResult::new( |
| 181 | test_name, | 182 | test_name, |
| 182 | "GRASP-01:git-http:44", | 183 | SpecRef::GitServeWebpage, |
| 183 | "Relay SHOULD serve a webpage for existing repositories", | 184 | "Relay SHOULD serve a webpage for existing repositories", |
| 184 | ) | 185 | ) |
| 185 | .fail(format!("Webpage not served: {}", e)); | 186 | .fail(format!("Webpage not served: {}", e)); |
| @@ -187,7 +188,7 @@ impl RepositoryCreationTests { | |||
| 187 | 188 | ||
| 188 | TestResult::new( | 189 | TestResult::new( |
| 189 | test_name, | 190 | test_name, |
| 190 | "GRASP-01:git-http:44", | 191 | SpecRef::GitServeWebpage, |
| 191 | "Relay SHOULD serve a webpage for existing repositories", | 192 | "Relay SHOULD serve a webpage for existing repositories", |
| 192 | ) | 193 | ) |
| 193 | .pass() | 194 | .pass() |
| @@ -209,12 +210,12 @@ impl RepositoryCreationTests { | |||
| 209 | 210 | ||
| 210 | let ctx = TestContext::new(client); | 211 | let ctx = TestContext::new(client); |
| 211 | 212 | ||
| 212 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 213 | let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { |
| 213 | Ok(r) => r, | 214 | Ok(r) => r, |
| 214 | Err(e) => { | 215 | Err(e) => { |
| 215 | return TestResult::new( | 216 | return TestResult::new( |
| 216 | test_name, | 217 | test_name, |
| 217 | "GRASP-01:git-http:44", | 218 | SpecRef::GitServeWebpage, |
| 218 | "Relay SHOULD return 404 for repositories it doesn't host", | 219 | "Relay SHOULD return 404 for repositories it doesn't host", |
| 219 | ) | 220 | ) |
| 220 | .fail(format!("Failed to create repo fixture: {}", e)) | 221 | .fail(format!("Failed to create repo fixture: {}", e)) |
| @@ -226,7 +227,7 @@ impl RepositoryCreationTests { | |||
| 226 | Err(e) => { | 227 | Err(e) => { |
| 227 | return TestResult::new( | 228 | return TestResult::new( |
| 228 | test_name, | 229 | test_name, |
| 229 | "GRASP-01:git-http:44", | 230 | SpecRef::GitServeWebpage, |
| 230 | "Relay SHOULD return 404 for repositories it doesn't host", | 231 | "Relay SHOULD return 404 for repositories it doesn't host", |
| 231 | ) | 232 | ) |
| 232 | .fail(format!("Failed to convert pubkey to npub: {}", e)) | 233 | .fail(format!("Failed to convert pubkey to npub: {}", e)) |
| @@ -239,7 +240,7 @@ impl RepositoryCreationTests { | |||
| 239 | if let Err(e) = check_404_for_nonexistent_repo(relay_domain, &npub, fake_repo_id).await { | 240 | if let Err(e) = check_404_for_nonexistent_repo(relay_domain, &npub, fake_repo_id).await { |
| 240 | return TestResult::new( | 241 | return TestResult::new( |
| 241 | test_name, | 242 | test_name, |
| 242 | "GRASP-01:git-http:44", | 243 | SpecRef::GitServeWebpage, |
| 243 | "Relay SHOULD return 404 for repositories it doesn't host", | 244 | "Relay SHOULD return 404 for repositories it doesn't host", |
| 244 | ) | 245 | ) |
| 245 | .fail(format!("Expected 404, got: {}", e)); | 246 | .fail(format!("Expected 404, got: {}", e)); |
| @@ -247,7 +248,7 @@ impl RepositoryCreationTests { | |||
| 247 | 248 | ||
| 248 | TestResult::new( | 249 | TestResult::new( |
| 249 | test_name, | 250 | test_name, |
| 250 | "GRASP-01:git-http:44", | 251 | SpecRef::GitServeWebpage, |
| 251 | "Relay SHOULD return 404 for repositories it doesn't host", | 252 | "Relay SHOULD return 404 for repositories it doesn't host", |
| 252 | ) | 253 | ) |
| 253 | .pass() | 254 | .pass() |
diff --git a/grasp-audit/src/specs/grasp01/spec_requirements.rs b/grasp-audit/src/specs/grasp01/spec_requirements.rs index 71b2d69..6bc961c 100644 --- a/grasp-audit/src/specs/grasp01/spec_requirements.rs +++ b/grasp-audit/src/specs/grasp01/spec_requirements.rs | |||
| @@ -6,9 +6,36 @@ | |||
| 6 | /// GRASP spec repository commit ID that this version is based on | 6 | /// GRASP spec repository commit ID that this version is based on |
| 7 | pub const GRASP_COMMIT_ID: &str = "1fdb8f7"; | 7 | pub const GRASP_COMMIT_ID: &str = "1fdb8f7"; |
| 8 | 8 | ||
| 9 | /// Reference to a specific GRASP-01 specification requirement | ||
| 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 11 | pub enum SpecRef { | ||
| 12 | NostrRelayNip01Compliant, | ||
| 13 | NostrRelayRejectMissingCloneRelays, | ||
| 14 | NostrRelayMayRejectOtherCriteria, | ||
| 15 | NostrRelayMustAcceptTaggedEvents, | ||
| 16 | NostrRelayMayRejectSpamCuration, | ||
| 17 | PurgatoryAcceptUntilGitData, | ||
| 18 | Nip11ServeDocument, | ||
| 19 | Nip11ListSupportedGrasps, | ||
| 20 | Nip11ListRepoAcceptanceCriteria, | ||
| 21 | Nip11ListCurationPolicy, | ||
| 22 | GitServeRepository, | ||
| 23 | GitAcceptPushesAlignState, | ||
| 24 | GitSetHeadOnReceive, | ||
| 25 | GitAcceptRefsNostrEventId, | ||
| 26 | GitIncludeAllowSha1InWant, | ||
| 27 | GitServeWebpage, | ||
| 28 | CorsAllowOrigin, | ||
| 29 | CorsAllowMethods, | ||
| 30 | CorsAllowHeaders, | ||
| 31 | CorsOptionsResponse, | ||
| 32 | } | ||
| 33 | |||
| 9 | /// A single specification requirement | 34 | /// A single specification requirement |
| 10 | #[derive(Debug, Clone)] | 35 | #[derive(Debug, Clone)] |
| 11 | pub struct SpecRequirement { | 36 | pub struct SpecRequirement { |
| 37 | /// Unique reference to this requirement | ||
| 38 | pub spec_ref: SpecRef, | ||
| 12 | /// Line number in the spec document | 39 | /// Line number in the spec document |
| 13 | pub line: u32, | 40 | pub line: u32, |
| 14 | /// Section name (e.g., "Nostr Relay", "Git Smart HTTP Service", "CORS Support") | 41 | /// Section name (e.g., "Nostr Relay", "Git Smart HTTP Service", "CORS Support") |
| @@ -37,121 +64,175 @@ impl std::fmt::Display for RequirementLevel { | |||
| 37 | } | 64 | } |
| 38 | } | 65 | } |
| 39 | 66 | ||
| 67 | impl SpecRef { | ||
| 68 | /// Get the spec reference string in format "GRASP-01:section:line" | ||
| 69 | pub fn spec_ref_string(self) -> &'static str { | ||
| 70 | match self { | ||
| 71 | SpecRef::NostrRelayNip01Compliant => "GRASP-01:nostr-relay:7", | ||
| 72 | SpecRef::NostrRelayRejectMissingCloneRelays => "GRASP-01:nostr-relay:9", | ||
| 73 | SpecRef::NostrRelayMayRejectOtherCriteria => "GRASP-01:nostr-relay:11", | ||
| 74 | SpecRef::NostrRelayMustAcceptTaggedEvents => "GRASP-01:nostr-relay:13", | ||
| 75 | SpecRef::NostrRelayMayRejectSpamCuration => "GRASP-01:nostr-relay:18", | ||
| 76 | SpecRef::PurgatoryAcceptUntilGitData => "GRASP-01:purgatory:22", | ||
| 77 | SpecRef::Nip11ServeDocument => "GRASP-01:nip-11:26", | ||
| 78 | SpecRef::Nip11ListSupportedGrasps => "GRASP-01:nip-11:28", | ||
| 79 | SpecRef::Nip11ListRepoAcceptanceCriteria => "GRASP-01:nip-11:29", | ||
| 80 | SpecRef::Nip11ListCurationPolicy => "GRASP-01:nip-11:30", | ||
| 81 | SpecRef::GitServeRepository => "GRASP-01:git-http:34", | ||
| 82 | SpecRef::GitAcceptPushesAlignState => "GRASP-01:git-http:36", | ||
| 83 | SpecRef::GitSetHeadOnReceive => "GRASP-01:git-http:39", | ||
| 84 | SpecRef::GitAcceptRefsNostrEventId => "GRASP-01:git-http:45", | ||
| 85 | SpecRef::GitIncludeAllowSha1InWant => "GRASP-01:git-http:56", | ||
| 86 | SpecRef::GitServeWebpage => "GRASP-01:git-http:58", | ||
| 87 | SpecRef::CorsAllowOrigin => "GRASP-01:cors:64", | ||
| 88 | SpecRef::CorsAllowMethods => "GRASP-01:cors:65", | ||
| 89 | SpecRef::CorsAllowHeaders => "GRASP-01:cors:66", | ||
| 90 | SpecRef::CorsOptionsResponse => "GRASP-01:cors:67", | ||
| 91 | } | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 40 | /// All GRASP-01 specification requirements | 95 | /// All GRASP-01 specification requirements |
| 41 | pub const GRASP_01_REQUIREMENTS: &[SpecRequirement] = &[ | 96 | pub const GRASP_01_REQUIREMENTS: &[SpecRequirement] = &[ |
| 42 | // Nostr Relay section | 97 | // Nostr Relay section |
| 43 | SpecRequirement { | 98 | SpecRequirement { |
| 99 | spec_ref: SpecRef::NostrRelayNip01Compliant, | ||
| 44 | line: 7, | 100 | line: 7, |
| 45 | section: "Nostr Relay", | 101 | section: "Nostr Relay", |
| 46 | text: "MUST serve a NIP-01 compliant nostr relay at `/` that accepts git repository announcements and their corresponding repo state announcements.", | 102 | text: "MUST serve a NIP-01 compliant nostr relay at `/` that accepts git repository announcements and their corresponding repo state announcements.", |
| 47 | level: RequirementLevel::Must, | 103 | level: RequirementLevel::Must, |
| 48 | }, | 104 | }, |
| 49 | SpecRequirement { | 105 | SpecRequirement { |
| 106 | spec_ref: SpecRef::NostrRelayRejectMissingCloneRelays, | ||
| 50 | line: 9, | 107 | line: 9, |
| 51 | section: "Nostr Relay", | 108 | section: "Nostr Relay", |
| 52 | text: "MUST reject git repository announcements that do not list the service in both `clone` and `relays` tags unless implementing `GRASP-05`.", | 109 | text: "MUST reject git repository announcements that do not list the service in both `clone` and `relays` tags unless implementing `GRASP-05`.", |
| 53 | level: RequirementLevel::Must, | 110 | level: RequirementLevel::Must, |
| 54 | }, | 111 | }, |
| 55 | SpecRequirement { | 112 | SpecRequirement { |
| 113 | spec_ref: SpecRef::NostrRelayMayRejectOtherCriteria, | ||
| 56 | line: 11, | 114 | line: 11, |
| 57 | section: "Nostr Relay", | 115 | section: "Nostr Relay", |
| 58 | text: "MAY reject git repository announcements based on other criteria such as pre-payment, quotas, WoT, whitelist, SPAM prevention, etc.", | 116 | text: "MAY reject git repository announcements based on other criteria such as pre-payment, quotas, WoT, whitelist, SPAM prevention, etc.", |
| 59 | level: RequirementLevel::May, | 117 | level: RequirementLevel::May, |
| 60 | }, | 118 | }, |
| 61 | SpecRequirement { | 119 | SpecRequirement { |
| 120 | spec_ref: SpecRef::NostrRelayMustAcceptTaggedEvents, | ||
| 62 | line: 13, | 121 | line: 13, |
| 63 | section: "Nostr Relay", | 122 | section: "Nostr Relay", |
| 64 | text: "MUST accept other events that tag, or are tagged by, either: 1. accepted git repository announcements; or 2. accepted issues or patches", | 123 | text: "MUST accept other events that tag, or are tagged by, either: 1. accepted git repository announcements; or 2. accepted issues or patches", |
| 65 | level: RequirementLevel::Must, | 124 | level: RequirementLevel::Must, |
| 66 | }, | 125 | }, |
| 67 | SpecRequirement { | 126 | SpecRequirement { |
| 127 | spec_ref: SpecRef::NostrRelayMayRejectSpamCuration, | ||
| 68 | line: 18, | 128 | line: 18, |
| 69 | section: "Nostr Relay", | 129 | section: "Nostr Relay", |
| 70 | text: "MAY reject or delete events for generic SPAM prevention reasons or curation eg. WoT, whitelist, user bans and banned topics.", | 130 | text: "MAY reject or delete events for generic SPAM prevention reasons or curation eg. WoT, whitelist, user bans and banned topics.", |
| 71 | level: RequirementLevel::May, | 131 | level: RequirementLevel::May, |
| 72 | }, | 132 | }, |
| 73 | SpecRequirement { | 133 | SpecRequirement { |
| 134 | spec_ref: SpecRef::PurgatoryAcceptUntilGitData, | ||
| 135 | line: 22, | ||
| 136 | section: "Purgatory", | ||
| 137 | text: "New repository announcements, repo state announcements, PRs and PR Updates SHOULD be accepted with message \"purgatory: won't be served until git data arrives\" and kept in purgatory (not served) until the related git data arrives and otherwise discarded after 30 minutes.", | ||
| 138 | level: RequirementLevel::Should, | ||
| 139 | }, | ||
| 140 | SpecRequirement { | ||
| 141 | spec_ref: SpecRef::Nip11ServeDocument, | ||
| 74 | line: 26, | 142 | line: 26, |
| 75 | section: "Nostr Relay", | 143 | section: "NIP-11", |
| 76 | text: "MUST serve a NIP-11 document", | 144 | text: "MUST serve a NIP-11 document", |
| 77 | level: RequirementLevel::Must, | 145 | level: RequirementLevel::Must, |
| 78 | }, | 146 | }, |
| 79 | SpecRequirement { | 147 | SpecRequirement { |
| 148 | spec_ref: SpecRef::Nip11ListSupportedGrasps, | ||
| 80 | line: 28, | 149 | line: 28, |
| 81 | section: "Nostr Relay", | 150 | section: "NIP-11", |
| 82 | text: "MUST list each supported GRASP under `supported_grasps` in format `GRASP-XX` eg `GRASP-01` as a string array", | 151 | text: "MUST list each supported GRASP under `supported_grasps` in format `GRASP-XX` eg `GRASP-01` as a string array", |
| 83 | level: RequirementLevel::Must, | 152 | level: RequirementLevel::Must, |
| 84 | }, | 153 | }, |
| 85 | SpecRequirement { | 154 | SpecRequirement { |
| 155 | spec_ref: SpecRef::Nip11ListRepoAcceptanceCriteria, | ||
| 86 | line: 29, | 156 | line: 29, |
| 87 | section: "Nostr Relay", | 157 | section: "NIP-11", |
| 88 | text: "MUST list repository acceptance criteria under `repo_acceptance_criteria` as a human readable string", | 158 | text: "MUST list repository acceptance criteria under `repo_acceptance_criteria` as a human readable string", |
| 89 | level: RequirementLevel::Must, | 159 | level: RequirementLevel::Must, |
| 90 | }, | 160 | }, |
| 91 | SpecRequirement { | 161 | SpecRequirement { |
| 162 | spec_ref: SpecRef::Nip11ListCurationPolicy, | ||
| 92 | line: 30, | 163 | line: 30, |
| 93 | section: "Nostr Relay", | 164 | section: "NIP-11", |
| 94 | text: "MUST list brief summary of curation policy under `curation` if events are curated beyond generic SPAM prevention; otherwise `curation` MUST be omitted", | 165 | text: "MUST list brief summary of curation policy under `curation` if events are curated beyond generic SPAM prevention; otherwise `curation` MUST be omitted", |
| 95 | level: RequirementLevel::Must, | 166 | level: RequirementLevel::Must, |
| 96 | }, | 167 | }, |
| 97 | // Git Smart HTTP Service section | 168 | // Git Smart HTTP Service section |
| 98 | SpecRequirement { | 169 | SpecRequirement { |
| 170 | spec_ref: SpecRef::GitServeRepository, | ||
| 99 | line: 34, | 171 | line: 34, |
| 100 | section: "Git Smart HTTP Service", | 172 | section: "Git Smart HTTP Service", |
| 101 | text: "MUST serve a git repository via an unauthenticated git smart http service at `/<npub>/<identifier>.git` for each accepted git repository announcement.", | 173 | text: "MUST serve a git repository via an unauthenticated git smart http service at `/<npub>/<identifier>.git` for each git repository announcement the relay serves or has in purgatory.", |
| 102 | level: RequirementLevel::Must, | 174 | level: RequirementLevel::Must, |
| 103 | }, | 175 | }, |
| 104 | SpecRequirement { | 176 | SpecRequirement { |
| 177 | spec_ref: SpecRef::GitAcceptPushesAlignState, | ||
| 105 | line: 36, | 178 | line: 36, |
| 106 | section: "Git Smart HTTP Service", | 179 | section: "Git Smart HTTP Service", |
| 107 | text: "MUST accept pushes via this service that match the latest repo state announcement on the relay, respecting the recursive maintainer set.", | 180 | text: "MUST accept pushes via this service that fully align the git repository state with a repo state announcement in purgatory that is authorised for this repository, respecting the recursive maintainer set.", |
| 108 | level: RequirementLevel::Must, | 181 | level: RequirementLevel::Must, |
| 109 | }, | 182 | }, |
| 110 | SpecRequirement { | 183 | SpecRequirement { |
| 111 | line: 38, | 184 | spec_ref: SpecRef::GitSetHeadOnReceive, |
| 185 | line: 39, | ||
| 112 | section: "Git Smart HTTP Service", | 186 | section: "Git Smart HTTP Service", |
| 113 | text: "MUST set repository HEAD per repo state announcement as soon as the git data related to that branch has been received.", | 187 | text: "As soon as the `receive-pack` is successful, the server MUST: 1. Release the event (and related repository announcement) from purgatory. 2. Align the repository HEAD with the repo state announcement. 3. Synchronize git state with other git repositories on the server for which this state event is authoritative.", |
| 114 | level: RequirementLevel::Must, | 188 | level: RequirementLevel::Must, |
| 115 | }, | 189 | }, |
| 116 | SpecRequirement { | 190 | SpecRequirement { |
| 117 | line: 40, | 191 | spec_ref: SpecRef::GitAcceptRefsNostrEventId, |
| 192 | line: 45, | ||
| 118 | section: "Git Smart HTTP Service", | 193 | section: "Git Smart HTTP Service", |
| 119 | text: "MUST accept pushes via this service to `refs/nostr/<event-id>` but SHOULD reject if event exists on relay listing a different tip and MAY reject based on criteria such as size, SPAM prevention, etc. SHOULD delete and MAY garbage collect these refs if no corresponding git PR event or git PR update event, with a `c` tag that matches the ref tip, is accepted by relay within 20 minutes.", | 194 | text: "MUST accept pushes via this service to `refs/nostr/<event-id>` but SHOULD reject if the event exists in purgatory listing a different tip, and MAY reject based on criteria such as size, SPAM prevention, etc.", |
| 120 | level: RequirementLevel::Must, | 195 | level: RequirementLevel::Must, |
| 121 | }, | 196 | }, |
| 122 | SpecRequirement { | 197 | SpecRequirement { |
| 123 | line: 42, | 198 | spec_ref: SpecRef::GitIncludeAllowSha1InWant, |
| 199 | line: 56, | ||
| 124 | section: "Git Smart HTTP Service", | 200 | section: "Git Smart HTTP Service", |
| 125 | text: "MUST include `allow-reachable-sha1-in-want` and `allow-tip-sha1-in-want` in advertisement and serve available oids.", | 201 | text: "MUST include `allow-reachable-sha1-in-want` and `allow-tip-sha1-in-want` in advertisement and serve available oids.", |
| 126 | level: RequirementLevel::Must, | 202 | level: RequirementLevel::Must, |
| 127 | }, | 203 | }, |
| 128 | SpecRequirement { | 204 | SpecRequirement { |
| 129 | line: 44, | 205 | spec_ref: SpecRef::GitServeWebpage, |
| 206 | line: 58, | ||
| 130 | section: "Git Smart HTTP Service", | 207 | section: "Git Smart HTTP Service", |
| 131 | text: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s) to browse the repository and a 404 page for repositories it doesn't host.", | 208 | text: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s) to browse the repository and a 404 page for repositories it doesn't host.", |
| 132 | level: RequirementLevel::Should, | 209 | level: RequirementLevel::Should, |
| 133 | }, | 210 | }, |
| 134 | // CORS Support section | 211 | // CORS Support section |
| 135 | SpecRequirement { | 212 | SpecRequirement { |
| 136 | line: 50, | 213 | spec_ref: SpecRef::CorsAllowOrigin, |
| 214 | line: 64, | ||
| 137 | section: "CORS Support", | 215 | section: "CORS Support", |
| 138 | text: "Set `Access-Control-Allow-Origin: *` on ALL responses", | 216 | text: "Set `Access-Control-Allow-Origin: *` on ALL responses", |
| 139 | level: RequirementLevel::Must, | 217 | level: RequirementLevel::Must, |
| 140 | }, | 218 | }, |
| 141 | SpecRequirement { | 219 | SpecRequirement { |
| 142 | line: 51, | 220 | spec_ref: SpecRef::CorsAllowMethods, |
| 221 | line: 65, | ||
| 143 | section: "CORS Support", | 222 | section: "CORS Support", |
| 144 | text: "Set `Access-Control-Allow-Methods: GET, POST` on ALL responses", | 223 | text: "Set `Access-Control-Allow-Methods: GET, POST` on ALL responses", |
| 145 | level: RequirementLevel::Must, | 224 | level: RequirementLevel::Must, |
| 146 | }, | 225 | }, |
| 147 | SpecRequirement { | 226 | SpecRequirement { |
| 148 | line: 52, | 227 | spec_ref: SpecRef::CorsAllowHeaders, |
| 228 | line: 66, | ||
| 149 | section: "CORS Support", | 229 | section: "CORS Support", |
| 150 | text: "Set `Access-Control-Allow-Headers: Content-Type` on ALL responses", | 230 | text: "Set `Access-Control-Allow-Headers: Content-Type` on ALL responses", |
| 151 | level: RequirementLevel::Must, | 231 | level: RequirementLevel::Must, |
| 152 | }, | 232 | }, |
| 153 | SpecRequirement { | 233 | SpecRequirement { |
| 154 | line: 53, | 234 | spec_ref: SpecRef::CorsOptionsResponse, |
| 235 | line: 67, | ||
| 155 | section: "CORS Support", | 236 | section: "CORS Support", |
| 156 | text: "Respond to OPTIONS requests with 204 No Content", | 237 | text: "Respond to OPTIONS requests with 204 No Content", |
| 157 | level: RequirementLevel::Must, | 238 | level: RequirementLevel::Must, |
| @@ -163,6 +244,13 @@ pub fn get_requirement(line: u32) -> Option<&'static SpecRequirement> { | |||
| 163 | GRASP_01_REQUIREMENTS.iter().find(|r| r.line == line) | 244 | GRASP_01_REQUIREMENTS.iter().find(|r| r.line == line) |
| 164 | } | 245 | } |
| 165 | 246 | ||
| 247 | /// Get a requirement by its SpecRef | ||
| 248 | pub fn get_requirement_by_ref(spec_ref: SpecRef) -> Option<&'static SpecRequirement> { | ||
| 249 | GRASP_01_REQUIREMENTS | ||
| 250 | .iter() | ||
| 251 | .find(|r| r.spec_ref == spec_ref) | ||
| 252 | } | ||
| 253 | |||
| 166 | /// Get all requirements for a section | 254 | /// Get all requirements for a section |
| 167 | pub fn get_requirements_for_section(section: &str) -> Vec<&'static SpecRequirement> { | 255 | pub fn get_requirements_for_section(section: &str) -> Vec<&'static SpecRequirement> { |
| 168 | GRASP_01_REQUIREMENTS | 256 | GRASP_01_REQUIREMENTS |
| @@ -194,16 +282,38 @@ mod tests { | |||
| 194 | } | 282 | } |
| 195 | 283 | ||
| 196 | #[test] | 284 | #[test] |
| 285 | fn test_get_requirement_by_ref() { | ||
| 286 | let req = get_requirement_by_ref(SpecRef::NostrRelayNip01Compliant) | ||
| 287 | .expect("SpecRef should exist"); | ||
| 288 | assert_eq!(req.line, 7); | ||
| 289 | assert_eq!(req.spec_ref, SpecRef::NostrRelayNip01Compliant); | ||
| 290 | } | ||
| 291 | |||
| 292 | #[test] | ||
| 197 | fn test_get_sections() { | 293 | fn test_get_sections() { |
| 198 | let sections = get_sections(); | 294 | let sections = get_sections(); |
| 199 | assert_eq!(sections.len(), 3); | 295 | assert_eq!(sections.len(), 5); |
| 200 | assert_eq!(sections[0], "Nostr Relay"); | 296 | assert_eq!(sections[0], "Nostr Relay"); |
| 201 | assert_eq!(sections[1], "Git Smart HTTP Service"); | 297 | assert_eq!(sections[1], "Purgatory"); |
| 202 | assert_eq!(sections[2], "CORS Support"); | 298 | assert_eq!(sections[2], "NIP-11"); |
| 299 | assert_eq!(sections[3], "Git Smart HTTP Service"); | ||
| 300 | assert_eq!(sections[4], "CORS Support"); | ||
| 203 | } | 301 | } |
| 204 | 302 | ||
| 205 | #[test] | 303 | #[test] |
| 206 | fn test_requirement_count() { | 304 | fn test_requirement_count() { |
| 207 | assert_eq!(GRASP_01_REQUIREMENTS.len(), 19); | 305 | assert_eq!(GRASP_01_REQUIREMENTS.len(), 20); |
| 306 | } | ||
| 307 | |||
| 308 | #[test] | ||
| 309 | fn test_spec_ref_unique() { | ||
| 310 | let mut refs = std::collections::HashSet::new(); | ||
| 311 | for req in GRASP_01_REQUIREMENTS { | ||
| 312 | assert!( | ||
| 313 | refs.insert(req.spec_ref), | ||
| 314 | "Duplicate SpecRef found: {:?}", | ||
| 315 | req.spec_ref | ||
| 316 | ); | ||
| 317 | } | ||
| 208 | } | 318 | } |
| 209 | } | 319 | } |
diff --git a/grasp-audit/src/specs/mod.rs b/grasp-audit/src/specs/mod.rs index bf711fa..ceae684 100644 --- a/grasp-audit/src/specs/mod.rs +++ b/grasp-audit/src/specs/mod.rs | |||
| @@ -7,5 +7,5 @@ pub mod grasp01; | |||
| 7 | // Re-export all test structs from grasp01 module | 7 | // Re-export all test structs from grasp01 module |
| 8 | pub use grasp01::{ | 8 | pub use grasp01::{ |
| 9 | CorsTests, EventAcceptancePolicyTests, GitCloneTests, GitFilterTests, Nip01SmokeTests, | 9 | CorsTests, EventAcceptancePolicyTests, GitCloneTests, GitFilterTests, Nip01SmokeTests, |
| 10 | Nip11DocumentTests, PushAuthorizationTests, RepositoryCreationTests, | 10 | Nip11DocumentTests, PurgatoryTests, PushAuthorizationTests, RepositoryCreationTests, |
| 11 | }; | 11 | }; |