/** * QR payload tests — instance-relative contract. * * The service mints `/api/alarm/a/{alertId}#{token}` (no host) and the client * composes `{configuredOrigin}{qrPayload}`. The parser matches path shape + * fragment, never host, so legacy `alarm.dcntrlized.org` codes and other * instances' codes still work. */ import { buildQrPayload, buildQrSchemePayload, parseManualToken, parseQrPayload, qrErrorMessage, truncateToken, } from '../src/lib/qrPayload'; const ID = 'alr_7f3c9a1b2d'; const TOKEN = 'sub_9Qx2mZp4Lk7VeR1sTn0'; const ORIGIN = 'https://instance.example'; describe('buildQrPayload', () => { it('composes the service path under the configured origin', () => { expect(buildQrPayload(ID, TOKEN, ORIGIN)).toBe(`${ORIGIN}/api/alarm/a/${ID}#${TOKEN}`); }); it('strips a trailing slash on the origin', () => { expect(buildQrPayload(ID, TOKEN, `${ORIGIN}/`)).toBe(`${ORIGIN}/api/alarm/a/${ID}#${TOKEN}`); }); it('renders a host-free relative path when no origin is configured', () => { expect(buildQrPayload(ID, TOKEN)).toBe(`/api/alarm/a/${ID}#${TOKEN}`); }); it('keeps the pre-existing scheme builder for old codes but mints no new host', () => { expect(buildQrSchemePayload(ID, TOKEN)).toBe(`dcntrlized-alarm://a/${ID}#${TOKEN}`); }); }); describe('parseQrPayload — path shape, never host', () => { it('parses the composed payload', () => { const res = parseQrPayload(buildQrPayload(ID, TOKEN, ORIGIN)); expect(res).toEqual({ ok: true, payload: { alertId: ID, subscribeToken: TOKEN, source: 'url' }, }); }); it('accepts a relative /api/alarm/a/{id}#{token}', () => { const res = parseQrPayload(`/api/alarm/a/${ID}#${TOKEN}`); expect(res.ok && res.payload.alertId).toBe(ID); expect(res.ok && res.payload.source).toBe('relative'); }); it('accepts a legacy https://alarm.dcntrlized.org/a/{id}#{token}', () => { const res = parseQrPayload(`https://alarm.dcntrlized.org/a/${ID}#${TOKEN}`); expect(res.ok && res.payload.alertId).toBe(ID); expect(res.ok && res.payload.subscribeToken).toBe(TOKEN); }); it('accepts a full http(s) URL on any host (host grants nothing)', () => { expect(parseQrPayload(`https://evil.example.com/a/${ID}#${TOKEN}`).ok).toBe(true); expect(parseQrPayload(`http://127.0.0.1:8080/api/alarm/a/${ID}#${TOKEN}`).ok).toBe(true); expect(parseQrPayload(`https://staging.alarm.dcntrlized.org/a/${ID}#${TOKEN}`).ok).toBe(true); }); it('rejects a URL whose path is not an alarm path', () => { expect(parseQrPayload(`https://example.com/foo/bar#${TOKEN}`)).toEqual({ ok: false, error: 'not-an-alarm-code', }); }); it('rejects a foreign scheme', () => { expect(parseQrPayload(`ftp://example.com/a/${ID}#${TOKEN}`)).toEqual({ ok: false, error: 'not-an-alarm-code', }); }); it('parses the pre-existing custom scheme', () => { const res = parseQrPayload(buildQrSchemePayload(ID, TOKEN)); expect(res.ok && res.payload.source).toBe('scheme'); expect(res.ok && res.payload.alertId).toBe(ID); }); it('accepts a relative a/{id}#{token} and raw {id}#{token}', () => { expect(parseQrPayload(`a/${ID}#${TOKEN}`).ok).toBe(true); const raw = parseQrPayload(`${ID}#${TOKEN}`); expect(raw.ok && raw.payload.source).toBe('raw'); }); it('tolerates whitespace, a trailing slash and query noise', () => { const res = parseQrPayload(` https://alarm.dcntrlized.org/a/${ID}/?utm=x#${TOKEN} `); expect(res.ok && res.payload.alertId).toBe(ID); expect(res.ok && res.payload.subscribeToken).toBe(TOKEN); }); it('reports empty, missing token and missing alert id', () => { expect(parseQrPayload(' ')).toEqual({ ok: false, error: 'empty' }); expect(parseQrPayload(`https://alarm.dcntrlized.org/a/${ID}`)).toEqual({ ok: false, error: 'missing-token', }); expect(parseQrPayload(`https://alarm.dcntrlized.org/a/#${TOKEN}`)).toEqual({ ok: false, error: 'missing-alert-id', }); }); it('never throws on garbage', () => { for (const junk of ['###', 'https://', '://a/b#c', '\u0000', '{"json":true}']) { expect(() => parseQrPayload(junk)).not.toThrow(); } }); it('round-trips ids that need encoding', () => { const weird = 'alr/with space'; const res = parseQrPayload(buildQrPayload(weird, TOKEN, ORIGIN)); expect(res.ok && res.payload.alertId).toBe(weird); }); }); describe('parseManualToken', () => { it('accepts a bare token when the alert id is known', () => { expect(parseManualToken(TOKEN, ID)).toEqual({ ok: true, payload: { alertId: ID, subscribeToken: TOKEN, source: 'raw' }, }); }); it('accepts a full payload even when an id is also supplied', () => { const res = parseManualToken(buildQrPayload(ID, TOKEN, ORIGIN), 'other'); expect(res.ok && res.payload.alertId).toBe(ID); }); it('fails a bare token with no known alert id', () => { expect(parseManualToken(TOKEN, '')).toEqual({ ok: false, error: 'missing-alert-id' }); }); it('fails empty input', () => { expect(parseManualToken('', ID)).toEqual({ ok: false, error: 'empty' }); }); }); describe('copy helpers', () => { it('truncates tokens to 12 chars + ellipsis', () => { expect(truncateToken('0123456789abcdef')).toBe('0123456789ab…'); expect(truncateToken('short')).toBe('short'); }); it('has a message for every error', () => { for (const err of ['empty', 'not-an-alarm-code', 'missing-alert-id', 'missing-token'] as const) { expect(qrErrorMessage(err).length).toBeGreaterThan(0); } }); });