149 lines
5.8 KiB
TypeScript
149 lines
5.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import test from 'node:test';
|
|
import { sha256Bytes } from '../src/crypto.js';
|
|
import {
|
|
InputAttachmentError,
|
|
createPinnedAttachmentLookup,
|
|
decodeInlineInputAttachment,
|
|
downloadAgentBusInputAttachment,
|
|
parseAgentBusInputAttachment,
|
|
resolveAgentBusAttachmentAddresses,
|
|
validateAgentBusAttachmentUrl
|
|
} from '../src/input-attachment.js';
|
|
|
|
function codeOf(callback: () => unknown): string {
|
|
try {
|
|
callback();
|
|
return '';
|
|
} catch (error) {
|
|
return error instanceof InputAttachmentError ? error.code : String(error);
|
|
}
|
|
}
|
|
|
|
test('manual roster attachment base64 is bounded and hash verified', () => {
|
|
const content = Buffer.from('PK\u0003\u0004synthetic-workbook');
|
|
const decoded = decodeInlineInputAttachment({
|
|
name: 'synthetic.xlsx',
|
|
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
size: content.byteLength,
|
|
sha256: sha256Bytes(content),
|
|
content_base64: content.toString('base64')
|
|
}, 1_000);
|
|
assert.equal(decoded.fileName, 'synthetic.xlsx');
|
|
assert.deepEqual(decoded.content, content);
|
|
assert.equal(decoded.source, 'manual');
|
|
});
|
|
|
|
test('manual roster attachment rejects unsupported names, invalid base64 and mismatched hash', () => {
|
|
assert.equal(codeOf(() => decodeInlineInputAttachment({
|
|
name: 'synthetic.csv', content_base64: 'YQ=='
|
|
}, 1_000)), 'roster_file_type_unsupported');
|
|
assert.equal(codeOf(() => decodeInlineInputAttachment({
|
|
name: 'synthetic.xls', content_base64: '%%%'
|
|
}, 1_000)), 'roster_file_base64_invalid');
|
|
assert.equal(codeOf(() => decodeInlineInputAttachment({
|
|
name: 'synthetic.xls', sha256: 'a'.repeat(64), content_base64: 'YQ=='
|
|
}, 1_000)), 'roster_file_sha256_mismatch');
|
|
});
|
|
|
|
test('AgentBus attachment URLs allow private HTTPS endpoints but reject unsafe URL syntax', () => {
|
|
for (const value of [
|
|
'http://files.example.test/list.xlsx',
|
|
'ftp://files.example.test/list.xlsx',
|
|
'https://user:secret@files.example.test/list.xlsx'
|
|
]) {
|
|
assert.equal(codeOf(() => validateAgentBusAttachmentUrl(value)), 'roster_attachment_url_unsafe');
|
|
}
|
|
for (const value of [
|
|
'https://files.example.com/list.xlsx',
|
|
'https://localhost/list.xlsx',
|
|
'https://127.0.0.1/list.xlsx',
|
|
'https://10.0.0.8/list.xlsx',
|
|
'https://[::1]/list.xlsx'
|
|
]) {
|
|
assert.equal(validateAgentBusAttachmentUrl(value).protocol, 'https:', value);
|
|
}
|
|
});
|
|
|
|
test('AgentBus attachment metadata is normalized without exposing URL credentials', () => {
|
|
const reference = parseAgentBusInputAttachment({
|
|
name: 'synthetic.xls',
|
|
content_type: 'application/vnd.ms-excel',
|
|
size: 128,
|
|
sha256: 'a'.repeat(64),
|
|
url: 'https://files.example.com/roster.xls'
|
|
}, 1_000);
|
|
assert.equal(reference.name, 'synthetic.xls');
|
|
assert.equal(reference.size, 128);
|
|
assert.equal(reference.sha256, 'a'.repeat(64));
|
|
});
|
|
|
|
test('AgentBus attachment DNS accepts internal names and private literal addresses', async () => {
|
|
assert.deepEqual(await resolveAgentBusAttachmentAddresses('10.0.0.8'), [
|
|
{ address: '10.0.0.8', family: 4 }
|
|
]);
|
|
assert.deepEqual(await resolveAgentBusAttachmentAddresses('fd00::8'), [
|
|
{ address: 'fd00::8', family: 6 }
|
|
]);
|
|
const localhost = await resolveAgentBusAttachmentAddresses('localhost');
|
|
assert.ok(localhost.length > 0);
|
|
assert.ok(localhost.every((entry) => entry.family === 4 || entry.family === 6));
|
|
});
|
|
|
|
test('AgentBus attachment DNS pinning follows the Node lookup callback shape', async () => {
|
|
const address = { address: '10.0.0.8', family: 4 };
|
|
const lookup = createPinnedAttachmentLookup(address);
|
|
await new Promise<void>((resolve, reject) => {
|
|
lookup('internal.example', { all: true }, (error, addresses, family) => {
|
|
try {
|
|
assert.ifError(error);
|
|
assert.deepEqual(addresses, [address]);
|
|
assert.equal(family, undefined);
|
|
resolve();
|
|
} catch (assertionError) {
|
|
reject(assertionError);
|
|
}
|
|
});
|
|
});
|
|
await new Promise<void>((resolve, reject) => {
|
|
lookup('internal.example', { all: false }, (error, resolvedAddress, family) => {
|
|
try {
|
|
assert.ifError(error);
|
|
assert.equal(resolvedAddress, address.address);
|
|
assert.equal(family, address.family);
|
|
resolve();
|
|
} catch (assertionError) {
|
|
reject(assertionError);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
test('AgentBus attachment diagnostics expose stages and codes without URL data', async () => {
|
|
const events: Array<{ event: string; metadata: Record<string, unknown> }> = [];
|
|
await assert.rejects(
|
|
() => downloadAgentBusInputAttachment({
|
|
name: 'synthetic.xls',
|
|
contentType: 'application/vnd.ms-excel',
|
|
size: 128,
|
|
url: 'https://user:secret@127.0.0.1/private-roster.xls?token=secret'
|
|
}, 1_000, (event, metadata) => events.push({ event, metadata })),
|
|
(error: unknown) => error instanceof InputAttachmentError
|
|
&& error.code === 'roster_attachment_url_unsafe'
|
|
);
|
|
assert.deepEqual(events.map((event) => event.event), ['download_started', 'download_failed']);
|
|
assert.equal(events[1].metadata.error_code, 'roster_attachment_url_unsafe');
|
|
assert.doesNotMatch(JSON.stringify(events), /127\.0\.0\.1|private-roster|token|secret/u);
|
|
});
|
|
|
|
test('input attachment migration stores only encrypted normalized data and waiting-task index', async () => {
|
|
const sql = await readFile(new URL('../migrations/014_task_input_attachments.sql', import.meta.url), 'utf8');
|
|
assert.match(sql, /CREATE TABLE IF NOT EXISTS task_input_attachments/);
|
|
assert.match(sql, /normalized_text_ciphertext text/);
|
|
assert.match(sql, /file_name_ciphertext text NOT NULL/);
|
|
assert.doesNotMatch(sql, /\n\s*file_name text/);
|
|
assert.doesNotMatch(sql, /public_url|content bytea|content_ciphertext/);
|
|
assert.match(sql, /WHERE status = 'awaiting_attachment'/);
|
|
});
|