Skip to content

Test: archive and restore lifecycle integration tests (#715)#755

Merged
Chris0Jeky merged 6 commits intomainfrom
test/715-archive-restore-lifecycle
Apr 4, 2026
Merged

Test: archive and restore lifecycle integration tests (#715)#755
Chris0Jeky merged 6 commits intomainfrom
test/715-archive-restore-lifecycle

Conversation

@Chris0Jeky
Copy link
Copy Markdown
Owner

Summary

  • 45 domain-level ArchiveItem state machine lifecycle tests covering all transitions, invalid transitions, full lifecycle sequences, and constructor validation
  • 29 API integration tests for full archive/restore lifecycle through HTTP endpoints
  • Covers boards, cards, columns, cascade behavior, cross-user isolation, audit trail, conflict detection (Rename/Fail strategies), snapshot integrity, double-archive/restore handling, immediate restore, and authentication enforcement
  • Fixes pre-existing build error in AuthControllerEdgeCaseTests (missing IUserContext parameter after SEC-20 fix)

Total: 74 new tests (45 domain + 29 API integration)

All 2706 backend tests pass (0 failures).

Closes #715

Test plan

  • All new tests pass
  • No existing tests broken
  • Full suite verified: Domain 455, Application 1495, Api 744, Cli 4, Architecture 8

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
AuthController constructor was updated to require IUserContext in the
SEC-20 fix (#722/#732) but AuthControllerEdgeCaseTests was not updated,
causing build failures.
Copilot AI review requested due to automatic review settings April 4, 2026 01:19
@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Adversarial Self-Review

Findings

1. Unused import: System.Net.Http.Headers in ArchiveRestoreLifecycleTests.cs -- this using is not needed since authentication is handled via ApiTestHarness.AuthenticateAsync. Minor cleanup.

2. RestoreCard_ToNonExistentBoard_ShouldReturnNotFound may be a false positive -- The test sends a non-existent TargetBoardId but the auth service checks board-write permission first. If the auth service returns a Forbidden (because the board doesn't exist in the user's access list), this test could get a 403 instead of 404. However, the test currently passes because the RestorePlanner checks board existence after auth. This is stable but worth noting as a coupling to implementation ordering.

3. RestoreCard_ToArchivedBoard_ShouldReturnError uses weak assertion -- restoreResponse.IsSuccessStatusCode.Should().BeFalse() doesn't assert the specific HTTP status code. Should assert the exact expected status (likely 400 or 409) for a stronger contract check.

4. RestoreCard_WithInvalidSnapshotJson_ShouldReturnError also uses weak assertion -- Same issue with IsSuccessStatusCode.Should().BeFalse().

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:

  • Items 1, 3, 4 (strengthen weak assertions, remove unused import)

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Review fixes applied

Fixed the 3 issues from self-review:

  • Removed unused System.Net.Http.Headers import
  • Strengthened RestoreCard_WithInvalidSnapshotJson_ShouldReturnError to assert specific status codes (400/500/409)
  • Strengthened RestoreCard_ToArchivedBoard_ShouldReturnError to assert specific status codes (400/409)

All 29 API integration tests + 45 domain tests continue to pass. Full suite green (2706 total).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ArchiveItem lifecycle/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 AuthControllerEdgeCaseTests to pass the required IUserContext dependency.

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
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
// Small delay to ensure timestamp difference
// MarkAsRestored should update UpdatedAt; this assertion allows equal-or-later timestamps.

Copilot uses AI. Check for mistakes.
var restoreResult = await restoreResponse.Content.ReadFromJsonAsync<RestoreResult>();
restoreResult.Should().NotBeNull();
restoreResult!.Success.Should().BeTrue();
restoreResult.RestoredEntityId.Should().NotBeNull();
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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();

Copilot uses AI. Check for mistakes.

var restoreResult = await restoreResponse.Content.ReadFromJsonAsync<RestoreResult>();
restoreResult!.Success.Should().BeTrue();
restoreResult.ResolvedName.Should().Be("Restored Card");
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +268 to +271
restoreResult!.Success.Should().BeTrue();
restoreResult.ResolvedName.Should().Be("Restored Column");
}

Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +578 to +583
// 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);
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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");

Copilot uses AI. Check for mistakes.
Comment on lines +732 to +735
restoreResponse.IsSuccessStatusCode.Should().BeFalse();
restoreResponse.StatusCode.Should().BeOneOf(
HttpStatusCode.BadRequest,
HttpStatusCode.Conflict);
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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");

Copilot uses AI. Check for mistakes.
@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Second-Pass Adversarial Review

Finding 1 (REAL BUG): RestoreArchivedBoard_ShouldSucceed_AndBoardReappears is a false-positive test

Severity: High

The test name claims the board "reappears" after restore, but it never verifies this. It only checks restoreResult.Success == true and RestoredEntityId != null. The board could remain archived and this test would still pass. This was flagged by Copilot but never addressed in the self-review fix commit.

Fix: Add assertions that fetch the board after restore and verify IsArchived == false.


Finding 2 (REAL BUG): RestoreCard_WhenOriginalColumnExists_ShouldRestoreToOriginalColumn never asserts ColumnId

Severity: High

The test name promises it verifies the card is restored to the original column, but it only checks ResolvedName == "Restored Card". It never fetches the restored card or checks its ColumnId. This is a false-positive test -- it would pass even if the restore planner put the card in a completely different column. Flagged by Copilot, not addressed.

Fix: Fetch the restored card via GET /api/boards/{boardId}/cards and assert ColumnId == column.Id.


Finding 3 (REAL BUG): RestoreColumn_ShouldAddToEndOfBoard never asserts position

Severity: Medium

Test name says "should add to end of board" but only asserts Success == true and ResolvedName. No position check. The column could be added at position 0 and this test would still pass. Flagged by Copilot, not addressed.


Finding 4 (Weak assertions not fully fixed): RestoreCard_ToArchivedBoard and RestoreCard_WithInvalidSnapshotJson

Severity: Medium

The self-review commit claimed to "strengthen weak assertions" but both tests still use BeOneOf with multiple status codes (400/409 and 400/500/409 respectively). The RestoreCard_WithInvalidSnapshotJson test accepts HttpStatusCode.InternalServerError as a valid outcome -- a 500 from invalid user input is a server bug, not expected behavior. These need to pin down the actual expected status code.


Finding 5 (Misleading comment): Domain test MarkAsRestored_ShouldCallTouch

Severity: Low

Line 121: Comment says "Small delay to ensure timestamp difference" but no delay is performed. The comment is copy-paste noise. Should be corrected or removed. Flagged by Copilot, not addressed.


Finding 6 (Convention): Unused System.Net.Http.Headers import claimed removed but still present

Severity: Low

The self-review said this was removed, but the actual file on the branch does not have this import (confirmed it was actually fixed). No action needed.


Summary of unaddressed Copilot feedback

4 of 6 Copilot review comments were acknowledged but never fixed in the follow-up commit. The self-review only addressed the unused import and the BeOneOf assertions (partially). The false-positive test names (Findings 1-3) are the most concerning because they give false confidence in lifecycle correctness.

I will fix Findings 1-5 on this branch.

- 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
Chris0Jeky added a commit that referenced this pull request Apr 4, 2026
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.
@Chris0Jeky Chris0Jeky merged commit 8ac9762 into main Apr 4, 2026
20 of 21 checks passed
@Chris0Jeky Chris0Jeky deleted the test/715-archive-restore-lifecycle branch April 4, 2026 02:43
@github-project-automation github-project-automation bot moved this from Pending to Done in Taskdeck Execution Apr 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

TST-48: Archive and restore lifecycle integration tests

2 participants