init
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user