Artur Mukhamadiev a9908a533f frontend: web UI track handoff (contract revision 1)
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/**.
2026-09-14 21:57:54 +03:00

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')
);
});
});