Use when push or in-app notifications need tests: Web Push with VAPID, Apple APNs and Firebase FCM subscription, payload encryption, badge, sound and click actions, expired subscriptions, silent vs alert pushes and topic routing, plus in-app delivery over WebSocket or SSE with read state.
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Three push platforms dominate:
| Platform | Standard / Provider |
|---|---|
| Web Push | IETF RFC 8030 + VAPID (RFC 8292) |
| APNs (iOS / iPadOS / macOS) | Apple proprietary HTTP/2 |
| FCM (Android + cross-platform) | Google Firebase |
Each platform has distinct test patterns; Step 1 picks the isolation level and Steps 2-4 cover each platform.
| Level | Example | Tradeoffs |
|---|---|---|
| Mock the SDK | Patch FCM/APNs/Web-Push library send method | Fast; misses provider-side behavior |
| Sandbox / emulator | APNs sandbox, FCM emulator (limited), web-push test browser | Realistic; slower |
| End-to-end with test devices | Real device farm | Highest fidelity; expensive + flaky |
Default: mock the SDK - fast, deterministic, covers payload-shape + error-path logic which is most of what regressions hit. Use sandbox/emulator when verifying provider-side behavior (encryption, rate-limit, real 410 handling); use real device farms only for grouped-notification / channel UX work.
Mock webPush.sendNotification, assert the payload, and test that a
410 Gone from the push service deletes the stored subscription
(RFC 8030 §6.2 - the subscription has expired or been
revoked). Add a service-worker test that dispatches a synthetic
PushEvent and asserts showNotification gets the right title and
body. Recipes: references/platform-test-patterns.md.
Mock the APNs client, assert the aps payload, and test that a 410
with reason Unregistered removes the device token
(sending requests to APNs). Assert the client is built for
api.sandbox.push.apple.com in every test and CI configuration;
api.push.apple.com reaches real users.
Use the HTTP v1 API: the legacy HTTP and XMPP APIs were deprecated on
20 June 2023 and their shutdown began on 22 July 2024
(FCM migration guide). Mock admin.messaging().send, assert
the message (token or topic, notification, data, android,
apns), and test that messaging/registration-token-not-registered
removes the token.
Code for each is in the "Payload variants" section of references/platform-test-patterns.md. What each test must assert:
| Variant | Assert |
|---|---|
| Silent (background) push, APNs | aps holds only content-available: 1 (no alert, sound, badge); request headers apns-push-type: background and apns-priority: 5. The system may throttle or drop these, so the app must not depend on every one arriving (background updates) |
| Alert push | alert title and body present; apns-push-type: alert |
| Deep link | The payload's link / data route points at the exact screen (/orders/123), built from the entity id, not a constant |
| FCM topic | Subscribe call uses the topic name the sender publishes to; a send to that topic uses topic, not token |
Opening the deep link on a device needs device automation
(appium-testing, espresso-suite, xcuitest-suite).
Before calling the suite done, prove each test can fail:
Unregistered / registration-token-not-registered tests
must go red. Restore it.alert to the silent payload: the silent-push test must go red.grep -rn "api.push.apple.com" tests/ config/test*: no hits.A test that stays green through its own sabotage is not testing the path; fix the assertion (usually it asserts on the mock call instead of the stored state) and repeat.
For each channel the app uses:
An e-commerce app sends a Web Push "order shipped" notification and must clean up revoked subscriptions.
webPush.sendNotification (Step 1 default).pushOrderUpdate(sub, { orderId: 123, status: 'shipped' }) and asserts sendNotification was called with a payload string containing "orderId":123. The mock returns { statusCode: 201 }; the assertion passes.sendNotification to reject with { statusCode: 410 }. After pushOrderUpdate runs, the test asserts the subscription row is gone from storage (Subscription.findOne(...) returns null), proving the 410 cleanup path per RFC 8030.PushEvent whose data.json() returns the order payload; assert self.registration.showNotification was called with body shipped.| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Test only happy path | Miss expired-token cleanup; storage grows; spam to dead devices | Step 2-4 410 / 404 / unregistered tests |
| Hardcode VAPID public key in tests + checked into repo | Key rotation breaks tests | Inject via env var |
| Send to real production APNs in tests | Real users get test push | Sandbox environment (Step 3) |
Silent payload with alert or without apns-push-type: background / apns-priority: 5 | Not delivered as a background update | Step 5 |
| Skip deep-link test | Deep links break silently after refactors | Step 5 |
| Cleanup test asserts only that the mock was called | Passes when the delete is gone | Assert stored state; sabotage check (Step 6) |
FCM legacy server key / fcm.googleapis.com/fcm/send | Legacy API shut down from July 2024 | HTTP v1 (Step 4) |
mailpit-testing (email flows in its references),
sms-test-author - sister channelsappium-testing,
xcuitest-suite,
espresso-suite - device-side click-action verification