import test from 'node:test'; import assert from 'node:assert/strict'; import { Client, ApiError, normalizeServer, ago, urlBase64ToUint8Array } from '../public/api.js'; const jsonRes = (status, body) => ({ ok: status < 300, status, text: async () => JSON.stringify(body) }); test('normalizeServer adds a scheme and drops paths', () => { assert.equal(normalizeServer('sandbar.example.com/app/'), 'https://sandbar.example.com'); assert.equal(normalizeServer('localhost:8090'), 'http://localhost:8090'); assert.equal(normalizeServer(' https://x.test:8443/y '), 'https://x.test:8443'); assert.throws(() => normalizeServer(''), /address/); }); test('login posts credentials and returns the token', async () => { let seen; const f = async (url, init) => { seen = { url, init }; return jsonRes(200, { token: 'sb_abc', keyId: 'k1', user: { name: 'Ana' } }); }; const r = await Client.login('http://s', 'a@b.c', 'pw', 'Pixel', f); assert.equal(r.token, 'sb_abc'); assert.equal(seen.url, 'http://s/api/v1/mobile/login'); assert.deepEqual(JSON.parse(seen.init.body), { email: 'a@b.c', password: 'pw', device: 'Pixel' }); assert.equal(seen.init.headers.Authorization, undefined); }); test('errors carry the server message and status', async () => { const c = new Client('http://s', 't', async () => jsonRes(401, { error: 'Wrong email or password' })); await assert.rejects(c.alerts(), (e) => e instanceof ApiError && e.status === 401 && e.message === 'Wrong email or password'); const down = new Client('http://s', 't', async () => { throw new TypeError('fetch failed'); }); await assert.rejects(down.alerts(), (e) => e.status === 0 && /Could not reach/.test(e.message)); }); test('alert actions hit the right endpoints with a bearer token', async () => { const calls = []; const c = new Client('http://s', 'tok', async (url, init) => { calls.push([init.method, url, init.headers.Authorization, init.body]); return jsonRes(200, { ok: true }); }); await c.ack('a1'); await c.snooze('a1', 30); await c.escalate('a1'); await c.resolve('a1'); assert.deepEqual(calls.map((x) => x.slice(0, 3)), [ ['POST', 'http://s/api/v1/alerts/a1/ack', 'Bearer tok'], ['POST', 'http://s/api/v1/alerts/a1/snooze', 'Bearer tok'], ['POST', 'http://s/api/v1/alerts/a1/escalate', 'Bearer tok'], ['POST', 'http://s/api/v1/alerts/a1/resolve', 'Bearer tok'], ]); assert.equal(calls[1][3], '{"minutes":30}'); }); test('stream parses newline-delimited messages split across chunks', async () => { const enc = new TextEncoder(); const chunks = ['{"event":"open"}\n{"event":"message","id":"1","mess', 'age":"hi"}\n\n{"event":"message","id":"2","message":"yo"}\n']; const body = new ReadableStream({ start(c) { chunks.forEach((x) => c.enqueue(enc.encode(x))); c.close(); } }); const got = []; const c = new Client('http://s', 't', async () => ({ ok: true, status: 200, body })); await c.stream('alerts', 'all', (m) => got.push(m.message), new AbortController().signal); assert.deepEqual(got, ['hi', 'yo']); }); test('ago and key decoding', () => { assert.equal(ago(1000, 1030), 'just now'); assert.equal(ago(1000, 1000 + 5 * 60), '5 min ago'); assert.equal(ago(1000, 1000 + 3 * 3600), '3 h ago'); assert.deepEqual([...urlBase64ToUint8Array('AQID')], [1, 2, 3]); });