diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-28 11:22:40 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-28 11:26:57 +0000 |
| commit | e5b8854d5065cda8601546fc888e2ef1e00cc166 (patch) | |
| tree | 839354b19ee8d13a88bfcffa099ded5dce63696e /grasp-audit/src/specs/grasp01/push_authorization.rs | |
| parent | 744094c61d6e65892bcdb5a29b90b845ce87559f (diff) | |
audit: fix rejected push wrong commit test
Diffstat (limited to 'grasp-audit/src/specs/grasp01/push_authorization.rs')
| -rw-r--r-- | grasp-audit/src/specs/grasp01/push_authorization.rs | 213 |
1 files changed, 71 insertions, 142 deletions
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index 69664d6..0e30238 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs | |||
| @@ -24,7 +24,6 @@ use crate::{ | |||
| 24 | }; | 24 | }; |
| 25 | use nostr_sdk::prelude::*; | 25 | use nostr_sdk::prelude::*; |
| 26 | use std::fs; | 26 | use std::fs; |
| 27 | use std::path::Path; | ||
| 28 | 27 | ||
| 29 | /// Test suite for Push Authorization operations | 28 | /// Test suite for Push Authorization operations |
| 30 | pub struct PushAuthorizationTests; | 29 | pub struct PushAuthorizationTests; |
| @@ -37,14 +36,60 @@ impl PushAuthorizationTests { | |||
| 37 | ) -> crate::AuditResult { | 36 | ) -> crate::AuditResult { |
| 38 | let mut results = crate::AuditResult::new("GRASP-01 Push Authorization Tests"); | 37 | let mut results = crate::AuditResult::new("GRASP-01 Push Authorization Tests"); |
| 39 | 38 | ||
| 40 | results.add(Self::test_push_authorized_by_owner_state(client, relay_domain).await); | ||
| 41 | results.add(Self::test_push_rejected_without_state_event(client, relay_domain).await); | 39 | results.add(Self::test_push_rejected_without_state_event(client, relay_domain).await); |
| 40 | results.add(Self::test_push_authorized_by_owner_state(client, relay_domain).await); | ||
| 42 | results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await); | 41 | results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await); |
| 43 | results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await); | 42 | results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await); |
| 44 | 43 | ||
| 45 | results | 44 | results |
| 46 | } | 45 | } |
| 47 | 46 | ||
| 47 | /// Test that push is rejected when no state event exists | ||
| 48 | pub async fn test_push_rejected_without_state_event( | ||
| 49 | client: &AuditClient, | ||
| 50 | relay_domain: &str, | ||
| 51 | ) -> TestResult { | ||
| 52 | let test_name = "test_push_rejected_without_state_event"; | ||
| 53 | let ctx = TestContext::new(client); | ||
| 54 | |||
| 55 | // Create repository (no state event) | ||
| 56 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | ||
| 57 | Ok(r) => r, | ||
| 58 | Err(e) => { | ||
| 59 | return TestResult::new(test_name, "GRASP-01", "Push rejected without state event") | ||
| 60 | .fail(&format!("Failed to create repo: {}", e)) | ||
| 61 | } | ||
| 62 | }; | ||
| 63 | |||
| 64 | tokio::time::sleep(std::time::Duration::from_millis(200)).await; | ||
| 65 | |||
| 66 | let repo_id = repo.tags.iter().find(|t| t.kind() == TagKind::d()) | ||
| 67 | .and_then(|t| t.content()).unwrap().to_string(); | ||
| 68 | let npub = repo.pubkey.to_bech32().unwrap(); | ||
| 69 | |||
| 70 | // Clone and create commit | ||
| 71 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { | ||
| 72 | Ok(p) => p, | ||
| 73 | Err(e) => return TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e), | ||
| 74 | }; | ||
| 75 | let cleanup = || { let _ = fs::remove_dir_all(&clone_path); }; | ||
| 76 | |||
| 77 | if let Err(e) = create_commit(&clone_path, "Unauthorized commit") { | ||
| 78 | cleanup(); | ||
| 79 | return TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e); | ||
| 80 | } | ||
| 81 | |||
| 82 | // Do NOT publish state event - push should be rejected | ||
| 83 | let push_result = try_push(&clone_path); | ||
| 84 | cleanup(); | ||
| 85 | |||
| 86 | match push_result { | ||
| 87 | Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").pass(), | ||
| 88 | Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail("Push accepted but should be rejected"), | ||
| 89 | Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e), | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 48 | /// Test that push is authorized when state event matches the commit | 93 | /// Test that push is authorized when state event matches the commit |
| 49 | /// | 94 | /// |
| 50 | /// GRASP-01: "MUST accept pushes via this service that match the latest | 95 | /// GRASP-01: "MUST accept pushes via this service that match the latest |
| @@ -210,52 +255,6 @@ impl PushAuthorizationTests { | |||
| 210 | } | 255 | } |
| 211 | } | 256 | } |
| 212 | 257 | ||
| 213 | /// Test that push is rejected when no state event exists | ||
| 214 | pub async fn test_push_rejected_without_state_event( | ||
| 215 | client: &AuditClient, | ||
| 216 | relay_domain: &str, | ||
| 217 | ) -> TestResult { | ||
| 218 | let test_name = "test_push_rejected_without_state_event"; | ||
| 219 | let ctx = TestContext::new(client); | ||
| 220 | |||
| 221 | // Create repository (no state event) | ||
| 222 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | ||
| 223 | Ok(r) => r, | ||
| 224 | Err(e) => { | ||
| 225 | return TestResult::new(test_name, "GRASP-01", "Push rejected without state event") | ||
| 226 | .fail(&format!("Failed to create repo: {}", e)) | ||
| 227 | } | ||
| 228 | }; | ||
| 229 | |||
| 230 | tokio::time::sleep(std::time::Duration::from_millis(200)).await; | ||
| 231 | |||
| 232 | let repo_id = repo.tags.iter().find(|t| t.kind() == TagKind::d()) | ||
| 233 | .and_then(|t| t.content()).unwrap().to_string(); | ||
| 234 | let npub = repo.pubkey.to_bech32().unwrap(); | ||
| 235 | |||
| 236 | // Clone and create commit | ||
| 237 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { | ||
| 238 | Ok(p) => p, | ||
| 239 | Err(e) => return TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e), | ||
| 240 | }; | ||
| 241 | let cleanup = || { let _ = fs::remove_dir_all(&clone_path); }; | ||
| 242 | |||
| 243 | if let Err(e) = create_commit(&clone_path, "Unauthorized commit") { | ||
| 244 | cleanup(); | ||
| 245 | return TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e); | ||
| 246 | } | ||
| 247 | |||
| 248 | // Do NOT publish state event - push should be rejected | ||
| 249 | let push_result = try_push(&clone_path); | ||
| 250 | cleanup(); | ||
| 251 | |||
| 252 | match push_result { | ||
| 253 | Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").pass(), | ||
| 254 | Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail("Push accepted but should be rejected"), | ||
| 255 | Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e), | ||
| 256 | } | ||
| 257 | } | ||
| 258 | |||
| 259 | /// Test that push is rejected when commit doesn't match state event | 258 | /// Test that push is rejected when commit doesn't match state event |
| 260 | /// | 259 | /// |
| 261 | /// GRASP-01: "MUST accept pushes via this service that match the latest repo state announcement" | 260 | /// GRASP-01: "MUST accept pushes via this service that match the latest repo state announcement" |
| @@ -264,10 +263,14 @@ impl PushAuthorizationTests { | |||
| 264 | /// ## Fixture-First Pattern | 263 | /// ## Fixture-First Pattern |
| 265 | /// | 264 | /// |
| 266 | /// 1. **Generate**: Create TestContext and get RepoState fixture | 265 | /// 1. **Generate**: Create TestContext and get RepoState fixture |
| 267 | /// (repo announcement + state event pointing to deterministic commit) | 266 | /// (repo announcement + state event pointing to DETERMINISTIC_COMMIT_HASH) |
| 268 | /// 2. **Send**: Clone repo, create deterministic commit, push (establishes state on relay) | 267 | /// 2. **Send**: Clone repo, create WRONG deterministic commit (Maintainer variant), |
| 269 | /// 3. **Test**: Create a NEW commit locally, try to push | 268 | /// try to push |
| 270 | /// 4. **Verify**: Push should be rejected because new commit doesn't match state event | 269 | /// 3. **Verify**: Push should be rejected because the commit doesn't match state event |
| 270 | /// | ||
| 271 | /// Note: This test directly pushes the wrong commit instead of first establishing | ||
| 272 | /// state on the relay. The state event already authorizes DETERMINISTIC_COMMIT_HASH, | ||
| 273 | /// but we try to push MAINTAINER_DETERMINISTIC_COMMIT_HASH which should be rejected. | ||
| 271 | pub async fn test_push_rejected_wrong_commit( | 274 | pub async fn test_push_rejected_wrong_commit( |
| 272 | client: &AuditClient, | 275 | client: &AuditClient, |
| 273 | relay_domain: &str, | 276 | relay_domain: &str, |
| @@ -278,6 +281,7 @@ impl PushAuthorizationTests { | |||
| 278 | 281 | ||
| 279 | // ============================================================ | 282 | // ============================================================ |
| 280 | // Step 1: GENERATE - Create TestContext and get RepoState fixture | 283 | // Step 1: GENERATE - Create TestContext and get RepoState fixture |
| 284 | // The state event points to DETERMINISTIC_COMMIT_HASH | ||
| 281 | // ============================================================ | 285 | // ============================================================ |
| 282 | let ctx = TestContext::new(client); | 286 | let ctx = TestContext::new(client); |
| 283 | 287 | ||
| @@ -314,8 +318,8 @@ impl PushAuthorizationTests { | |||
| 314 | }; | 318 | }; |
| 315 | 319 | ||
| 316 | // ============================================================ | 320 | // ============================================================ |
| 317 | // Step 2: SEND - Clone repo, create deterministic commit, push | 321 | // Step 2: SEND - Clone repo and create an unauthorized commit |
| 318 | // (establishes the state on the relay) | 322 | // Any commit with a hash different from what's in the state event will work |
| 319 | // ============================================================ | 323 | // ============================================================ |
| 320 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { | 324 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { |
| 321 | Ok(p) => p, | 325 | Ok(p) => p, |
| @@ -330,29 +334,9 @@ impl PushAuthorizationTests { | |||
| 330 | let _ = fs::remove_dir_all(&clone_path); | 334 | let _ = fs::remove_dir_all(&clone_path); |
| 331 | }; | 335 | }; |
| 332 | 336 | ||
| 333 | // Create deterministic commit locally | 337 | // Create/checkout main branch |
| 334 | let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") { | ||
| 335 | Ok(h) => h, | ||
| 336 | Err(e) => { | ||
| 337 | cleanup(); | ||
| 338 | return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") | ||
| 339 | .fail(&format!("Failed to create deterministic commit: {}", e)); | ||
| 340 | } | ||
| 341 | }; | ||
| 342 | |||
| 343 | // Verify commit hash matches expected | ||
| 344 | if commit_hash != DETERMINISTIC_COMMIT_HASH { | ||
| 345 | cleanup(); | ||
| 346 | return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") | ||
| 347 | .fail(&format!( | ||
| 348 | "Commit hash mismatch: got {}, expected {}", | ||
| 349 | commit_hash, DETERMINISTIC_COMMIT_HASH | ||
| 350 | )); | ||
| 351 | } | ||
| 352 | |||
| 353 | // Create main branch pointing to our deterministic commit | ||
| 354 | let branch_output = Command::new("git") | 338 | let branch_output = Command::new("git") |
| 355 | .args(["branch", "main"]) | 339 | .args(["checkout", "-B", "main"]) |
| 356 | .current_dir(&clone_path) | 340 | .current_dir(&clone_path) |
| 357 | .output(); | 341 | .output(); |
| 358 | 342 | ||
| @@ -360,82 +344,30 @@ impl PushAuthorizationTests { | |||
| 360 | Err(e) => { | 344 | Err(e) => { |
| 361 | cleanup(); | 345 | cleanup(); |
| 362 | return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") | 346 | return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") |
| 363 | .fail(&format!("Failed to create main branch: {}", e)); | 347 | .fail(&format!("Failed to create/checkout main branch: {}", e)); |
| 364 | } | 348 | } |
| 365 | Ok(output) if !output.status.success() => { | 349 | Ok(output) if !output.status.success() => { |
| 366 | cleanup(); | 350 | cleanup(); |
| 367 | return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") | 351 | return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") |
| 368 | .fail(&format!( | 352 | .fail(&format!( |
| 369 | "Failed to create main branch: {}", | 353 | "Failed to create/checkout main branch: {}", |
| 370 | String::from_utf8_lossy(&output.stderr) | 354 | String::from_utf8_lossy(&output.stderr) |
| 371 | )); | 355 | )); |
| 372 | } | 356 | } |
| 373 | _ => {} | 357 | _ => {} |
| 374 | } | 358 | } |
| 375 | 359 | ||
| 376 | // Checkout main branch | 360 | // Create a commit that is NOT in the state event |
| 377 | let checkout_output = Command::new("git") | 361 | // Any commit hash different from what's authorized in the state event will work |
| 378 | .args(["checkout", "main"]) | 362 | if let Err(e) = create_commit(&clone_path, "Unauthorized commit - should be rejected") { |
| 379 | .current_dir(&clone_path) | 363 | cleanup(); |
| 380 | .output(); | 364 | return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") |
| 381 | 365 | .fail(&format!("Failed to create wrong commit: {}", e)); | |
| 382 | match checkout_output { | ||
| 383 | Err(e) => { | ||
| 384 | cleanup(); | ||
| 385 | return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") | ||
| 386 | .fail(&format!("Failed to checkout main branch: {}", e)); | ||
| 387 | } | ||
| 388 | Ok(output) if !output.status.success() => { | ||
| 389 | cleanup(); | ||
| 390 | return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") | ||
| 391 | .fail(&format!( | ||
| 392 | "Failed to checkout main branch: {}", | ||
| 393 | String::from_utf8_lossy(&output.stderr) | ||
| 394 | )); | ||
| 395 | } | ||
| 396 | _ => {} | ||
| 397 | } | ||
| 398 | |||
| 399 | // Push the deterministic commit to establish state on relay | ||
| 400 | let push_output = Command::new("git") | ||
| 401 | .args(["push", "origin", "main"]) | ||
| 402 | .current_dir(&clone_path) | ||
| 403 | .env("GIT_TERMINAL_PROMPT", "0") | ||
| 404 | .output(); | ||
| 405 | |||
| 406 | match push_output { | ||
| 407 | Err(e) => { | ||
| 408 | cleanup(); | ||
| 409 | return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") | ||
| 410 | .fail(&format!("Failed to push initial commit: {}", e)); | ||
| 411 | } | ||
| 412 | Ok(output) if !output.status.success() => { | ||
| 413 | cleanup(); | ||
| 414 | return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") | ||
| 415 | .fail(&format!( | ||
| 416 | "Failed to push initial commit: {}", | ||
| 417 | String::from_utf8_lossy(&output.stderr) | ||
| 418 | )); | ||
| 419 | } | ||
| 420 | _ => {} | ||
| 421 | } | 366 | } |
| 422 | 367 | ||
| 423 | // ============================================================ | 368 | // ============================================================ |
| 424 | // Step 3: TEST - Create a NEW commit that is NOT announced | 369 | // Step 3: VERIFY - Push should be rejected because the commit |
| 425 | // in any state event | 370 | // doesn't match the state event |
| 426 | // ============================================================ | ||
| 427 | let new_commit = match create_commit(&clone_path, "Unauthorized commit") { | ||
| 428 | Ok(h) => h, | ||
| 429 | Err(e) => { | ||
| 430 | cleanup(); | ||
| 431 | return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") | ||
| 432 | .fail(&format!("Failed to create commit: {}", e)); | ||
| 433 | } | ||
| 434 | }; | ||
| 435 | |||
| 436 | // ============================================================ | ||
| 437 | // Step 4: VERIFY - Push should be rejected because new commit | ||
| 438 | // doesn't match state event | ||
| 439 | // ============================================================ | 371 | // ============================================================ |
| 440 | let push_result = try_push(&clone_path); | 372 | let push_result = try_push(&clone_path); |
| 441 | cleanup(); | 373 | cleanup(); |
| @@ -443,10 +375,7 @@ impl PushAuthorizationTests { | |||
| 443 | match push_result { | 375 | match push_result { |
| 444 | Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").pass(), | 376 | Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").pass(), |
| 445 | Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") | 377 | Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") |
| 446 | .fail(&format!( | 378 | .fail("Push accepted but should be rejected. The pushed commit is not in the state event."), |
| 447 | "Push accepted but should be rejected. State event points to {}, but pushed {}", | ||
| 448 | DETERMINISTIC_COMMIT_HASH, new_commit | ||
| 449 | )), | ||
| 450 | Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").fail(&e), | 379 | Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").fail(&e), |
| 451 | } | 380 | } |
| 452 | } | 381 | } |