mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 03:03:24 +08:00
172 lines
6 KiB
JavaScript
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();
|
|
});
|