diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-27 15:55:13 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-27 15:55:13 +0000 |
| commit | 33a8870b6015fb989430edbbf5810a2d7d1a5247 (patch) | |
| tree | 48982bc839c2d5167eaccd963530b79acd6729ec | |
| parent | bf93f737aeec7b0ba6d007e867a55a8528615c23 (diff) | |
Task 3: Refactor maintainer push authorization test to fixture-first pattern
- Deprecated setup_repo_for_maintainer helper
- test_push_authorized_by_maintainer_state_only now creates own TestContext
- Uses FixtureKind::RepoState and FixtureKind::MaintainerState
- Uses git helpers from fixtures.rs (clone_repo, create_deterministic_commit_with_variant, try_push)
- Uses CommitVariant::Maintainer and MAINTAINER_DETERMINISTIC_COMMIT_HASH
- Test compiles and passes: cargo test --lib (25 passed, 0 failed)
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 30 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/push_authorization.rs | 247 |
2 files changed, 247 insertions, 30 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index 02e9810..571ab20 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs | |||
| @@ -1270,6 +1270,32 @@ pub async fn setup_repo_with_deterministic_commit( | |||
| 1270 | 1270 | ||
| 1271 | /// Set up a maintainer repository with deterministic commit (state only) | 1271 | /// Set up a maintainer repository with deterministic commit (state only) |
| 1272 | /// | 1272 | /// |
| 1273 | /// # Deprecated | ||
| 1274 | /// | ||
| 1275 | /// This function is deprecated in favor of the fixture-first pattern. | ||
| 1276 | /// Tests should create their own TestContext and use `FixtureKind::MaintainerState` | ||
| 1277 | /// directly, following the Generate → Send → Verify pattern. | ||
| 1278 | /// | ||
| 1279 | /// See `test_push_authorized_by_maintainer_state_only` in `push_authorization.rs` for | ||
| 1280 | /// an example of the fixture-first pattern. | ||
| 1281 | /// | ||
| 1282 | /// ## Migration Guide | ||
| 1283 | /// | ||
| 1284 | /// Instead of: | ||
| 1285 | /// ```ignore | ||
| 1286 | /// let setup = setup_repo_for_maintainer(client, git_data_dir, relay_domain).await?; | ||
| 1287 | /// ``` | ||
| 1288 | /// | ||
| 1289 | /// Use: | ||
| 1290 | /// ```ignore | ||
| 1291 | /// let ctx = TestContext::new(client); | ||
| 1292 | /// let _state_event = ctx.get_fixture(FixtureKind::RepoState).await?; | ||
| 1293 | /// let _maintainer_state = ctx.get_fixture(FixtureKind::MaintainerState).await?; | ||
| 1294 | /// // Then clone, create maintainer deterministic commit, and push inline | ||
| 1295 | /// ``` | ||
| 1296 | /// | ||
| 1297 | /// --- | ||
| 1298 | /// | ||
| 1273 | /// This performs all the common setup steps needed for maintainer push authorization tests: | 1299 | /// This performs all the common setup steps needed for maintainer push authorization tests: |
| 1274 | /// 1. Gets RepoState fixture (owner's repo announcement + state event with owner's deterministic commit) | 1300 | /// 1. Gets RepoState fixture (owner's repo announcement + state event with owner's deterministic commit) |
| 1275 | /// 2. Gets MaintainerState fixture (maintainer's state event ONLY - no announcement) | 1301 | /// 2. Gets MaintainerState fixture (maintainer's state event ONLY - no announcement) |
| @@ -1286,6 +1312,10 @@ pub async fn setup_repo_with_deterministic_commit( | |||
| 1286 | /// which publishes MaintainerAnnouncement separately. | 1312 | /// which publishes MaintainerAnnouncement separately. |
| 1287 | /// | 1313 | /// |
| 1288 | /// Returns RepoSetup which auto-cleans up the clone_path on drop | 1314 | /// Returns RepoSetup which auto-cleans up the clone_path on drop |
| 1315 | #[deprecated( | ||
| 1316 | since = "0.1.0", | ||
| 1317 | note = "Use fixture-first pattern with TestContext and FixtureKind::MaintainerState instead. See test_push_authorized_by_maintainer_state_only for example." | ||
| 1318 | )] | ||
| 1289 | pub async fn setup_repo_for_maintainer( | 1319 | pub async fn setup_repo_for_maintainer( |
| 1290 | client: &crate::AuditClient, | 1320 | client: &crate::AuditClient, |
| 1291 | git_data_dir: &Path, | 1321 | 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 7b7e9dc..1de3fe1 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs | |||
| @@ -17,9 +17,10 @@ | |||
| 17 | //! ``` | 17 | //! ``` |
| 18 | 18 | ||
| 19 | use crate::{ | 19 | use crate::{ |
| 20 | clone_repo, create_commit, create_deterministic_commit, setup_repo_for_maintainer, | 20 | clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, |
| 21 | setup_repo_for_recursive_maintainer, setup_repo_with_deterministic_commit, try_push, | 21 | setup_repo_for_recursive_maintainer, setup_repo_with_deterministic_commit, try_push, |
| 22 | AuditClient, FixtureKind, TestContext, TestResult, DETERMINISTIC_COMMIT_HASH, | 22 | AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, DETERMINISTIC_COMMIT_HASH, |
| 23 | MAINTAINER_DETERMINISTIC_COMMIT_HASH, | ||
| 23 | }; | 24 | }; |
| 24 | use nostr_sdk::prelude::*; | 25 | use nostr_sdk::prelude::*; |
| 25 | use std::fs; | 26 | use std::fs; |
| @@ -313,57 +314,243 @@ impl PushAuthorizationTests { | |||
| 313 | /// without publishing their own repo announcement. The maintainer is still | 314 | /// without publishing their own repo announcement. The maintainer is still |
| 314 | /// listed in the owner's announcement, so they're a valid maintainer. | 315 | /// listed in the owner's announcement, so they're a valid maintainer. |
| 315 | /// | 316 | /// |
| 317 | /// ## Fixture-First Pattern | ||
| 318 | /// | ||
| 319 | /// 1. **Generate**: Create TestContext, get RepoState (owner) and MaintainerState fixtures | ||
| 320 | /// 2. **Send**: Clone repo, create maintainer deterministic commit, push to relay | ||
| 321 | /// 3. **Verify**: Push should succeed because maintainer's state event authorizes this commit | ||
| 322 | /// | ||
| 316 | /// Scenario: | 323 | /// Scenario: |
| 317 | /// 1. Owner's repo announcement lists maintainer in maintainers tag | 324 | /// 1. Owner's repo announcement lists maintainer in maintainers tag |
| 318 | /// 2. Maintainer publishes ONLY a state event (no announcement) | 325 | /// 2. Maintainer publishes ONLY a state event (no announcement) |
| 319 | /// 3. setup_repo_for_maintainer() clones, creates maintainer commit, verifies hash, pushes | 326 | /// 3. Clone, create maintainer commit, verify hash, push |
| 320 | /// 4. The push should be ACCEPTED because maintainer's state event authorizes it | 327 | /// 4. The push should be ACCEPTED because maintainer's state event authorizes it |
| 321 | pub async fn test_push_authorized_by_maintainer_state_only( | 328 | pub async fn test_push_authorized_by_maintainer_state_only( |
| 322 | client: &AuditClient, | 329 | client: &AuditClient, |
| 323 | git_data_dir: &Path, | 330 | git_data_dir: &Path, |
| 324 | relay_domain: &str, | 331 | relay_domain: &str, |
| 325 | ) -> TestResult { | 332 | ) -> TestResult { |
| 333 | use std::process::Command; | ||
| 334 | |||
| 326 | let test_name = "test_push_authorized_by_maintainer_state_only"; | 335 | let test_name = "test_push_authorized_by_maintainer_state_only"; |
| 327 | 336 | ||
| 328 | // Use setup_repo_for_maintainer which publishes ONLY the state event, no announcement | 337 | // ============================================================ |
| 329 | match setup_repo_for_maintainer(client, git_data_dir, relay_domain).await { | 338 | // Step 1: GENERATE - Create TestContext and get fixtures |
| 330 | Ok(_setup) => { | 339 | // ============================================================ |
| 331 | // Push succeeded in setup - this means the relay accepted the push | 340 | let ctx = TestContext::new(client); |
| 332 | // authorized by the maintainer's state event alone | 341 | |
| 333 | TestResult::new( | 342 | // Get RepoState fixture (owner's repo announcement + state event) |
| 343 | let state_event = match ctx.get_fixture(FixtureKind::RepoState).await { | ||
| 344 | Ok(e) => e, | ||
| 345 | Err(e) => { | ||
| 346 | return TestResult::new( | ||
| 334 | test_name, | 347 | test_name, |
| 335 | "GRASP-01", | 348 | "GRASP-01", |
| 336 | "Push authorized by maintainer state event only (no announcement)", | 349 | "Push authorized by maintainer state event only (no announcement)", |
| 337 | ) | 350 | ) |
| 338 | .pass() | 351 | .fail(&format!("Failed to create RepoState fixture: {}", e)); |
| 339 | } | 352 | } |
| 353 | }; | ||
| 354 | |||
| 355 | // Get MaintainerState fixture (maintainer's state event ONLY - no announcement) | ||
| 356 | // This tests that state-only authorization works without a maintainer announcement | ||
| 357 | match ctx.get_fixture(FixtureKind::MaintainerState).await { | ||
| 358 | Ok(_) => {} | ||
| 340 | Err(e) => { | 359 | Err(e) => { |
| 341 | // Check if this was specifically a push rejection | 360 | return TestResult::new( |
| 342 | if e.contains("Failed to push") { | 361 | test_name, |
| 343 | TestResult::new( | 362 | "GRASP-01", |
| 344 | test_name, | 363 | "Push authorized by maintainer state event only (no announcement)", |
| 345 | "GRASP-01", | 364 | ) |
| 346 | "Push authorized by maintainer state event only (no announcement)", | 365 | .fail(&format!("Failed to create MaintainerState fixture: {}", e)); |
| 347 | ) | 366 | } |
| 348 | .fail(&format!( | 367 | }; |
| 349 | "Push was rejected but should have been accepted. \ | 368 | |
| 350 | The maintainer published a state event with a commit hash, \ | 369 | tokio::time::sleep(std::time::Duration::from_millis(200)).await; |
| 351 | and even without a separate announcement, the relay should \ | 370 | |
| 352 | authorize pushes matching this state event since the maintainer \ | 371 | // Extract repo_id and npub from owner's state event |
| 353 | is listed in the owner's announcement. \ | 372 | let repo_id = match state_event |
| 354 | Error: {}", | 373 | .tags |
| 355 | e | 374 | .iter() |
| 356 | )) | 375 | .find(|t| t.kind() == TagKind::d()) |
| 357 | } else { | 376 | .and_then(|t| t.content()) |
| 358 | // Some other error during setup | 377 | { |
| 359 | TestResult::new( | 378 | Some(id) => id.to_string(), |
| 379 | None => { | ||
| 380 | return TestResult::new( | ||
| 381 | test_name, | ||
| 382 | "GRASP-01", | ||
| 383 | "Push authorized by maintainer state event only (no announcement)", | ||
| 384 | ) | ||
| 385 | .fail("Missing repo_id in state event"); | ||
| 386 | } | ||
| 387 | }; | ||
| 388 | |||
| 389 | let npub = match state_event.pubkey.to_bech32() { | ||
| 390 | Ok(n) => n, | ||
| 391 | Err(e) => { | ||
| 392 | return TestResult::new( | ||
| 393 | test_name, | ||
| 394 | "GRASP-01", | ||
| 395 | "Push authorized by maintainer state event only (no announcement)", | ||
| 396 | ) | ||
| 397 | .fail(&format!("Failed to convert pubkey to bech32: {}", e)); | ||
| 398 | } | ||
| 399 | }; | ||
| 400 | |||
| 401 | // Verify repo exists on disk | ||
| 402 | let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id)); | ||
| 403 | if !repo_path.exists() { | ||
| 404 | return TestResult::new( | ||
| 405 | test_name, | ||
| 406 | "GRASP-01", | ||
| 407 | "Push authorized by maintainer state event only (no announcement)", | ||
| 408 | ) | ||
| 409 | .fail(&format!("Repo not found: {}", repo_path.display())); | ||
| 410 | } | ||
| 411 | |||
| 412 | // ============================================================ | ||
| 413 | // Step 2: SEND - Clone, create maintainer commit, push | ||
| 414 | // ============================================================ | ||
| 415 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { | ||
| 416 | Ok(p) => p, | ||
| 417 | Err(e) => { | ||
| 418 | return TestResult::new( | ||
| 419 | test_name, | ||
| 420 | "GRASP-01", | ||
| 421 | "Push authorized by maintainer state event only (no announcement)", | ||
| 422 | ) | ||
| 423 | .fail(&e); | ||
| 424 | } | ||
| 425 | }; | ||
| 426 | let cleanup = || { | ||
| 427 | let _ = fs::remove_dir_all(&clone_path); | ||
| 428 | }; | ||
| 429 | |||
| 430 | // Create maintainer deterministic commit | ||
| 431 | let commit_hash = | ||
| 432 | match create_deterministic_commit_with_variant(&clone_path, CommitVariant::Maintainer) { | ||
| 433 | Ok(h) => h, | ||
| 434 | Err(e) => { | ||
| 435 | cleanup(); | ||
| 436 | return TestResult::new( | ||
| 360 | test_name, | 437 | test_name, |
| 361 | "GRASP-01", | 438 | "GRASP-01", |
| 362 | "Push authorized by maintainer state event only (no announcement)", | 439 | "Push authorized by maintainer state event only (no announcement)", |
| 363 | ) | 440 | ) |
| 364 | .fail(&format!("Setup failed: {}", e)) | 441 | .fail(&format!("Failed to create maintainer commit: {}", e)); |
| 365 | } | 442 | } |
| 443 | }; | ||
| 444 | |||
| 445 | // Verify commit hash matches expected | ||
| 446 | if commit_hash != MAINTAINER_DETERMINISTIC_COMMIT_HASH { | ||
| 447 | cleanup(); | ||
| 448 | return TestResult::new( | ||
| 449 | test_name, | ||
| 450 | "GRASP-01", | ||
| 451 | "Push authorized by maintainer state event only (no announcement)", | ||
| 452 | ) | ||
| 453 | .fail(&format!( | ||
| 454 | "Maintainer commit hash mismatch: got {}, expected {}", | ||
| 455 | commit_hash, MAINTAINER_DETERMINISTIC_COMMIT_HASH | ||
| 456 | )); | ||
| 457 | } | ||
| 458 | |||
| 459 | // Create main branch | ||
| 460 | let branch_output = Command::new("git") | ||
| 461 | .args(["branch", "main"]) | ||
| 462 | .current_dir(&clone_path) | ||
| 463 | .output(); | ||
| 464 | |||
| 465 | match branch_output { | ||
| 466 | Err(e) => { | ||
| 467 | cleanup(); | ||
| 468 | return TestResult::new( | ||
| 469 | test_name, | ||
| 470 | "GRASP-01", | ||
| 471 | "Push authorized by maintainer state event only (no announcement)", | ||
| 472 | ) | ||
| 473 | .fail(&format!("Failed to create main branch: {}", e)); | ||
| 474 | } | ||
| 475 | Ok(output) if !output.status.success() => { | ||
| 476 | cleanup(); | ||
| 477 | return TestResult::new( | ||
| 478 | test_name, | ||
| 479 | "GRASP-01", | ||
| 480 | "Push authorized by maintainer state event only (no announcement)", | ||
| 481 | ) | ||
| 482 | .fail(&format!( | ||
| 483 | "Failed to create main branch: {}", | ||
| 484 | String::from_utf8_lossy(&output.stderr) | ||
| 485 | )); | ||
| 486 | } | ||
| 487 | _ => {} | ||
| 488 | } | ||
| 489 | |||
| 490 | // Checkout main branch | ||
| 491 | let checkout_output = Command::new("git") | ||
| 492 | .args(["checkout", "main"]) | ||
| 493 | .current_dir(&clone_path) | ||
| 494 | .output(); | ||
| 495 | |||
| 496 | match checkout_output { | ||
| 497 | Err(e) => { | ||
| 498 | cleanup(); | ||
| 499 | return TestResult::new( | ||
| 500 | test_name, | ||
| 501 | "GRASP-01", | ||
| 502 | "Push authorized by maintainer state event only (no announcement)", | ||
| 503 | ) | ||
| 504 | .fail(&format!("Failed to checkout main branch: {}", e)); | ||
| 366 | } | 505 | } |
| 506 | Ok(output) if !output.status.success() => { | ||
| 507 | cleanup(); | ||
| 508 | return TestResult::new( | ||
| 509 | test_name, | ||
| 510 | "GRASP-01", | ||
| 511 | "Push authorized by maintainer state event only (no announcement)", | ||
| 512 | ) | ||
| 513 | .fail(&format!( | ||
| 514 | "Failed to checkout main branch: {}", | ||
| 515 | String::from_utf8_lossy(&output.stderr) | ||
| 516 | )); | ||
| 517 | } | ||
| 518 | _ => {} | ||
| 519 | } | ||
| 520 | |||
| 521 | // ============================================================ | ||
| 522 | // Step 3: VERIFY - Push should succeed because maintainer's | ||
| 523 | // state event authorizes this commit | ||
| 524 | // ============================================================ | ||
| 525 | let push_result = try_push(&clone_path); | ||
| 526 | cleanup(); | ||
| 527 | |||
| 528 | match push_result { | ||
| 529 | Ok(true) => TestResult::new( | ||
| 530 | test_name, | ||
| 531 | "GRASP-01", | ||
| 532 | "Push authorized by maintainer state event only (no announcement)", | ||
| 533 | ) | ||
| 534 | .pass(), | ||
| 535 | Ok(false) => TestResult::new( | ||
| 536 | test_name, | ||
| 537 | "GRASP-01", | ||
| 538 | "Push authorized by maintainer state event only (no announcement)", | ||
| 539 | ) | ||
| 540 | .fail(&format!( | ||
| 541 | "Push was rejected but should have been accepted. \ | ||
| 542 | The maintainer published a state event with commit {}, \ | ||
| 543 | and even without a separate announcement, the relay should \ | ||
| 544 | authorize pushes matching this state event since the maintainer \ | ||
| 545 | is listed in the owner's announcement.", | ||
| 546 | MAINTAINER_DETERMINISTIC_COMMIT_HASH | ||
| 547 | )), | ||
| 548 | Err(e) => TestResult::new( | ||
| 549 | test_name, | ||
| 550 | "GRASP-01", | ||
| 551 | "Push authorized by maintainer state event only (no announcement)", | ||
| 552 | ) | ||
| 553 | .fail(&format!("Push error: {}", e)), | ||
| 367 | } | 554 | } |
| 368 | } | 555 | } |
| 369 | 556 | ||