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
Full per-platform test-pattern code for push-notification-test-author. Each recipe mocks at the SDK boundary (the default isolation level); swap for sandbox / emulator when verifying provider-side behavior.
The RFC 8030 flow and the 410 Gone cleanup requirement are covered in Step 2 of the skill; the recipe below mocks webPush.sendNotification at the SDK boundary.
const webPush = require('web-push');
const { jest } = require('@jest/globals');
describe('push notification', () => {
beforeAll(() => {
webPush.setVapidDetails(
'mailto:test@example.com',
VAPID_PUBLIC_KEY,
VAPID_PRIVATE_KEY,
);
});
it('sends order-status notification with correct payload', async () => {
const sendSpy = jest.spyOn(webPush, 'sendNotification').mockResolvedValue({
statusCode: 201,
});
await pushOrderUpdate(testSubscription, { orderId: 123, status: 'shipped' });
expect(sendSpy).toHaveBeenCalledWith(
testSubscription,
expect.stringContaining('"orderId":123'),
expect.any(Object),
);
});
it('removes expired subscription on 410 response', async () => {
jest.spyOn(webPush, 'sendNotification').mockRejectedValue({ statusCode: 410 });
await pushOrderUpdate(testSubscription, { orderId: 123 });
const stored = await Subscription.findOne({ endpoint: testSubscription.endpoint });
expect(stored).toBeNull();
});
});In a service-worker test harness (sw-toolbox-test or workbox-cli's testing utilities):
self.addEventListener('push', event => {
event.waitUntil(
self.registration.showNotification('Order update', {
body: event.data.json().status,
icon: '/icons/order.png',
data: { orderId: event.data.json().orderId },
})
);
});
// Test: simulate push event
const event = { data: { json: () => ({ orderId: 123, status: 'shipped' }) } };
self.dispatchEvent(new PushEvent('push', event));
expect(self.registration.showNotification).toHaveBeenCalled();Run against api.sandbox.push.apple.com with a development APNs certificate or Auth Key (.p8 file).
import pytest
from unittest.mock import patch
from my_app.notifications import send_apns
def test_apns_payload_shape():
with patch("my_app.notifications.apns_client.send") as mock_send:
mock_send.return_value = {"status": 200}
send_apns(device_token="abc123", title="Order shipped", body="Your order is on the way")
sent_payload = mock_send.call_args.kwargs["payload"]
assert sent_payload["aps"]["alert"]["title"] == "Order shipped"
assert sent_payload["aps"]["alert"]["body"] == "Your order is on the way"
assert sent_payload["aps"]["sound"] == "default"Invalid-token handling (HTTP 410 from APNs):
def test_apns_410_removes_token():
with patch("my_app.notifications.apns_client.send") as mock_send:
mock_send.return_value = {"status": 410, "reason": "Unregistered"}
send_apns_with_cleanup(device_token="abc123", ...)
token = DeviceToken.objects.filter(token="abc123").first()
assert token is NoneUse the HTTP v1 API for new code (legacy HTTP API is deprecated). Invalid-token responses include messaging/registration-token-not-registered; test the cleanup path the same way as APNs.
const admin = require('firebase-admin');
const { jest } = require('@jest/globals');
it('sends FCM message with correct shape', async () => {
const sendSpy = jest.spyOn(admin.messaging(), 'send').mockResolvedValue('msg-id-123');
await sendFcmOrderUpdate('device-token', { orderId: 123 });
expect(sendSpy).toHaveBeenCalledWith(
expect.objectContaining({
token: 'device-token',
notification: expect.objectContaining({ title: 'Order Update' }),
data: expect.objectContaining({ orderId: '123' }),
android: expect.objectContaining({ priority: 'high' }),
apns: expect.any(Object), // FCM cross-platform routing
}),
);
});A background notification's aps holds only content-available: 1,
and the request carries apns-push-type: background and
apns-priority: 5 (pushing background updates).
it('builds a background sync push', () => {
const { payload, headers } = buildSilentSyncPush({ since: 42 });
expect(payload.aps).toEqual({ 'content-available': 1 }); // nothing user-facing
expect(payload.since).toBe(42);
expect(headers['apns-push-type']).toBe('background');
expect(headers['apns-priority']).toBe('5');
});def test_order_push_deep_links_to_order_screen():
payload = build_order_push(order_id=123)
assert payload["data"]["route"] == "/orders/123"
def test_deep_link_uses_the_order_id():
assert build_order_push(order_id=7)["data"]["route"] == "/orders/7"The second case catches a builder that returns a constant route.
it('subscribes the device to the order-updates topic it later sends to', async () => {
const subscribe = jest.spyOn(admin.messaging(), 'subscribeToTopic')
.mockResolvedValue({ successCount: 1, failureCount: 0, errors: [] });
const send = jest.spyOn(admin.messaging(), 'send').mockResolvedValue('projects/p/messages/1');
await subscribeToOrderUpdates('device-token-1', 'user-9');
await broadcastOrderUpdate('user-9', { status: 'shipped' });
expect(subscribe).toHaveBeenCalledWith(['device-token-1'], 'order-updates-user-9');
expect(send.mock.calls[0][0].topic).toBe('order-updates-user-9');
expect(send.mock.calls[0][0].token).toBeUndefined();
});subscribeToTopic(tokens, topic) and send(message) are the
firebase-admin Messaging methods; a message names exactly one of
token, topic or condition (FCM send docs).