153 lines
4.3 KiB
TypeScript
153 lines
4.3 KiB
TypeScript
|
|
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();
|