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
Apply the four Greptile P1 fixes to the Notifications worker and
extend it for C3 email read tracking and ST4's stripped SMTP plumbing.
P1 #1: alreadyDelivered() now queries the indexed messageId attribute
instead of getDocument($messageId). The action loop and Console
adapter both write compound `$id`s (messageId + recipient hash), so
the previous direct-id lookup always missed.
P1 #3: action() no longer calls persistAlert after dispatchConsole;
ConsoleAdapter persists internally. Email persists inside
dispatchEmail BEFORE the adapter send so the alertId is available for
the tracking pixel; webhook persists in the action loop after a
successful HTTP send.
P1 #4: dispatchConsole now throws when the adapter reports
`deliveredTo === 0`, surfacing the per-recipient error.
Recipient threading: dispatch() now takes the full recipient map and
returns the alertId (or null when persistence is the caller's
responsibility). persistAlert() reads userId/teamId from the
recipient and grants per-user / per-team-owner CRUD permissions,
falling back to payload permissions only when neither is set. The
returned alertId lets dispatchEmail splice a 1x1 tracking pixel
before the last `</body>` tag, signed with a 30-day HS256 JWT
(_APP_OPENSSL_KEY_V1) carrying {alertId, userId}.
SMTP resolution: ST4 stripped `smtp` and `customMailOptions` from
the Notification event payload, so the worker now resolves SMTP
from the injected project Document (mirroring Mails.php /
Memberships/Create.php), falling back to the env-driven cloud SMTP
adapter when the project has no enabled override.
Tests updated: SpyNotifications.dispatch() matches the new
signature and emulates per-channel persistence so existing routing
assertions keep their semantics. Memory `alerts` collection adds
the `read` boolean attribute to mirror platform.php.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the bin/worker-notifications entrypoint, Dockerfile chmod, and
docker-compose service definitions for the new Notifications worker
without disturbing the existing Mails worker, and re-adds the Mails
queue/class constants on Event so the legacy callers still resolve.
The full mail->notification swap (caller migration, Mails removal,
worker rename) will land as a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Drop the global _APP_NOTIFICATIONS_WEBHOOK_SECRET env var. There's no
analogous global webhook secret in Appwrite; the existing Webhooks
worker carries a per-webhook signatureKey on the webhook document.
Move the same pattern into the Notification event: each webhook
recipient may carry an optional signatureKey, which the worker
forwards to the Webhook adapter for HMAC-SHA256 signing. Recipients
without a key are delivered unsigned and a tag is logged for audit.
Mirror the upstream Utopia\Messaging package namespace under the
Appwrite\Utopia\Messaging prefix, matching the convention used by
Appwrite\Utopia\Database and Appwrite\Utopia\Response.
Registers the webhook signing secret in app/config/variables.php under a
new Notifications category, threads it through the worker container in
docker-compose.yml and tests/resources/docker/docker-compose.yml, and
adds an empty default to .env so operators see the knob alongside the
existing SMTP block.
When set, outbound webhook deliveries from the notifications worker
include an X-Appwrite-Webhook-Signature header carrying
sha256=<hex(hmac_sha256(timestamp.body, secret))>. When unset, the
worker delivers payloads unsigned — receivers must decide whether to
accept them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Notifications e2e suite under tests/e2e/Services/Notifications.
Asserts that the live notifications queue depth is reported via
GET /v1/health/queue/notifications, that the threshold guard is honoured,
and that the failed-jobs endpoint accepts the v1-notifications queue name.
Dispatch routing, dedup, and webhook signing are covered by the unit
suite — the worker cannot be deterministically driven through the live
queue from a test client without an admin enqueue endpoint, so the e2e
file pins the public health contract that ops dashboards and KEDA scale
on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the dedup short-circuit, per-channel dispatch routing, alert
persistence, error tagging, and legacy single-recipient fallback in the
Notifications worker, plus the Console adapter's permission shape and the
Webhook adapter's HMAC-SHA256 signing contract, header layout, response
handling, and unsigned-when-secret-missing behaviour.
Worker dispatch helpers move from private to protected so a test spy can
override them without monkey-patching. The Swoole runtime hook flag
mutation is now guarded by class_exists so the action can run under bare
PHPUnit (no Swoole extension on the test host).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>