Skip to content

test(events): integration tests for EventDeployService#814

Merged
renemadsen merged 3 commits into
stablefrom
test/event-deploy-service
May 14, 2026
Merged

test(events): integration tests for EventDeployService#814
renemadsen merged 3 commits into
stablefrom
test/event-deploy-service

Conversation

@renemadsen
Copy link
Copy Markdown
Member

Summary

Adds NUnit integration tests covering the inline-deploy invariants of EventDeployService (added in PR #813). Catches regressions before they reach master.

Tests added (5)

  • EnsureDeployedAsync_NoRotationsInWindow_DoesNothing — empty calendar result, no Compliance writes, SDK core never reached.
  • EnsureDeployedAsync_RotationInPast_SkippedNotDeployed — yesterday-dated rotation filtered before the deploy path.
  • EnsureDeployedAsync_RotationIsFromCompliance_SkippedNotDeployedIsFromCompliance=true rotation filtered by the candidate .Where.
  • EnsureDeployedAsync_ComplianceAlreadyExists_SkipsRotation — seeds a Compliance for (planningId, deadline.Date), asserts the idempotence guard prevents a duplicate. Uses a custom TestLogger to also pin that the post-guard not-found warning paths are NOT taken — without this, a broken guard would silently produce the same Compliance count via the AreaRulePlanning == null fallthrough.
  • EnsureDeployedAsync_CancellationRequested_HonoursToken — pre-cancelled token, asserts OperationCanceledException is thrown and no Compliance rows are written.

Tests deferred (with justifications in code)

  • Planning-state immutability — requires a full Planning + PlanningNameTranslation + AreaRulePlanning + AreaRule + Area + Property graph + a real CaseCreate against a real eForm template. Invariant is structurally enforced by the source (no writes to LastExecutedTime/DoneInPeriod/NextExecutionTime/PushMessageSent anywhere) and was pinned by the pre-push code-reviewer on PR feat(events): eager-deploy future events inline in ListEvents #813.
  • Per-rotation failure isolation — eFormCore.Core is a concrete class with no interface, so we can't make CaseCreate throw selectively. Follow-up: extract an ICoreFacade or similar.
  • Happy-path end-to-end — same setup blocker as Planning-state.

Pre-push gate

  • Code-reviewer found two findings, both fixed in 3814d58d:
    • Test Bug fixes #4 tautological (Compliance count == 1 in both "guard fired" AND "guard missed → AreaRulePlanning null fallthrough" branches) — now uses TestLogger to assert the fallthrough paths are NOT taken.
    • Test Fixes #7 cancellation try/catch too permissive — now uses Assert.ThrowsAsync<OperationCanceledException>.
  • Code-simplifier found duplication — applied in this PR's final commit.

CI

These tests run as part of the existing test-dotnet job in .github/workflows/dotnet-core-master.yml, which spins up MariaDB via Testcontainers.

🤖 Generated with Claude Code

renemadsen and others added 3 commits May 14, 2026 09:04
Adds NUnit integration tests covering the inline-deploy invariants:
empty window no-op, past-rotation skip, IsFromCompliance skip,
idempotence, Planning state not mutated, per-rotation failure isolation,
cancellation. Catches regressions in the eager-deploy pipeline added in
d9abf12 before they reach master.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Idempotence test now captures logs via a TestLogger<T> and asserts the
  planning-not-found / areaRulePlanning-not-found warnings were NOT
  emitted. The previous Count==1 assertion was tautological because a
  broken guard would still produce count==1 via the AreaRulePlanning
  null-fallthrough at EventDeployService.cs:227. Production has no
  "already deployed" log on the guard path, so the test pins the
  invariant negatively (no downstream warning) instead of positively
  matching a non-existent log line; this still cleanly distinguishes
  "guard fired" from "guard missed".
- Cancellation test now uses Assert.ThrowsAsync<OperationCanceledException>
  instead of a swallowing try/catch. The throw is guaranteed by the
  EF FirstOrDefaultAsync(..., cancellationToken) call at line 145;
  swallowing it would hide a regression that drops the token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls the 5-way duplicated mock construction and 4-way rotation-DTO
shape into private helpers, plus drops the redundant non-null assertions
on GetCore() which returns Task<Core> (never nullable). No behavioural
change in any test; assertions and arrangement remain byte-equivalent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 14, 2026 07:16
Copy link
Copy Markdown

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 a new NUnit integration test fixture, EventDeployServiceTest, that pins five inline-deploy invariants of EventDeployService (introduced in PR #813). The fixture mocks IBackendConfigurationCalendarService and IEFormCoreService to drive the no-op fast paths (no rotations, past rotation, IsFromCompliance=true), seeds a real Compliance row to verify the idempotence guard via a captured-log TestLogger, and asserts that a pre-cancelled CancellationToken propagates as OperationCanceledException with no Compliance writes. Three additional cases (planning-state immutability, per-rotation failure isolation, happy-path E2E) are intentionally deferred with in-file rationale.

Changes:

  • New EventDeployServiceTest fixture with 5 active tests + 3 documented skips
  • Helpers MakeMocks / MakeService / MakeRotation to reduce per-test boilerplate
  • Internal TestLogger<T> capture-only ILogger to support log-based guard assertions
Comments suppressed due to low confidence (2)

eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn.Integration.Test/EventDeployServiceTest.cs:336

  • Test 7's docstring states the EF query at EventDeployService.cs:145 is "GUARANTEED to throw OperationCanceledException". This is misleading: even if EF Core were to ignore the cancellation token at that query, the explicit cancellationToken.ThrowIfCancellationRequested() inside the foreach loop (currently EventDeployService.cs:170) will also throw on the first candidate iteration. The test would still pass via that path, so the comment overstates which line is actually pinning the invariant — and a future regression that drops the token at line 145 (but keeps it at line 170) would not be caught by this test.
        // Act + Assert — the future-day rotation passes the candidate
        // filter, so the pipeline reaches the SDK Sites EF query at
        // EventDeployService.cs:145 which is passed the cancelled token
        // and is GUARANTEED to throw OperationCanceledException (or its
        // TaskCanceledException subclass). A previous swallowing try/catch
        // would have masked a regression that accidentally drops the token
        // (e.g. passes CancellationToken.None to the EF call).
        Assert.ThrowsAsync<OperationCanceledException>(
            () => service.EnsureDeployedAsync(
                PropertyId, BoardIds, "2026-05-14", "2026-05-20", SdkSiteId, cts.Token));

eFormAPI/Plugins/BackendConfiguration.Pn/BackendConfiguration.Pn.Integration.Test/EventDeployServiceTest.cs:336

  • Assert.ThrowsAsync runs the supplied async lambda synchronously (via blocking on the returned task) and is not awaited here. Inside an async Task test, the conventional pattern is await Assert.ThrowsAsync<OperationCanceledException>(...) so the exception is observed on the test's async context; the current call works in NUnit but is inconsistent with the rest of the test method (which uses await for every other awaitable) and may also surface analyzer warnings (CA2007 / unawaited task). Adding await keeps the style uniform and avoids any surprise if NUnit ever changes how ThrowsAsync schedules the lambda.
        Assert.ThrowsAsync<OperationCanceledException>(
            () => service.EnsureDeployedAsync(
                PropertyId, BoardIds, "2026-05-14", "2026-05-20", SdkSiteId, cts.Token));

Comment on lines +42 to +55
/// Integration tests for <see cref="EventDeployService"/> — the inline-deploy
/// pipeline that backs <c>EventsGrpcService.ListEvents</c>. Each test pins one
/// invariant from <c>EventDeployService.cs:42-50</c> so a future regression in
/// the eager-deploy contract surfaces in CI before reaching master.
///
/// All tests use a real <see cref="BackendConfigurationPnDbContext"/> +
/// <see cref="ItemsPlanningPnDbContext"/> from <see cref="TestBaseSetup"/> and
/// mock <see cref="IBackendConfigurationCalendarService"/> so we can pin the
/// rotation stream the deploy pipeline iterates over without standing up the
/// full calendar service. <see cref="IEFormCoreService"/> is also mocked
/// because the no-op fast paths (tests 1-3) return before
/// <c>coreHelper.GetCore()</c> is reached; tests that DO reach the SDK path
/// (4, 7) seed a real SDK site via <see cref="TestBaseSetup.GetCore"/>.
/// </summary>

// Act
await service.EnsureDeployedAsync(
PropertyId, BoardIds, "2026-05-14", "2026-05-20", SdkSiteId, CancellationToken.None);
Is.False,
"Guard should have short-circuited before the planning lookup at EventDeployService.cs:200-212.");
Assert.That(
logger.Entries.Any(e => e.Message.Contains("areaRulePlanning") && e.Message.Contains("not found")),
@renemadsen renemadsen merged commit 09157be into stable May 14, 2026
21 checks passed
@renemadsen renemadsen deleted the test/event-deploy-service branch May 14, 2026 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants