Files
alarm-client/__tests__/qrPayload.test.ts
T
2026-10-18 22:42:00 +01:00

158 lines
5.0 KiB
TypeScript

/**
* 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);
}
});
});