CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/web-push-tests

Test the browser web-push subscription lifecycle - `pushManager.subscribe({ userVisibleOnly, applicationServerKey })` per [W3C Push API][w3c-push] returning a `PushSubscription` with `endpoint` + `keys.p256dh` + `keys.auth` + optional `expirationTime`; the `pushsubscriptionchange` service-worker event on refresh / revoke / expiry; the `push` event delivery with `PushMessageData`; VAPID auth per RFC 8292 (ES256 JWT, `aud` / `exp` ≤ 24h / `sub`); RFC 8030 push-service responses (201 Created, 410 Gone for expired endpoints, 413 Payload Too Large, 429); and `unsubscribe()` cleanup. Use when a PWA ships web-push and the subscription lifecycle needs release-gate coverage - scoped to the browser Push API, not to cross-channel delivery over native APNs / FCM.

72

Quality

90%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

push-protocol.mdreferences/

Server-side push protocol: RFC 8030 responses and VAPID JWT

Companion detail for web-push-tests. The browser-side subscription lifecycle (subscribe, getSubscription, push event, pushsubscriptionchange, unsubscribe) is the runnable core in SKILL.md; this file holds the server-side protocol tests those steps reference.

Server-side: handle 410 Gone for expired endpoints

Per [rfc8030], 410 Gone is "Returned when the push service ceases retry delivery before advertised expiration." In practice it means the subscription is no longer valid - the application server must delete the endpoint from its store. Test the cleanup path:

import { describe, it, expect, vi } from 'vitest';
import webpush from 'web-push';

describe('subscription cleanup', () => {
  it('deletes the subscription record on 410 Gone', async () => {
    vi.spyOn(webpush, 'sendNotification').mockRejectedValue({
      statusCode: 410,
      body: 'Gone',
    } as any);

    const db = { delete: vi.fn() };

    await sendAndPrune(db, { endpoint: 'https://push.example/abc', keys: { p256dh: '...', auth: '...' } }, { title: 'x' });

    expect(db.delete).toHaveBeenCalledWith('https://push.example/abc');
  });
});

The relevant push-service responses per [rfc8030]:

CodePer [rfc8030]
201 Created"Indicates accepted push messages without delivery confirmation requests"
410 Gone"Returned when the push service ceases retry delivery before advertised expiration" - delete endpoint
413 Payload Too Large"may be returned for entity bodies exceeding size limits (though not for bodies ≤ 4096 bytes)" - split or shrink payload
429 Too Many Requests"Used to reject requests exceeding rate limits" - back off

VAPID JWT shape

Per [rfc8292], the application server signs a JWT with ECDSA on P-256 (the ES256 algorithm) and sends:

Authorization: vapid t=<JWT>, k=<base64url(public key, X9.62 uncompressed)>

The JWT must include three claims per [rfc8292]:

ClaimConstraint
aud"MUST include the Unicode serialization of the origin of the push resource URL"
exp"MUST NOT be more than 24 hours from the time of the request"
sub"SHOULD include a contact URI for the application server as either a 'mailto:' (email) or an 'https:' URI"

Test the server-side VAPID generation:

import { decode } from 'jsonwebtoken';

it('VAPID JWT has aud, exp ≤ 24h, mailto sub', () => {
  const auth = generateVapidAuth('https://updates.push.services.mozilla.com', 'mailto:ops@example.com');
  // Parse t=<jwt>, k=<key>
  const tMatch = auth.match(/t=([^,]+)/);
  expect(tMatch).toBeTruthy();
  const payload = decode(tMatch![1]) as any;

  expect(payload.aud).toBe('https://updates.push.services.mozilla.com');
  expect(payload.exp - Math.floor(Date.now() / 1000)).toBeLessThanOrEqual(24 * 3600);
  expect(payload.sub).toMatch(/^(mailto:|https:)/);
});

Source references:

  • rfc8030: https://datatracker.ietf.org/doc/html/rfc8030
  • rfc8292: https://datatracker.ietf.org/doc/html/rfc8292
  • w3c-push: https://www.w3.org/TR/push-api/

SKILL.md

tile.json