Locks the bug-fix invariants from PR #12195's last review pass and rounds out
worker channel coverage:
C1 testEmailSendFailureDoesNotPersistAlert — SMTP throw must NOT leave a dedup
row behind, retry must deliver and persist exactly once.
C2 testNotificationEventResetClearsAllState — reset() drops every state-bearing
field including preview (regression: missed in original reset body).
C3 testWebhookSendAlertResetsBetweenCalls — Webhooks::sendAlert must reset()
the DI-shared Notification event so two paused-webhook alerts in one worker
pass do not bleed recipients/subject/body into each other.
C4 testConsoleAdapterTreatsDuplicateAsDelivered — Duplicate on createDocument
must surface as a successful idempotent send, not a per-recipient error.
M7 testTrackingPixelRejectsJwtWithoutPurposeClaim — Track endpoint silently
ignores JWTs missing or with the wrong purpose claim (defends against
replaying session/reset JWTs to mark alerts read).
Worker happy-path tests: testEmailChannelHappyPath, testConsoleChannelHappyPath,
testWebhookChannelHappyPath cover the full per-channel dispatch contract end
to end, including HMAC signing for webhooks and the tracking pixel injection
+ post-send persistence for email.
Also extracts CapturingWebhook into its own PSR-4 file so reused across tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Account/Alerts/Track endpoint is scope:public (email clients have no
session); SDK Method now declares auth: [] so generators do not require
an auth header for an unauthenticated endpoint.
- NotificationsTest setUp now creates the production `_key_recipient`
UNIQUE composite index on (messageId, channel, userId, teamId), and
testPersistAlertReturnsExistingAlertIdOnDuplicate exercises the
DuplicateException → return-existing-alertId branch end-to-end (both
primary-key collision and unique-index collision via a sibling $id).
- Tracking-pixel e2e now asserts _APP_OPENSSL_KEY_V1 is set instead of
silently falling back to the .env.example placeholder, so a missing
CI secret fails loudly rather than passing against the wrong key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add 6 new test methods to NotificationsBase that exercise the wave3
account-alerts surface end-to-end:
- testListAccountAlertsEmpty: GET /v1/account/alerts shape check
- testWebhookFailureCreatesConsoleAlert: drives a webhook past
_APP_WEBHOOK_MAX_FAILED_ATTEMPTS via user-create events and polls
/account/alerts until the worker fans the paused alert out to the
project owner on the console channel
- testMarkAlertReadTogglesFlag: PATCH /:alertId/read happy path
- testMarkAlertReadUnauthorized: stranger console user cannot mark
someone else's alert as read; alert remains unread for the owner
- testTrackingPixelTogglesRead: GET /:alertId/track with a valid
HS256 JWT signed with _APP_OPENSSL_KEY_V1 returns the canonical 1x1
PNG and atomically marks the alert as read
- testTrackingPixelInvalidTokenReturnsPng: tampered JWT still gets a
PNG (no information disclosure) but performs no DB write
Helpers:
- seedWebhookFailureAlert: registers a webhook pointing at an
unroutable address (http://127.0.0.1:1/), drives max+2 user-create
events through the project, polls assertEventually with a 60s budget
for the paused alert keyed by the deterministic md5 of
'webhook:<id>:paused:<attempts>'
- createConsoleUser: spins up a fresh, unrelated console user with its
own session for the unauthorized assertion
- getConsoleAlertHeaders: console-session auth bundle reused across
every alerts call, so the trait works identically under SideServer
and SideConsole hosts
Mirrors the dominant e2e pattern (FunctionsBase/MigrationsBase): the
shared health-queue assertions live in a NotificationsBase trait,
with thin per-side overlays for ProjectCustom + SideServer and
ProjectCustom + SideConsole.