diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-27 15:49:27 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-27 15:49:27 +0000 |
| commit | bf93f737aeec7b0ba6d007e867a55a8528615c23 (patch) | |
| tree | 986dabfa0d1486d3f7b83c2a982e8424c296c964 /grasp-audit | |
| parent | 6a77173127b5915c4c1b9219924e793795e0d051 (diff) | |
Task 2: Refactor owner push authorization test to fixture-first pattern
- Refactored test_push_authorized_by_owner_state to use fixture-first pattern
- Test now creates its own TestContext and uses FixtureKind::RepoState
- Uses git helper functions from fixtures.rs (clone_repo, create_deterministic_commit, try_push)
- Follows the 3-step pattern: Generate fixtures → Send to relay → Verify behavior
- Deprecated setup_repo_with_deterministic_commit with migration guide
- Test passes: cargo test --test push_authorization test_push_authorized_by_owner_state
- No API changes required for main project tests
Diffstat (limited to 'grasp-audit')
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 29 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/push_authorization.rs | 168 |
2 files changed, 188 insertions, 9 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index 45a413d..02e9810 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs | |||
| @@ -1108,6 +1108,31 @@ impl Drop for RepoSetup { | |||
| 1108 | 1108 | ||
| 1109 | /// Set up a repository with deterministic commit for testing | 1109 | /// Set up a repository with deterministic commit for testing |
| 1110 | /// | 1110 | /// |
| 1111 | /// # Deprecated | ||
| 1112 | /// | ||
| 1113 | /// This function is deprecated in favor of the fixture-first pattern. | ||
| 1114 | /// Tests should create their own TestContext and use `FixtureKind::RepoState` | ||
| 1115 | /// directly, following the Generate → Send → Verify pattern. | ||
| 1116 | /// | ||
| 1117 | /// See `test_push_authorized_by_owner_state` in `push_authorization.rs` for | ||
| 1118 | /// an example of the fixture-first pattern. | ||
| 1119 | /// | ||
| 1120 | /// ## Migration Guide | ||
| 1121 | /// | ||
| 1122 | /// Instead of: | ||
| 1123 | /// ```ignore | ||
| 1124 | /// let setup = setup_repo_with_deterministic_commit(client, git_data_dir, relay_domain).await?; | ||
| 1125 | /// ``` | ||
| 1126 | /// | ||
| 1127 | /// Use: | ||
| 1128 | /// ```ignore | ||
| 1129 | /// let ctx = TestContext::new(client); | ||
| 1130 | /// let state_event = ctx.get_fixture(FixtureKind::RepoState).await?; | ||
| 1131 | /// // Then clone, create deterministic commit, and push inline | ||
| 1132 | /// ``` | ||
| 1133 | /// | ||
| 1134 | /// --- | ||
| 1135 | /// | ||
| 1111 | /// This performs all the common setup steps needed for push authorization tests: | 1136 | /// This performs all the common setup steps needed for push authorization tests: |
| 1112 | /// 1. Gets RepoState fixture (repo announcement + state event with deterministic commit) | 1137 | /// 1. Gets RepoState fixture (repo announcement + state event with deterministic commit) |
| 1113 | /// 2. Extracts repo_id and npub | 1138 | /// 2. Extracts repo_id and npub |
| @@ -1128,6 +1153,10 @@ impl Drop for RepoSetup { | |||
| 1128 | /// # Returns | 1153 | /// # Returns |
| 1129 | /// * `Ok(RepoSetup)` - The setup data | 1154 | /// * `Ok(RepoSetup)` - The setup data |
| 1130 | /// * `Err(String)` - Error message if setup failed | 1155 | /// * `Err(String)` - Error message if setup failed |
| 1156 | #[deprecated( | ||
| 1157 | since = "0.1.0", | ||
| 1158 | note = "Use fixture-first pattern with TestContext and FixtureKind::RepoState instead. See test_push_authorized_by_owner_state for example." | ||
| 1159 | )] | ||
| 1131 | pub async fn setup_repo_with_deterministic_commit( | 1160 | pub async fn setup_repo_with_deterministic_commit( |
| 1132 | client: &crate::AuditClient, | 1161 | client: &crate::AuditClient, |
| 1133 | git_data_dir: &Path, | 1162 | git_data_dir: &Path, |
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index d58247d..7b7e9dc 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs | |||
| @@ -17,9 +17,9 @@ | |||
| 17 | //! ``` | 17 | //! ``` |
| 18 | 18 | ||
| 19 | use crate::{ | 19 | use crate::{ |
| 20 | clone_repo, create_commit, setup_repo_for_maintainer, setup_repo_for_recursive_maintainer, | 20 | clone_repo, create_commit, create_deterministic_commit, setup_repo_for_maintainer, |
| 21 | setup_repo_with_deterministic_commit, try_push, AuditClient, FixtureKind, TestContext, | 21 | setup_repo_for_recursive_maintainer, setup_repo_with_deterministic_commit, try_push, |
| 22 | TestResult, | 22 | AuditClient, FixtureKind, TestContext, TestResult, DETERMINISTIC_COMMIT_HASH, |
| 23 | }; | 23 | }; |
| 24 | use nostr_sdk::prelude::*; | 24 | use nostr_sdk::prelude::*; |
| 25 | use std::fs; | 25 | use std::fs; |
| @@ -33,23 +33,173 @@ impl PushAuthorizationTests { | |||
| 33 | /// | 33 | /// |
| 34 | /// GRASP-01: "MUST accept pushes via this service that match the latest | 34 | /// GRASP-01: "MUST accept pushes via this service that match the latest |
| 35 | /// repo state announcement on the relay" | 35 | /// repo state announcement on the relay" |
| 36 | /// | ||
| 37 | /// ## Fixture-First Pattern | ||
| 38 | /// | ||
| 39 | /// 1. **Generate**: Create TestContext and get RepoState fixture | ||
| 40 | /// (repo announcement + state event pointing to deterministic commit) | ||
| 41 | /// 2. **Send**: Clone repo, create deterministic commit locally, push to relay | ||
| 42 | /// 3. **Verify**: Push should succeed because state event authorizes this commit | ||
| 36 | pub async fn test_push_authorized_by_owner_state( | 43 | pub async fn test_push_authorized_by_owner_state( |
| 37 | client: &AuditClient, | 44 | client: &AuditClient, |
| 38 | git_data_dir: &Path, | 45 | git_data_dir: &Path, |
| 39 | relay_domain: &str, | 46 | relay_domain: &str, |
| 40 | ) -> TestResult { | 47 | ) -> TestResult { |
| 48 | use std::process::Command; | ||
| 49 | |||
| 41 | let test_name = "test_push_authorized_by_owner_state"; | 50 | let test_name = "test_push_authorized_by_owner_state"; |
| 42 | 51 | ||
| 43 | // this setup is exactly what we are testing | 52 | // ============================================================ |
| 44 | match setup_repo_with_deterministic_commit(client, git_data_dir, relay_domain).await { | 53 | // Step 1: GENERATE - Create TestContext and get RepoState fixture |
| 45 | Ok(_) => { | 54 | // ============================================================ |
| 46 | return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state").pass() | 55 | let ctx = TestContext::new(client); |
| 47 | }, | 56 | |
| 57 | let state_event = match ctx.get_fixture(FixtureKind::RepoState).await { | ||
| 58 | Ok(e) => e, | ||
| 59 | Err(e) => { | ||
| 60 | return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") | ||
| 61 | .fail(&format!("Failed to create RepoState fixture: {}", e)); | ||
| 62 | } | ||
| 63 | }; | ||
| 64 | |||
| 65 | tokio::time::sleep(std::time::Duration::from_millis(200)).await; | ||
| 66 | |||
| 67 | // Extract repo_id and npub from state event | ||
| 68 | let repo_id = match state_event | ||
| 69 | .tags | ||
| 70 | .iter() | ||
| 71 | .find(|t| t.kind() == TagKind::d()) | ||
| 72 | .and_then(|t| t.content()) | ||
| 73 | { | ||
| 74 | Some(id) => id.to_string(), | ||
| 75 | None => { | ||
| 76 | return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") | ||
| 77 | .fail("Missing repo_id in state event"); | ||
| 78 | } | ||
| 79 | }; | ||
| 80 | |||
| 81 | let npub = match state_event.pubkey.to_bech32() { | ||
| 82 | Ok(n) => n, | ||
| 83 | Err(e) => { | ||
| 84 | return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") | ||
| 85 | .fail(&format!("Failed to convert pubkey to bech32: {}", e)); | ||
| 86 | } | ||
| 87 | }; | ||
| 88 | |||
| 89 | // Verify repo exists on disk | ||
| 90 | let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id)); | ||
| 91 | if !repo_path.exists() { | ||
| 92 | return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") | ||
| 93 | .fail(&format!("Repo not found: {}", repo_path.display())); | ||
| 94 | } | ||
| 95 | |||
| 96 | // ============================================================ | ||
| 97 | // Step 2: SEND - Clone repo, create deterministic commit, push | ||
| 98 | // ============================================================ | ||
| 99 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { | ||
| 100 | Ok(p) => p, | ||
| 101 | Err(e) => { | ||
| 102 | return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") | ||
| 103 | .fail(&format!("Failed to clone repo: {}", e)); | ||
| 104 | } | ||
| 105 | }; | ||
| 106 | |||
| 107 | // Cleanup helper | ||
| 108 | let cleanup = || { | ||
| 109 | let _ = fs::remove_dir_all(&clone_path); | ||
| 110 | }; | ||
| 111 | |||
| 112 | // Create deterministic commit locally | ||
| 113 | let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") { | ||
| 114 | Ok(h) => h, | ||
| 48 | Err(e) => { | 115 | Err(e) => { |
| 116 | cleanup(); | ||
| 49 | return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") | 117 | return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") |
| 50 | .fail(&format!("Failed: {}", e)) | 118 | .fail(&format!("Failed to create deterministic commit: {}", e)); |
| 51 | } | 119 | } |
| 52 | }; | 120 | }; |
| 121 | |||
| 122 | // Verify commit hash matches expected | ||
| 123 | if commit_hash != DETERMINISTIC_COMMIT_HASH { | ||
| 124 | cleanup(); | ||
| 125 | return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") | ||
| 126 | .fail(&format!( | ||
| 127 | "Commit hash mismatch: got {}, expected {}", | ||
| 128 | commit_hash, DETERMINISTIC_COMMIT_HASH | ||
| 129 | )); | ||
| 130 | } | ||
| 131 | |||
| 132 | // Create main branch pointing to our deterministic commit | ||
| 133 | let branch_output = Command::new("git") | ||
| 134 | .args(["branch", "main"]) | ||
| 135 | .current_dir(&clone_path) | ||
| 136 | .output(); | ||
| 137 | |||
| 138 | match branch_output { | ||
| 139 | Err(e) => { | ||
| 140 | cleanup(); | ||
| 141 | return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") | ||
| 142 | .fail(&format!("Failed to create main branch: {}", e)); | ||
| 143 | } | ||
| 144 | Ok(output) if !output.status.success() => { | ||
| 145 | cleanup(); | ||
| 146 | return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") | ||
| 147 | .fail(&format!( | ||
| 148 | "Failed to create main branch: {}", | ||
| 149 | String::from_utf8_lossy(&output.stderr) | ||
| 150 | )); | ||
| 151 | } | ||
| 152 | _ => {} | ||
| 153 | } | ||
| 154 | |||
| 155 | // Checkout main branch | ||
| 156 | let checkout_output = Command::new("git") | ||
| 157 | .args(["checkout", "main"]) | ||
| 158 | .current_dir(&clone_path) | ||
| 159 | .output(); | ||
| 160 | |||
| 161 | match checkout_output { | ||
| 162 | Err(e) => { | ||
| 163 | cleanup(); | ||
| 164 | return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") | ||
| 165 | .fail(&format!("Failed to checkout main branch: {}", e)); | ||
| 166 | } | ||
| 167 | Ok(output) if !output.status.success() => { | ||
| 168 | cleanup(); | ||
| 169 | return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") | ||
| 170 | .fail(&format!( | ||
| 171 | "Failed to checkout main branch: {}", | ||
| 172 | String::from_utf8_lossy(&output.stderr) | ||
| 173 | )); | ||
| 174 | } | ||
| 175 | _ => {} | ||
| 176 | } | ||
| 177 | |||
| 178 | // ============================================================ | ||
| 179 | // Step 3: VERIFY - Push should succeed because state event | ||
| 180 | // authorizes this commit | ||
| 181 | // ============================================================ | ||
| 182 | let push_result = try_push(&clone_path); | ||
| 183 | cleanup(); | ||
| 184 | |||
| 185 | match push_result { | ||
| 186 | Ok(true) => { | ||
| 187 | TestResult::new(test_name, "GRASP-01", "Push authorized with matching state").pass() | ||
| 188 | } | ||
| 189 | Ok(false) => { | ||
| 190 | TestResult::new(test_name, "GRASP-01", "Push authorized with matching state").fail( | ||
| 191 | &format!( | ||
| 192 | "Push was rejected but should have been accepted. \ | ||
| 193 | The state event points to commit {} which matches the pushed commit.", | ||
| 194 | DETERMINISTIC_COMMIT_HASH | ||
| 195 | ), | ||
| 196 | ) | ||
| 197 | } | ||
| 198 | Err(e) => { | ||
| 199 | TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") | ||
| 200 | .fail(&format!("Push error: {}", e)) | ||
| 201 | } | ||
| 202 | } | ||
| 53 | } | 203 | } |
| 54 | 204 | ||
| 55 | /// Test that push is rejected when no state event exists | 205 | /// Test that push is rejected when no state event exists |