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