import { test } from 'node:test'; import assert from 'node:assert/strict'; import { validateKeys, MAX_OBJECT_BYTES, metadata } from '../s3.mjs'; import { cleanupProbe, atomicJson } from '../s3.mjs'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; test('accepts FEDEO keys, special characters, empty objects and size boundary', () => { validateKeys([{ Key: 'tenant/filesbyid/123/รค + %#.pdf', Size: 0 }, { Key: 'export.zip', Size: MAX_OBJECT_BYTES }]); }); test('blocks lossy paths and file/directory collisions regardless of ordering', () => { for (const Key of ['/a', 'a/', 'a//b', 'a/../b', './a', 'a\\b', 'a\nb']) assert.throws(() => validateKeys([{ Key, Size: 1 }])); assert.throws(() => validateKeys([{ Key: 'a/b', Size: 1 }, { Key: 'a', Size: 1 }])); assert.throws(() => validateKeys([{ Key: 'a', Size: MAX_OBJECT_BYTES + 1 }])); }); test('rejects encryption and nonportable attributes rather than dropping them', () => { for (const value of [{ ServerSideEncryption: 'AES256' }, { ObjectLockMode: 'GOVERNANCE' }, { StorageClass: 'GLACIER' }, { WebsiteRedirectLocation: '/other' }]) assert.throws(() => metadata(value)); assert.deepEqual(metadata({ ContentType: 'application/pdf', Metadata: { custom: 'value' } }), { ContentType: 'application/pdf', Metadata: { custom: 'value' } }); }); test('interrupted contract cleanup deletes only its two journalled probe objects', async () => { const directory = await mkdtemp(join(tmpdir(), 'fedeo-probe-test-')); try { const path = join(directory, 'probe.json'); const key = 'fedeo-contract-11111111-1111-1111-1111-111111111111/test'; await atomicJson(path, { key }); const inputs = []; await cleanupProbe({ send: async command => { inputs.push(command.input); return {}; } }, 'bucket', path); assert.deepEqual(inputs, [{ Bucket: 'bucket', Key: key }, { Bucket: 'bucket', Key: key + '.stream' }]); await atomicJson(path, { key: 'real-business-file.pdf' }); await assert.rejects(cleanupProbe({ send: async () => { throw new Error('must not send'); } }, 'bucket', path), /Probejournal/); } finally { await rm(directory, { recursive: true, force: true }); } });