Static frontend with vendored marked/DOMPurify, bounded Markdown pipeline, same-origin mock server with scenario selection, unit, contract and CDP end-to-end tests under frontend/**.
111 lines
3.8 KiB
JavaScript
111 lines
3.8 KiB
JavaScript
/**
|
|
* Unit tests for api.js validation logic and boundaries.
|
|
*/
|
|
|
|
import { test, describe } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { getUtf8ByteLength, validateCredentials, submitQuery } from '../js/api.js';
|
|
|
|
describe('API validation and boundaries', () => {
|
|
test('getUtf8ByteLength correctly calculates ASCII and multibyte UTF-8 lengths', () => {
|
|
assert.equal(getUtf8ByteLength('hello'), 5);
|
|
// Multibyte characters:
|
|
// '€' is 3 bytes (0xE2 0x82 0xAC)
|
|
// '🚀' is 4 bytes (0xF0 0x9F 0x99 0x80)
|
|
assert.equal(getUtf8ByteLength('€'), 3);
|
|
assert.equal(getUtf8ByteLength('🚀'), 4);
|
|
assert.equal(getUtf8ByteLength('こんにちは'), 15); // 5 x 3 bytes
|
|
});
|
|
|
|
test('validateCredentials validates valid HTTP and HTTPS URLs', () => {
|
|
assert.doesNotThrow(() => {
|
|
validateCredentials({
|
|
url: 'https://confluence.example.com',
|
|
pat: 'valid-pat-string'
|
|
});
|
|
});
|
|
|
|
assert.doesNotThrow(() => {
|
|
validateCredentials({
|
|
url: 'http://localhost:8080/confluence',
|
|
pat: 'pat-token'
|
|
});
|
|
});
|
|
});
|
|
|
|
test('validateCredentials rejects empty or missing fields', () => {
|
|
assert.throws(
|
|
() => validateCredentials({ url: '', pat: 'pat' }),
|
|
(err) => err.code === 'invalid_input'
|
|
);
|
|
|
|
assert.throws(
|
|
() => validateCredentials({ url: 'https://example.com', pat: ' ' }),
|
|
(err) => err.code === 'invalid_input'
|
|
);
|
|
});
|
|
|
|
test('validateCredentials rejects invalid protocols like javascript: or file:', () => {
|
|
assert.throws(
|
|
() => validateCredentials({ url: 'javascript:alert(1)', pat: 'pat' }),
|
|
(err) => err.code === 'invalid_input'
|
|
);
|
|
|
|
assert.throws(
|
|
() => validateCredentials({ url: 'file:///etc/passwd', pat: 'pat' }),
|
|
(err) => err.code === 'invalid_input'
|
|
);
|
|
});
|
|
|
|
test('validateCredentials exact 8 KiB boundary check', () => {
|
|
// Exactly 8192 bytes (8 KiB) passes
|
|
const exact8KiBPat = 'a'.repeat(8192);
|
|
assert.doesNotThrow(() => {
|
|
validateCredentials({ url: 'https://example.com', pat: exact8KiBPat });
|
|
});
|
|
|
|
// 8193 bytes fails
|
|
const over8KiBPat = 'a'.repeat(8193);
|
|
assert.throws(
|
|
() => validateCredentials({ url: 'https://example.com', pat: over8KiBPat }),
|
|
(err) => err.code === 'invalid_input' && err.message.includes('8 KiB')
|
|
);
|
|
|
|
// Multibyte 8 KiB boundary: 2048 emojis = 8192 bytes (passes)
|
|
const exact8KiBEmoji = '🚀'.repeat(2048);
|
|
assert.equal(getUtf8ByteLength(exact8KiBEmoji), 8192);
|
|
assert.doesNotThrow(() => {
|
|
validateCredentials({ url: 'https://example.com', pat: exact8KiBEmoji });
|
|
});
|
|
|
|
// 2049 emojis = 8196 bytes (fails)
|
|
const over8KiBEmoji = exact8KiBEmoji + '🚀';
|
|
assert.throws(
|
|
() => validateCredentials({ url: 'https://example.com', pat: over8KiBEmoji }),
|
|
(err) => err.code === 'invalid_input' && err.message.includes('8 KiB')
|
|
);
|
|
});
|
|
|
|
test('runtime 16 MiB multibyte UTF-8 boundary validation on prompt', async () => {
|
|
const validCredentials = { url: 'https://approved.example.com', pat: 'dummy-pat-123' };
|
|
|
|
// Construct a real 16 MiB string containing 4-byte multibyte emojis at runtime
|
|
// 4 bytes * 4,194,304 = 16,777,216 bytes (exactly 16 MiB)
|
|
const chunk = '🚀'.repeat(1024); // 4096 bytes
|
|
const exactly16MiBPrompt = chunk.repeat(4096); // 16 MiB
|
|
assert.equal(getUtf8ByteLength(exactly16MiBPrompt), 16 * 1024 * 1024);
|
|
|
|
// Prompt exceeding 16 MiB by 1 byte
|
|
const over16MiBPrompt = exactly16MiBPrompt + 'a';
|
|
assert.equal(getUtf8ByteLength(over16MiBPrompt), 16 * 1024 * 1024 + 1);
|
|
|
|
// Rejection above 16 MiB
|
|
await assert.rejects(
|
|
async () => {
|
|
await submitQuery({ prompt: over16MiBPrompt, credentials: validCredentials });
|
|
},
|
|
(err) => err.code === 'invalid_input' && err.message.includes('16 MiB')
|
|
);
|
|
});
|
|
});
|