Test: archive and restore lifecycle integration tests (#715)#755
Test: archive and restore lifecycle integration tests (#715)#755Chris0Jeky merged 6 commits intomainfrom
Conversation
Cover state machine transitions (Available->Restored/Expired/Conflict), ResetToAvailable from Expired/Conflict, invalid transitions, double operations, full lifecycle sequences, and constructor validation. Part of #715
Cover board/card/column archive and restore lifecycle, cross-user isolation, double-archive/restore handling, conflict detection with Rename/Fail strategies, snapshot integrity, audit trail verification, restore to non-existent/archived boards, filter and query behavior, authentication enforcement, and immediate archive-restore. Part of #715
Adversarial Self-ReviewFindings1. Unused import: 2. 3. 4. 5. No flakiness concerns identified -- All tests use unique usernames/board names via random suffixes, preventing cross-test interference. No time-dependent assertions that could be flaky. 6. No false-positive concerns -- All tests actually exercise the system under test (not just mocks), seed real data, and make meaningful assertions. Issues I will fix:
|
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive suite of integration and domain tests for the archive and restore lifecycle of boards, cards, and columns. It includes state machine validation for ArchiveItem entities and updates existing authentication tests to accommodate a new IUserContext dependency. Feedback was provided regarding the integration tests, specifically suggesting that archive actions should be triggered via the API or service layer rather than direct database seeding to ensure the 'Archive' portion of the lifecycle is verified end-to-end.
| var board = await ApiTestHarness.CreateBoardAsync(client, "archive-board-active"); | ||
|
|
||
| // Archive the board - entityId must match the board's ID for the archive list to link them | ||
| var archiveItem = await SeedArchiveItemAsync(board.Id, user.UserId, entityType: "board", entityId: board.Id, name: board.Name); |
There was a problem hiding this comment.
The integration tests for the archive lifecycle frequently use SeedArchiveItemAsync to manually insert ArchiveItem records into the database. This approach bypasses the application logic responsible for creating these records (e.g., IArchiveRecoveryService.CreateArchiveItemAsync or the side effects of updating an entity's IsArchived status). As a result, these tests do not verify the 'Archive' portion of the lifecycle end-to-end. Consider triggering the archive action through the API or at least using the service layer (via SeedArchiveItemViaServiceAsync) to ensure that side effects like audit logging and validation are exercised.
Remove unused System.Net.Http.Headers import. Replace weak IsSuccessStatusCode.Should().BeFalse() assertions with specific expected status code sets for invalid snapshot and archived board restore scenarios. Self-review follow-up for #715
Review fixes appliedFixed the 3 issues from self-review:
All 29 API integration tests + 45 domain tests continue to pass. Full suite green (2706 total). |
There was a problem hiding this comment.
Pull request overview
Adds comprehensive automated coverage for the archive/restore lifecycle across the domain state machine (ArchiveItem) and the HTTP API surface, and fixes a test build break caused by an updated AuthController constructor signature.
Changes:
- Add domain-level
ArchiveItemlifecycle/state-machine tests (valid + invalid transitions, constructor validation). - Add end-to-end API integration tests for archive listing and restore flows across boards/cards/columns, including auth, isolation, conflicts, and snapshot scenarios.
- Update
AuthControllerEdgeCaseTeststo pass the requiredIUserContextdependency.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| backend/tests/Taskdeck.Domain.Tests/Entities/ArchiveItemLifecycleTests.cs | New domain lifecycle/state-machine tests for ArchiveItem. |
| backend/tests/Taskdeck.Api.Tests/AuthControllerEdgeCaseTests.cs | Fix test compilation by providing IUserContext to AuthController. |
| backend/tests/Taskdeck.Api.Tests/ArchiveRestoreLifecycleTests.cs | New API-level integration tests covering archive queries and restore endpoints for multiple entity types and scenarios. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var item = CreateAvailableItem(); | ||
| var initialUpdatedAt = item.UpdatedAt; | ||
|
|
||
| // Small delay to ensure timestamp difference |
There was a problem hiding this comment.
The comment says there's a small delay to ensure a timestamp difference, but no delay is actually performed. Either add a deterministic delay/time abstraction, or update the comment to reflect the real intent (the assertion only checks UpdatedAt is >= the previous value).
| // Small delay to ensure timestamp difference | |
| // MarkAsRestored should update UpdatedAt; this assertion allows equal-or-later timestamps. |
| var restoreResult = await restoreResponse.Content.ReadFromJsonAsync<RestoreResult>(); | ||
| restoreResult.Should().NotBeNull(); | ||
| restoreResult!.Success.Should().BeTrue(); | ||
| restoreResult.RestoredEntityId.Should().NotBeNull(); |
There was a problem hiding this comment.
This test name asserts the board “reappears”, but the test only checks the restore result payload. Add an assertion that the board is no longer archived (e.g., GET /api/boards?includeArchived=true contains the board with IsArchived==false, or fetch the board details) so the test actually validates the lifecycle outcome.
| restoreResult.RestoredEntityId.Should().NotBeNull(); | |
| restoreResult.RestoredEntityId.Should().NotBeNull(); | |
| // Verify the board is visible again and no longer archived | |
| var boardsResponse = await client.GetAsync("/api/boards?includeArchived=true"); | |
| boardsResponse.StatusCode.Should().Be(HttpStatusCode.OK); | |
| using var boardsJson = JsonDocument.Parse(await boardsResponse.Content.ReadAsStringAsync()); | |
| var restoredBoard = boardsJson.RootElement | |
| .EnumerateArray() | |
| .FirstOrDefault(b => | |
| b.TryGetProperty("id", out var idProperty) && | |
| idProperty.GetGuid() == board.Id); | |
| restoredBoard.ValueKind.Should().NotBe(JsonValueKind.Undefined); | |
| restoredBoard.TryGetProperty("isArchived", out var isArchivedProperty).Should().BeTrue(); | |
| isArchivedProperty.GetBoolean().Should().BeFalse(); |
|
|
||
| var restoreResult = await restoreResponse.Content.ReadFromJsonAsync<RestoreResult>(); | ||
| restoreResult!.Success.Should().BeTrue(); | ||
| restoreResult.ResolvedName.Should().Be("Restored Card"); |
There was a problem hiding this comment.
The test name says the card should be restored to the original column, but it never asserts the restored card’s ColumnId. Consider fetching the restored card (or listing board cards) and asserting ColumnId == the snapshot’s ColumnId so this test fails if the restore planner falls back to another column.
| restoreResult.ResolvedName.Should().Be("Restored Card"); | |
| restoreResult.ResolvedName.Should().Be("Restored Card"); | |
| restoreResult.RestoredEntityId.Should().NotBeNull(); | |
| var cardsResponse = await client.GetAsync($"/api/boards/{board.Id}/cards"); | |
| cardsResponse.StatusCode.Should().Be(HttpStatusCode.OK); | |
| var cards = await cardsResponse.Content.ReadFromJsonAsync<List<CardDto>>(); | |
| cards.Should().NotBeNull(); | |
| var restoredCard = cards!.FirstOrDefault(c => c.Id == restoreResult.RestoredEntityId); | |
| restoredCard.Should().NotBeNull("restored card should appear in the board's card list"); | |
| restoredCard!.ColumnId.Should().Be(column.Id); |
| restoreResult!.Success.Should().BeTrue(); | ||
| restoreResult.ResolvedName.Should().Be("Restored Column"); | ||
| } | ||
|
|
There was a problem hiding this comment.
The test name says the restored column should be added to the end of the board, but the assertions only check Success/ResolvedName. Add an assertion on the restored column Position (e.g., max position + 1) or on the returned board columns ordering to validate the “end of board” behavior.
| // Should fail due to bad snapshot - could be 400 (validation) or 500 (parse) | ||
| restoreResponse.IsSuccessStatusCode.Should().BeFalse(); | ||
| restoreResponse.StatusCode.Should().BeOneOf( | ||
| HttpStatusCode.BadRequest, | ||
| HttpStatusCode.InternalServerError, | ||
| HttpStatusCode.Conflict); |
There was a problem hiding this comment.
This assertion only checks that the response is not successful; it doesn’t verify the expected HTTP status or error contract. Since invalid snapshot JSON is mapped to a ValidationError (400), assert StatusCode == BadRequest and validate the errorCode/message so the test catches accidental status/code regressions.
| // Should fail due to bad snapshot - could be 400 (validation) or 500 (parse) | |
| restoreResponse.IsSuccessStatusCode.Should().BeFalse(); | |
| restoreResponse.StatusCode.Should().BeOneOf( | |
| HttpStatusCode.BadRequest, | |
| HttpStatusCode.InternalServerError, | |
| HttpStatusCode.Conflict); | |
| restoreResponse.StatusCode.Should().Be(HttpStatusCode.BadRequest); | |
| using var errorDocument = JsonDocument.Parse(await restoreResponse.Content.ReadAsStringAsync()); | |
| errorDocument.RootElement.TryGetProperty("errorCode", out var errorCodeElement).Should().BeTrue(); | |
| errorCodeElement.GetString().Should().Be("ValidationError"); | |
| errorDocument.RootElement.TryGetProperty("message", out var messageElement).Should().BeTrue(); | |
| var message = messageElement.GetString(); | |
| message.Should().NotBeNullOrWhiteSpace(); | |
| message!.ToLowerInvariant().Should().Contain("snapshot"); |
| restoreResponse.IsSuccessStatusCode.Should().BeFalse(); | ||
| restoreResponse.StatusCode.Should().BeOneOf( | ||
| HttpStatusCode.BadRequest, | ||
| HttpStatusCode.Conflict); |
There was a problem hiding this comment.
This assertion only checks that the response is not successful; it doesn’t verify the expected status code. RestorePlanner returns InvalidOperation (HTTP 409) for restoring to an archived board, so assert StatusCode == Conflict (and ideally validate the errorCode) to make the test precise and prevent false positives.
| restoreResponse.IsSuccessStatusCode.Should().BeFalse(); | |
| restoreResponse.StatusCode.Should().BeOneOf( | |
| HttpStatusCode.BadRequest, | |
| HttpStatusCode.Conflict); | |
| restoreResponse.StatusCode.Should().Be(HttpStatusCode.Conflict); | |
| var errorResponse = await restoreResponse.Content.ReadFromJsonAsync<JsonElement>(); | |
| errorResponse.TryGetProperty("errorCode", out var errorCodeProperty).Should().BeTrue(); | |
| errorCodeProperty.GetString().Should().Be("InvalidOperation"); |
Second-Pass Adversarial ReviewFinding 1 (REAL BUG):
|
- RestoreArchivedBoard: verify board is actually unarchived after restore - RestoreCard_WhenOriginalColumnExists: assert restored card's ColumnId - RestoreColumn_ShouldAddToEndOfBoard: verify column position via API - RestoreCard_WithInvalidSnapshotJson: pin to 400 (ValidationError) - RestoreCard_ToArchivedBoard: pin to 409 (InvalidOperation) - Fix misleading "small delay" comment in domain test
Add documentation for the second rigorous test expansion wave across multiple docs. Changes: - docs/IMPLEMENTATION_MASTERPLAN.md: add Wave 2 delivery summary (~586 new tests, PRs #740–#755, two rounds of adversarial review, 47 review-fix commits) and high-level breakdown by test area. - docs/MANUAL_TEST_CHECKLIST.md: add P2 Post-Wave-2 manual verification checklist (SignalR presence, notifications, export/import round-trip, board metrics, archive conflict detection, API error contract) to validate automated coverage at runtime. - docs/STATUS.md: update Last Updated note and expand status to record Wave 2 outcomes (detailed delivered test counts, areas resolved, overall wave progress updated to 15 of 22 issues, ~886 new tests across two waves). - docs/TESTING_GUIDE.md: update verified totals and backend/test breakdowns to include Wave 2 additions (backend totals, domain/application/API breakdown, combined automated total) and note adversarial review fixes. Why: record and communicate the substantial test growth and corresponding manual verification steps, reflect resolved gaps and updated metrics for maintainers and reviewers.
Summary
Total: 74 new tests (45 domain + 29 API integration)
All 2706 backend tests pass (0 failures).
Closes #715
Test plan