DodoTransfer/tests/integration/one-time-download.test.js
2026-11-05 10:28:10 +09:00

196 lines
6.5 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const os = require('os');
const http = require('node:http');
const { createHash } = require('node:crypto');
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'psitransfer-onetime-'));
process.env.PSITRANSFER_UPLOAD_DIR = tmpDir;
// these tests upload without a password
process.env.PSITRANSFER_REQUIRE_BUCKET_PASSWORD = 'false';
const { test, before, after } = require('node:test');
const assert = require('node:assert');
const tusMeta = require('../../lib/tusboy/tus-metadata');
const app = require('../../lib/endpoints');
const TUS = { 'Tus-Resumable': '1.0.0' };
const PARALLEL = 10;
let server;
let port;
function request(method, pathname, headers = {}, body = null) {
return new Promise((resolve, reject) => {
const req = http.request(
{ hostname: '127.0.0.1', port, method, path: pathname, headers, agent: false },
(res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) });
});
}
);
req.on('error', reject);
if (body != null) req.write(body);
req.end();
});
}
// Upload a file via Tus and return its fid (sid++key)
async function upload(sid, content, retention = 'one-time') {
const meta = { name: 'file.bin', sid, retention };
const created = await request('POST', '/files/', {
...TUS,
'Upload-Length': String(content.length),
'Upload-Metadata': tusMeta.encode(meta),
});
assert.strictEqual(created.status, 201);
const fid = decodeURIComponent(created.headers.location.split('/').pop());
const patched = await request('PATCH', `/files/${ encodeURIComponent(fid) }`, {
...TUS,
'Upload-Offset': '0',
'Content-Type': 'application/offset+octet-stream',
}, content);
assert.strictEqual(patched.status, 204);
return fid;
}
function archiveUrl(sid, fids, format = 'zip') {
const keyList = fids.map(fid => fid.split('++')[1]).join();
const hash = createHash('sha256').update(keyList).digest('hex').slice(0, 32);
return `/files/${ sid }++${ hash }.${ format }`;
}
// Wait until the server-side finish/close handlers have run
const tick = () => new Promise(resolve => setTimeout(resolve, 100));
before(async () => {
server = http.createServer(app);
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
port = server.address().port;
});
after(async () => {
server.closeAllConnections();
await new Promise((resolve) => server.close(resolve));
fs.rmSync(tmpDir, { recursive: true, force: true });
});
test('parallel downloads of a one-time file deliver it exactly once', async () => {
const content = Buffer.alloc(1024 * 1024, 'a');
const fid = await upload('onetimeparallel', content);
const results = await Promise.all(
Array.from({ length: PARALLEL }, () => request('GET', `/files/${ fid }`))
);
const ok = results.filter(r => r.status === 200);
assert.strictEqual(ok.length, 1);
assert.ok(ok[0].body.equals(content));
assert.strictEqual(results.filter(r => r.status === 404).length, PARALLEL - 1);
await tick();
assert.strictEqual((await request('GET', `/files/${ fid }`)).status, 404);
// server is still alive
assert.strictEqual((await request('GET', '/robots.txt')).status, 200);
});
test('parallel archive downloads of a one-time bucket deliver it exactly once', async () => {
const sid = 'onetimearchive';
const fids = [
await upload(sid, Buffer.alloc(512 * 1024, 'a')),
await upload(sid, Buffer.alloc(512 * 1024, 'b')),
];
const url = archiveUrl(sid, fids);
const results = await Promise.all(
Array.from({ length: PARALLEL }, () => request('GET', url))
);
assert.strictEqual(results.filter(r => r.status === 200).length, 1);
assert.strictEqual(results.filter(r => r.status === 404).length, PARALLEL - 1);
await tick();
for (const fid of fids) {
assert.strictEqual((await request('GET', `/files/${ fid }`)).status, 404);
}
assert.strictEqual((await request('GET', '/robots.txt')).status, 200);
});
test('archive download is refused while a one-time file is being downloaded', async () => {
const sid = 'onetimemixed';
const fids = [
await upload(sid, Buffer.alloc(32 * 1024 * 1024, 'a')),
await upload(sid, Buffer.from('permanent'), '3600'),
];
// start a single file download and keep it open
const pending = new Promise((resolve, reject) => {
const req = http.get({ hostname: '127.0.0.1', port, path: `/files/${ fids[0] }`, agent: false }, (res) => {
res.pause();
resolve({ req, res });
});
req.on('error', reject);
});
const { req, res } = await pending;
assert.strictEqual(res.statusCode, 200);
assert.strictEqual((await request('GET', archiveUrl(sid, fids))).status, 404);
// non one-time files are not affected
assert.strictEqual((await request('GET', `/files/${ fids[1] }`)).status, 200);
req.destroy();
await tick();
});
test('aborted download releases the one-time file', async () => {
const content = Buffer.alloc(32 * 1024 * 1024, 'a');
const fid = await upload('onetimeabort', content);
await new Promise((resolve, reject) => {
const req = http.get({ hostname: '127.0.0.1', port, path: `/files/${ fid }`, agent: false }, (res) => {
assert.strictEqual(res.statusCode, 200);
res.once('data', () => {
req.destroy();
resolve();
});
});
req.on('error', (e) => e.code === 'ECONNRESET' ? resolve() : reject(e));
});
await tick();
const res = await request('GET', `/files/${ fid }`);
assert.strictEqual(res.status, 200);
assert.ok(res.body.equals(content));
});
test('HEAD does not consume a one-time file', async () => {
const content = Buffer.from('head request');
const fid = await upload('onetimehead', content);
const head = await request('HEAD', `/files/${ fid }`);
assert.strictEqual(head.status, 200);
await tick();
const res = await request('GET', `/files/${ fid }`);
assert.strictEqual(res.status, 200);
assert.ok(res.body.equals(content));
await tick();
assert.strictEqual((await request('HEAD', `/files/${ fid }`)).status, 404);
});
test('completed range request consumes a one-time file', async () => {
const fid = await upload('onetimerange', Buffer.from('0123456789abcdef'));
const res = await request('GET', `/files/${ fid }`, { Range: 'bytes=0-9' });
assert.strictEqual(res.status, 206);
assert.strictEqual(res.body.toString(), '0123456789');
await tick();
assert.strictEqual((await request('GET', `/files/${ fid }`)).status, 404);
});