import { expect, tap } from '@push.rocks/tapbundle'; import { encodeConnectionToken, decodeConnectionToken, type IConnectionTokenData } from '../ts/classes.token.js'; tap.test('token roundtrip with unicode chars in secret', async () => { const data: IConnectionTokenData = { hubHost: 'hub.example.com', hubPort: 8443, edgeId: 'edge-1', secret: 'sécret-with-ünïcödé-日本語', }; const token = encodeConnectionToken(data); const decoded = decodeConnectionToken(token); expect(decoded.secret).toEqual(data.secret); }); tap.test('token roundtrip with empty edgeId', async () => { const data: IConnectionTokenData = { hubHost: 'hub.test', hubPort: 443, edgeId: '', secret: 'key', }; const token = encodeConnectionToken(data); const decoded = decodeConnectionToken(token); expect(decoded.edgeId).toEqual(''); }); tap.test('token roundtrip with port 0', async () => { const data: IConnectionTokenData = { hubHost: 'h', hubPort: 0, edgeId: 'e', secret: 's', }; const token = encodeConnectionToken(data); const decoded = decodeConnectionToken(token); expect(decoded.hubPort).toEqual(0); }); tap.test('token roundtrip with port 65535', async () => { const data: IConnectionTokenData = { hubHost: 'h', hubPort: 65535, edgeId: 'e', secret: 's', }; const token = encodeConnectionToken(data); const decoded = decodeConnectionToken(token); expect(decoded.hubPort).toEqual(65535); }); tap.test('token roundtrip with very long secret (10k chars)', async () => { const longSecret = 'x'.repeat(10000); const data: IConnectionTokenData = { hubHost: 'host', hubPort: 1234, edgeId: 'edge', secret: longSecret, }; const token = encodeConnectionToken(data); const decoded = decodeConnectionToken(token); expect(decoded.secret).toEqual(longSecret); expect(decoded.secret.length).toEqual(10000); }); tap.test('token string is URL-safe', async () => { const data: IConnectionTokenData = { hubHost: 'hub.example.com', hubPort: 8443, edgeId: 'edge-001', secret: 'super+secret/key==with+special/chars', }; const token = encodeConnectionToken(data); expect(token).toMatch(/^[A-Za-z0-9_-]+$/); }); tap.test('decode empty string throws', async () => { let error: Error | undefined; try { decodeConnectionToken(''); } catch (e) { error = e as Error; } expect(error).toBeInstanceOf(Error); }); tap.test('decode valid base64 but wrong JSON shape throws missing required fields', async () => { // Encode { "a": 1, "b": 2 } — valid JSON but wrong shape const token = Buffer.from(JSON.stringify({ a: 1, b: 2 }), 'utf-8') .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); let error: Error | undefined; try { decodeConnectionToken(token); } catch (e) { error = e as Error; } expect(error).toBeInstanceOf(Error); expect(error!.message).toInclude('missing required fields'); }); tap.test('decode valid JSON but wrong field types throws missing required fields', async () => { // h is number instead of string, p is string instead of number const token = Buffer.from(JSON.stringify({ h: 123, p: 'notnum', e: 'e', s: 's' }), 'utf-8') .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); let error: Error | undefined; try { decodeConnectionToken(token); } catch (e) { error = e as Error; } expect(error).toBeInstanceOf(Error); expect(error!.message).toInclude('missing required fields'); }); tap.test('decode with extra fields succeeds', async () => { const token = Buffer.from( JSON.stringify({ h: 'host', p: 443, e: 'edge', s: 'secret', extra: 'ignored' }), 'utf-8', ) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); const decoded = decodeConnectionToken(token); expect(decoded.hubHost).toEqual('host'); expect(decoded.hubPort).toEqual(443); expect(decoded.edgeId).toEqual('edge'); expect(decoded.secret).toEqual('secret'); }); tap.test('encode is deterministic', async () => { const data: IConnectionTokenData = { hubHost: 'hub.test', hubPort: 8443, edgeId: 'edge-1', secret: 'deterministic-key', }; const token1 = encodeConnectionToken(data); const token2 = encodeConnectionToken(data); expect(token1).toEqual(token2); }); export default tap.start();