dsh-im-ops/test/inbound-file.test.mjs

172 lines
6 KiB
JavaScript

import assert from 'node:assert/strict';
import {
mkdtemp,
readdir,
readFile,
rm,
} from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { isAbsolute, join, resolve } from 'node:path';
import { Readable } from 'node:stream';
import test from 'node:test';
import {
InboundFileError,
stageInboundFiles,
} from '../src/channels/shared/inbound-file.mjs';
async function workspace(t) {
const directory = await mkdtemp(join(tmpdir(), 'dsh-im-inbound-file-'));
t.after(() => rm(directory, { recursive: true, force: true }));
return directory;
}
function absoluteStagedPath(root, file) {
assert.equal(isAbsolute(file.path), false, 'Harness receives a workspace-relative path');
const path = resolve(root, file.path);
assert.equal(path.startsWith(`${resolve(root)}/`), true, 'staged path stays in the Session cwd');
return path;
}
test('stageInboundFiles preserves exact bytes, including a zero-byte file', async (t) => {
const root = await workspace(t);
const binary = Buffer.from([0x00, 0xff, 0x01, 0x80, 0x0a, 0x0d]);
const staged = await stageInboundFiles({
files: [
{ name: 'binary.dat', data: binary, mediaType: 'application/octet-stream' },
{ name: 'empty.txt', data: Buffer.alloc(0), mediaType: 'text/plain' },
],
}, { workspace: root });
assert.deepEqual(staged.files.map(({ name, mediaType }) => ({ name, mediaType })), [
{ name: 'binary.dat', mediaType: 'application/octet-stream' },
{ name: 'empty.txt', mediaType: 'text/plain' },
]);
assert.deepEqual(
await readFile(absoluteStagedPath(root, staged.files[0])),
binary,
);
assert.deepEqual(
await readFile(absoluteStagedPath(root, staged.files[1])),
Buffer.alloc(0),
);
const turnDirectory = resolve(root, staged.files[0].path, '..');
await staged.cleanup();
await assert.rejects(readFile(resolve(turnDirectory, '01-binary.dat')), /ENOENT/);
});
test('stageInboundFiles writes async streams without changing their bytes', async (t) => {
const root = await workspace(t);
const chunks = [
Buffer.from([0xde, 0xad]),
new Uint8Array([0xbe, 0xef]),
Buffer.from('stream tail'),
];
const expected = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
const staged = await stageInboundFiles({
files: [{
name: 'stream.bin',
load: async () => ({
stream: Readable.from(chunks),
name: 'provider-stream.bin',
mimetype: 'application/x-provider-stream',
}),
}],
}, { workspace: root });
assert.deepEqual(staged.files, [{
name: 'provider-stream.bin',
path: staged.files[0].path,
mediaType: 'application/x-provider-stream',
}]);
assert.deepEqual(await readFile(absoluteStagedPath(root, staged.files[0])), expected);
await staged.cleanup();
});
test('stageInboundFiles reports failures raised while streaming provider bytes as file downloads', async (t) => {
const root = await workspace(t);
await assert.rejects(stageInboundFiles({
files: [{
name: 'broken.bin',
load: async () => ({
stream: Readable.from((async function* brokenStream() {
yield Buffer.from('partial');
throw new Error('provider stream disconnected');
}())),
}),
}],
}, { workspace: root }), (error) => (
error instanceof InboundFileError
&& error.code === 'inbound-file-download-failed'
&& error.userMessage === '文件下载失败,请重新发送后再试。'
&& error.cause?.message === 'provider stream disconnected'
));
assert.deepEqual(await readdir(join(root, '.dsh-im', 'inbound')), []);
});
test('stageInboundFiles removes the whole partial batch when any file download fails', async (t) => {
const root = await workspace(t);
let secondAttempted = false;
await assert.rejects(
stageInboundFiles({
files: [
{ name: 'first.txt', data: Buffer.from('must be removed') },
{
name: 'second.txt',
async load() {
secondAttempted = true;
throw new Error('private signed URL expired');
},
},
{
name: 'third.txt',
async load() {
assert.fail('files after a failed member must not be downloaded');
},
},
],
}, { workspace: root }),
(error) => error instanceof InboundFileError
&& error.code === 'inbound-file-download-failed'
&& error.userMessage === '文件下载失败,请重新发送后再试。',
);
assert.equal(secondAttempted, true);
assert.deepEqual(await readdir(join(root, '.dsh-im', 'inbound')), []);
});
test('stageInboundFiles keeps display names but makes traversal-like storage names path-safe', async (t) => {
const root = await workspace(t);
const staged = await stageInboundFiles({
files: [{
name: '../../private\\folder/..\\report:?*.txt\u0000\n',
data: Buffer.from('safe'),
}],
}, { workspace: root });
assert.equal(staged.files[0].name, 'report:?*.txt');
assert.match(staged.files[0].path, /^\.dsh-im\/inbound\/turn-[^/]+\/01-report___\.txt$/);
assert.equal(await readFile(absoluteStagedPath(root, staged.files[0]), 'utf8'), 'safe');
await staged.cleanup();
});
test('stageInboundFiles adds no count, extension, MIME, or declared-size acceptance limits', async (t) => {
const root = await workspace(t);
const sources = Array.from({ length: 33 }, (_, index) => ({
name: index === 0 ? 'no-extension' : `unknown-${index}.unsupported-${index}`,
mediaType: `application/x-unknown-${index}`,
size: Number.MAX_SAFE_INTEGER,
data: Buffer.from(`file-${index}`),
}));
const staged = await stageInboundFiles({ files: sources }, { workspace: root });
assert.equal(staged.files.length, 33);
assert.deepEqual(
await Promise.all(staged.files.map((file) => readFile(absoluteStagedPath(root, file), 'utf8'))),
sources.map((_source, index) => `file-${index}`),
);
assert.equal(staged.files[0].name, 'no-extension');
assert.equal(staged.files.at(-1).mediaType, 'application/x-unknown-32');
await staged.cleanup();
});