/** * QR payload parser tests — the frozen payload contract. * https://alarm.dcntrlized.org/a/{alertId}#{subscribeToken} */ import { buildQrPayload, buildQrSchemePayload, parseManualToken, parseQrPayload, qrErrorMessage, truncateToken, QR_HOST, } from '../src/lib/qrPayload'; const ID = 'alr_7f3c9a1b2d'; const TOKEN = 'sub_9Qx2mZp4Lk7VeR1sTn0'; describe('buildQrPayload', () => { it('renders the canonical contract payload', () => { expect(buildQrPayload(ID, TOKEN)).toBe(`https://${QR_HOST}/a/${ID}#${TOKEN}`); }); it('renders the custom-scheme variant', () => { expect(buildQrSchemePayload(ID, TOKEN)).toBe(`dcntrlized-alarm://a/${ID}#${TOKEN}`); }); }); describe('parseQrPayload', () => { it('parses the canonical payload', () => { const res = parseQrPayload(buildQrPayload(ID, TOKEN)); expect(res).toEqual({ ok: true, payload: { alertId: ID, subscribeToken: TOKEN, source: 'url' }, }); }); it('parses the custom scheme', () => { const res = parseQrPayload(buildQrSchemePayload(ID, TOKEN)); expect(res.ok && res.payload.alertId).toBe(ID); expect(res.ok && res.payload.subscribeToken).toBe(TOKEN); expect(res.ok && res.payload.source).toBe('scheme'); }); it('accepts a relative a/{id}#{token} payload', () => { const res = parseQrPayload(`a/${ID}#${TOKEN}`); expect(res.ok && res.payload.source).toBe('relative'); expect(res.ok && res.payload.alertId).toBe(ID); }); it('accepts a raw {id}#{token} payload (what the owner reads off the panel)', () => { const res = parseQrPayload(`${ID}#${TOKEN}`); expect(res.ok && res.payload.source).toBe('raw'); expect(res.ok && res.payload.subscribeToken).toBe(TOKEN); }); it('tolerates surrounding whitespace and a trailing slash', () => { const res = parseQrPayload(` https://${QR_HOST}/a/${ID}/#${TOKEN} `); expect(res.ok && res.payload.alertId).toBe(ID); }); it('tolerates query noise before the fragment', () => { const res = parseQrPayload(`https://${QR_HOST}/a/${ID}?utm=x#${TOKEN}`); expect(res.ok && res.payload.alertId).toBe(ID); expect(res.ok && res.payload.subscribeToken).toBe(TOKEN); }); it('accepts a subdomain of the alarm host (staging)', () => { const res = parseQrPayload(`https://staging.alarm.dcntrlized.org/a/${ID}#${TOKEN}`); expect(res.ok).toBe(true); }); it('accepts localhost / LAN dev payloads', () => { expect(parseQrPayload(`http://127.0.0.1:8088/a/${ID}#${TOKEN}`).ok).toBe(true); expect(parseQrPayload(`http://192.168.1.232:8088/a/${ID}#${TOKEN}`).ok).toBe(true); }); it('rejects a foreign host', () => { expect(parseQrPayload(`https://evil.example.com/a/${ID}#${TOKEN}`)).toEqual({ ok: false, error: 'not-an-alarm-code', }); }); it('rejects a foreign scheme', () => { expect(parseQrPayload(`ftp://${QR_HOST}/a/${ID}#${TOKEN}`)).toEqual({ ok: false, error: 'not-an-alarm-code', }); }); it('reports an empty payload', () => { expect(parseQrPayload(' ')).toEqual({ ok: false, error: 'empty' }); }); it('reports a missing token', () => { expect(parseQrPayload(`https://${QR_HOST}/a/${ID}`)).toEqual({ ok: false, error: 'missing-token', }); }); it('reports a missing alert id', () => { expect(parseQrPayload(`https://${QR_HOST}/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 built = buildQrPayload(weird, TOKEN); const res = parseQrPayload(built); expect(res.ok && res.payload.alertId).toBe(weird); }); }); describe('parseManualToken', () => { it('accepts a bare token when the alert id is known', () => { const res = parseManualToken(TOKEN, ID); expect(res).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), '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); } }); });