484 lines
24 KiB
JavaScript
484 lines
24 KiB
JavaScript
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const os = require('os');
|
||
const http = require('node:http');
|
||
|
||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'psitransfer-features-'));
|
||
process.env.PSITRANSFER_UPLOAD_DIR = tmpDir;
|
||
process.env.PSITRANSFER_UPLOAD_PASS = 'up-secret';
|
||
process.env.PSITRANSFER_ADMIN_PASS = 'ad-secret';
|
||
process.env.PSITRANSFER_ACCESS_LOG = '';
|
||
|
||
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 PNG = Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), Buffer.alloc(64)]);
|
||
const ADMIN = { 'x-passwd': 'ad-secret' };
|
||
|
||
let server;
|
||
let port;
|
||
let token;
|
||
|
||
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', () => {
|
||
const buf = Buffer.concat(chunks);
|
||
let json;
|
||
try { json = JSON.parse(buf.toString()); } catch (e) { /* not json */ }
|
||
resolve({ status: res.statusCode, headers: res.headers, body: buf, text: buf.toString(), json });
|
||
});
|
||
}
|
||
);
|
||
req.on('error', reject);
|
||
if (body != null) req.write(body);
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
const bearer = () => ({ Authorization: `Bearer ${ token }` });
|
||
const jsonBody = (obj) => [{ 'Content-Type': 'application/json' }, JSON.stringify(obj)];
|
||
|
||
/** Upload one file via Tus, returns { fid, key, created } or the failed creation response */
|
||
async function upload({ sid, name = 'file.txt', content = 'hello', retention = '3600', password, internalBypass, headers = bearer() }) {
|
||
const meta = { name, sid, retention };
|
||
if (password) meta.password = password;
|
||
if (internalBypass) meta.internalBypass = internalBypass;
|
||
const created = await request('POST', '/files/', {
|
||
...TUS, ...headers,
|
||
'Upload-Length': String(Buffer.byteLength(content)),
|
||
'Upload-Metadata': tusMeta.encode(meta),
|
||
});
|
||
if (created.status !== 201) return { created };
|
||
const fid = decodeURIComponent(created.headers.location.split('/').pop());
|
||
const patched = await request('PATCH', `/files/${ encodeURIComponent(fid) }`, {
|
||
...TUS, ...headers,
|
||
'Upload-Offset': '0',
|
||
'Content-Type': 'application/offset+octet-stream',
|
||
}, content);
|
||
assert.strictEqual(patched.status, 204);
|
||
return { fid, key: fid.split('++')[1], created };
|
||
}
|
||
|
||
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;
|
||
const login = await request('POST', '/login', ...jsonBody({ password: 'up-secret' }));
|
||
assert.strictEqual(login.status, 200);
|
||
token = login.json.token;
|
||
});
|
||
|
||
after(async () => {
|
||
server.closeAllConnections();
|
||
await new Promise((resolve) => server.close(resolve));
|
||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||
});
|
||
|
||
// ---------------------------------------------------------------- login
|
||
|
||
test('login token: wrong password, right password, token reuse', async () => {
|
||
const wrong = await request('POST', '/login', ...jsonBody({ password: 'nope' }));
|
||
assert.strictEqual(wrong.status, 403);
|
||
|
||
assert.ok(token && token.includes('.'));
|
||
|
||
assert.strictEqual((await request('GET', '/config.json')).status, 401);
|
||
assert.strictEqual((await request('GET', '/config.json', { Authorization: 'Bearer garbage' })).status, 403);
|
||
|
||
const withToken = await request('GET', '/config.json', bearer());
|
||
assert.strictEqual(withToken.status, 200);
|
||
assert.ok(withToken.json.session.token, 'the token is renewed on use');
|
||
assert.ok(withToken.json.session.exp > Date.now() + 6 * 24 * 3600 * 1000, 'valid for about 7 days');
|
||
assert.deepStrictEqual(withToken.json.quickRetentions, ['one-time', '86400', '259200', '432000', '604800']);
|
||
assert.strictEqual(withToken.json.authRequired, true);
|
||
});
|
||
|
||
test('legacy x-passwd header still works for scripts', async () => {
|
||
const res = await request('GET', '/config.json', { 'x-passwd': 'up-secret' });
|
||
assert.strictEqual(res.status, 200);
|
||
const up = await upload({ sid: 'legacyheader', headers: { 'x-passwd': 'up-secret' }, password: 'pw' });
|
||
assert.ok(up.fid);
|
||
});
|
||
|
||
test('uploading needs a login', async () => {
|
||
const meta = tusMeta.encode({ name: 'x', sid: 'nologin', retention: '3600', password: 'p' });
|
||
const res = await request('POST', '/files/', { ...TUS, 'Upload-Length': '1', 'Upload-Metadata': meta });
|
||
assert.strictEqual(res.status, 401);
|
||
});
|
||
|
||
// ------------------------------------------------------- password rules
|
||
|
||
test('a password is required except for one-time downloads', async () => {
|
||
const noPw = await upload({ sid: 'needpw', retention: '3600' });
|
||
assert.strictEqual(noPw.created.status, 400);
|
||
assert.match(noPw.created.json.message, /password/i);
|
||
|
||
const oneTime = await upload({ sid: 'needpw1', retention: 'one-time' });
|
||
assert.ok(oneTime.fid, 'one-time may go without password');
|
||
|
||
const withPw = await upload({ sid: 'needpw2', retention: '3600', password: 'pw' });
|
||
assert.ok(withPw.fid);
|
||
});
|
||
|
||
test('metadata the server owns can not be forged by the client', async () => {
|
||
const up = await upload({ sid: 'forgery', password: 'pw' });
|
||
const meta = tusMeta.encode({ name: 'a.txt', sid: 'forgery', retention: '3600', password: 'pw', thumb: 'text/html', lastDownload: '1', bucketLocked: 'true' });
|
||
const res = await request('POST', '/files/', { ...TUS, ...bearer(), 'Upload-Length': '1', 'Upload-Metadata': meta });
|
||
assert.strictEqual(res.status, 201);
|
||
const list = await request('GET', '/forgery.json', { 'x-download-pass': 'pw' });
|
||
const forged = list.json.items.find(i => i.key !== up.key);
|
||
// unfinished uploads are not listed
|
||
assert.strictEqual(forged, undefined);
|
||
});
|
||
|
||
// -------------------------------------------------- listing and bypass
|
||
|
||
test('download list: password, wrong password, no password leak', async () => {
|
||
const { key } = await upload({ sid: 'plainlist', password: 'secret1', name: 'doc.txt' });
|
||
assert.strictEqual((await request('GET', '/plainlist.json')).status, 401);
|
||
assert.strictEqual((await request('GET', '/plainlist.json', { 'x-download-pass': 'bad' })).status, 401);
|
||
// a login alone does not skip a password unless the uploader allowed it
|
||
assert.strictEqual((await request('GET', '/plainlist.json', bearer())).status, 401);
|
||
|
||
const ok = await request('GET', '/plainlist.json', { 'x-download-pass': 'secret1' });
|
||
assert.strictEqual(ok.status, 200);
|
||
assert.strictEqual(ok.json.bypassed, false);
|
||
const item = ok.json.items[0];
|
||
assert.strictEqual(item.key, key);
|
||
assert.strictEqual(item.metadata.hasPassword, true);
|
||
assert.strictEqual(item.metadata.password, undefined, 'the hash is not sent');
|
||
assert.strictEqual(ok.json.session, null, 'no login, no token');
|
||
assert.ok(ok.json.expiresAt > Date.now());
|
||
assert.strictEqual(item.expiresAt, ok.json.expiresAt);
|
||
});
|
||
|
||
test('logged in users skip the password when the uploader allowed it', async () => {
|
||
await upload({ sid: 'bypassed', password: 'secret2', internalBypass: 'true' });
|
||
assert.strictEqual((await request('GET', '/bypassed.json')).status, 401);
|
||
assert.strictEqual((await request('GET', '/bypassed.json', { Authorization: 'Bearer garbage' })).status, 401);
|
||
|
||
const viaLogin = await request('GET', '/bypassed.json', bearer());
|
||
assert.strictEqual(viaLogin.status, 200);
|
||
assert.strictEqual(viaLogin.json.bypassed, true);
|
||
assert.ok(viaLogin.json.session.token, 'using the download page renews the login');
|
||
|
||
// everybody else still needs the password
|
||
const viaPass = await request('GET', '/bypassed.json', { 'x-download-pass': 'secret2' });
|
||
assert.strictEqual(viaPass.status, 200);
|
||
assert.strictEqual(viaPass.json.bypassed, false);
|
||
});
|
||
|
||
test('the bypass flag is ignored without a password', async () => {
|
||
const { fid } = await upload({ sid: 'bypassnopw', retention: 'one-time', internalBypass: 'true' });
|
||
const info = JSON.parse(fs.readFileSync(path.join(tmpDir, ...fid.split('++')) + '.json', 'utf8'));
|
||
assert.strictEqual(info.metadata.internalBypass, undefined);
|
||
assert.strictEqual(info.metadata.password, undefined);
|
||
});
|
||
|
||
test('archive of a bucket needs the token from the list', async () => {
|
||
await upload({ sid: 'arch1', password: 'pw', name: 'a.txt', content: 'AAA' });
|
||
await upload({ sid: 'arch1', password: 'pw', name: 'b.txt', content: 'BBB' });
|
||
const list = await request('GET', '/arch1.json', { 'x-download-pass': 'pw' });
|
||
const res = await request('GET', `/files/arch1++${ list.json.archiveToken }.zip`);
|
||
assert.strictEqual(res.status, 200);
|
||
assert.strictEqual(res.headers['content-type'], 'application/zip');
|
||
assert.strictEqual(res.body.subarray(0, 2).toString(), 'PK');
|
||
const bad = await request('GET', '/files/arch1++' + '0'.repeat(32) + '.zip');
|
||
assert.strictEqual(bad.status, 404);
|
||
});
|
||
|
||
// -------------------------------------------------------------- preview
|
||
|
||
test('inline preview: type from the file name, not a download, no one-time', async () => {
|
||
const { fid, key } = await upload({ sid: 'preview1', password: 'pw', name: 'note.txt', content: '<b>hi</b>' });
|
||
|
||
const res = await request('GET', `/files/${ fid }?inline=1`);
|
||
assert.strictEqual(res.status, 200);
|
||
assert.strictEqual(res.headers['content-type'], 'text/plain; charset=utf-8');
|
||
assert.strictEqual(res.headers['content-disposition'], 'inline');
|
||
assert.strictEqual(res.headers['x-content-type-options'], 'nosniff');
|
||
assert.match(res.headers['content-security-policy'], /sandbox/);
|
||
await tick();
|
||
|
||
let list = await request('GET', '/preview1.json', { 'x-download-pass': 'pw' });
|
||
assert.strictEqual(list.json.items[0].metadata.lastDownload, undefined, 'a preview is not a download');
|
||
assert.strictEqual(list.json.items[0].previewType, 'text');
|
||
|
||
const dl = await request('GET', `/files/${ fid }`);
|
||
assert.strictEqual(dl.status, 200);
|
||
assert.match(dl.headers['content-disposition'], /^attachment; filename="note.txt"/);
|
||
await tick();
|
||
list = await request('GET', '/preview1.json', { 'x-download-pass': 'pw' });
|
||
assert.ok(list.json.items.find(i => i.key === key).metadata.lastDownload, 'a download is counted');
|
||
});
|
||
|
||
test('inline preview: html and unknown types are refused, pdf has no sandbox', async () => {
|
||
const html = await upload({ sid: 'preview2', password: 'pw', name: 'page.html', content: '<script>alert(1)</script>' });
|
||
assert.strictEqual((await request('GET', `/files/${ html.fid }?inline=1`)).status, 404);
|
||
const bin = await upload({ sid: 'preview2', password: 'pw', name: 'data.bin' });
|
||
assert.strictEqual((await request('GET', `/files/${ bin.fid }?inline=1`)).status, 404);
|
||
|
||
const pdf = await upload({ sid: 'preview2', password: 'pw', name: 'doc.pdf', content: '%PDF-1.4' });
|
||
const res = await request('GET', `/files/${ pdf.fid }?inline=1`);
|
||
assert.strictEqual(res.status, 200);
|
||
assert.strictEqual(res.headers['content-type'], 'application/pdf');
|
||
assert.strictEqual(res.headers['content-security-policy'], undefined);
|
||
});
|
||
|
||
test('inline preview never uses up a one-time file', async () => {
|
||
const { fid } = await upload({ sid: 'preview3', name: 'once.txt', retention: 'one-time', content: 'secret' });
|
||
assert.strictEqual((await request('GET', `/files/${ fid }?inline=1`)).status, 404);
|
||
const dl = await request('GET', `/files/${ fid }`);
|
||
assert.strictEqual(dl.status, 200);
|
||
assert.strictEqual(dl.text, 'secret');
|
||
});
|
||
|
||
// ------------------------------------------------------------ thumbnails
|
||
|
||
test('thumbnails: upload, serve, delete with the file', async () => {
|
||
const { fid, key } = await upload({ sid: 'thumbs1', password: 'pw', name: 'pic.png', content: 'x' });
|
||
|
||
assert.strictEqual((await request('PUT', `/files/${ encodeURIComponent(fid) }.thumb`, {}, PNG)).status, 401);
|
||
const bad = await request('PUT', `/files/${ encodeURIComponent(fid) }.thumb`, bearer(), Buffer.from('<html>not an image</html>'));
|
||
assert.strictEqual(bad.status, 400);
|
||
|
||
const ok = await request('PUT', `/files/${ encodeURIComponent(fid) }.thumb`, { ...bearer(), 'Content-Type': 'image/png' }, PNG);
|
||
assert.strictEqual(ok.status, 204);
|
||
|
||
const list = await request('GET', '/thumbs1.json', { 'x-download-pass': 'pw' });
|
||
const item = list.json.items[0];
|
||
assert.match(item.thumbUrl, /files\/thumbs1\+\+.+\.thumb$/);
|
||
|
||
const img = await request('GET', item.thumbUrl.replace(/^\//, '/'));
|
||
assert.strictEqual(img.status, 200);
|
||
assert.strictEqual(img.headers['content-type'], 'image/png');
|
||
assert.strictEqual(img.headers['x-content-type-options'], 'nosniff');
|
||
assert.ok(img.body.equals(PNG));
|
||
|
||
const thumbFile = path.join(tmpDir, 'thumbs1', key) + '.thumb';
|
||
assert.ok(fs.existsSync(thumbFile));
|
||
|
||
// too large
|
||
const big = await request('PUT', `/files/${ encodeURIComponent(fid) }.thumb`, bearer(), Buffer.concat([PNG, Buffer.alloc(70 * 1024)]));
|
||
assert.strictEqual(big.status, 413);
|
||
|
||
// locked buckets take no more thumbnails
|
||
assert.strictEqual((await request('PATCH', '/files/thumbs1?lock=yes', { ...TUS, ...bearer() })).status, 204);
|
||
const locked = await request('PUT', `/files/${ encodeURIComponent(fid) }.thumb`, bearer(), PNG);
|
||
assert.strictEqual(locked.status, 400);
|
||
|
||
// deleting the file deletes the thumbnail
|
||
const del = await request('DELETE', `/admin/buckets/thumbs1/files/${ key }`, ADMIN);
|
||
assert.strictEqual(del.status, 200);
|
||
assert.ok(!fs.existsSync(thumbFile));
|
||
assert.strictEqual((await request('GET', item.thumbUrl)).status, 404);
|
||
});
|
||
|
||
test('one-time files show no thumbnail', async () => {
|
||
const { fid } = await upload({ sid: 'thumbs2', name: 'pic.png', retention: 'one-time' });
|
||
const put = await request('PUT', `/files/${ encodeURIComponent(fid) }.thumb`, bearer(), PNG);
|
||
assert.strictEqual(put.status, 204);
|
||
const list = await request('GET', '/thumbs2.json');
|
||
assert.strictEqual(list.json.items[0].thumbUrl, null);
|
||
assert.strictEqual((await request('GET', `/files/${ fid }.thumb`)).status, 404);
|
||
assert.strictEqual((await request('GET', `/files/${ fid }`)).status, 200);
|
||
});
|
||
|
||
// ----------------------------------------------------------------- admin
|
||
|
||
test('admin area needs the admin password', async () => {
|
||
assert.strictEqual((await request('GET', '/admin/data.json')).status, 401);
|
||
assert.strictEqual((await request('GET', '/admin/data.json', { 'x-passwd': 'up-secret' })).status, 403);
|
||
assert.strictEqual((await request('GET', '/admin/data.json', bearer())).status, 403, 'a login token is no admin');
|
||
const ok = await request('GET', '/admin/data.json', ADMIN);
|
||
assert.strictEqual(ok.status, 200);
|
||
assert.ok(Array.isArray(ok.json.buckets));
|
||
assert.strictEqual((await request('GET', '/admin')).status, 200);
|
||
});
|
||
|
||
test('admin sees everything and removes single files', async () => {
|
||
const a = await upload({ sid: 'admin1', password: 'pw', name: 'a.txt', content: 'AAA' });
|
||
const b = await upload({ sid: 'admin1', password: 'pw', name: 'b.txt', content: 'BBB' });
|
||
const data = await request('GET', '/admin/data.json', ADMIN);
|
||
const bucket = data.json.buckets.find(x => x.sid === 'admin1');
|
||
assert.strictEqual(bucket.files.length, 2);
|
||
assert.strictEqual(bucket.files[0].hasPassword, true);
|
||
assert.ok(bucket.files[0].url, 'urls of password buckets are not hidden');
|
||
assert.strictEqual(bucket.files[0].password, undefined);
|
||
|
||
assert.strictEqual((await request('DELETE', `/admin/buckets/admin1/files/${ a.key }`)).status, 401);
|
||
assert.strictEqual((await request('DELETE', `/admin/buckets/admin1/files/not-a-uuid`, ADMIN)).status, 400);
|
||
assert.strictEqual((await request('DELETE', `/admin/buckets/admin1/files/${ a.key }`, ADMIN)).status, 200);
|
||
assert.strictEqual((await request('DELETE', `/admin/buckets/admin1/files/${ a.key }`, ADMIN)).status, 404);
|
||
|
||
// recipients see the remaining file only
|
||
const list = await request('GET', '/admin1.json', { 'x-download-pass': 'pw' });
|
||
assert.deepStrictEqual(list.json.items.map(i => i.key), [b.key]);
|
||
assert.strictEqual((await request('GET', `/files/${ a.fid }`)).status, 404);
|
||
|
||
// the last file takes the bucket with it
|
||
assert.strictEqual((await request('DELETE', `/admin/buckets/admin1/files/${ b.key }`, ADMIN)).status, 200);
|
||
assert.strictEqual((await request('GET', '/admin1.json')).status, 404);
|
||
});
|
||
|
||
test('admin removes a whole bucket', async () => {
|
||
await upload({ sid: 'admin2', password: 'pw', name: 'a.txt' });
|
||
await upload({ sid: 'admin2', password: 'pw', name: 'b.txt' });
|
||
const res = await request('DELETE', '/admin/buckets/admin2', ADMIN);
|
||
assert.strictEqual(res.status, 200);
|
||
assert.strictEqual(res.json.removed, 2);
|
||
assert.strictEqual((await request('GET', '/admin2.json')).status, 404);
|
||
assert.strictEqual((await request('DELETE', '/admin/buckets/admin2', ADMIN)).status, 404);
|
||
assert.strictEqual(fs.existsSync(path.join(tmpDir, 'admin2')), false);
|
||
});
|
||
|
||
test('admin download uses a short lived link and keeps one-time files', async () => {
|
||
const { key } = await upload({ sid: 'admin3', name: 'once.txt', retention: 'one-time', content: 'only once' });
|
||
|
||
const noAuth = await request('POST', '/admin/download-link', ...jsonBody({ sid: 'admin3', key }));
|
||
assert.strictEqual(noAuth.status, 401);
|
||
|
||
const link = await request('POST', '/admin/download-link', { ...ADMIN, ...jsonBody({ sid: 'admin3', key })[0] }, jsonBody({ sid: 'admin3', key })[1]);
|
||
assert.strictEqual(link.status, 200);
|
||
const res = await request('GET', link.json.url);
|
||
assert.strictEqual(res.status, 200);
|
||
assert.strictEqual(res.text, 'only once');
|
||
assert.match(res.headers['content-disposition'], /^attachment; filename="once.txt"/);
|
||
await tick();
|
||
|
||
// twice is fine and the recipient can still get the file once
|
||
assert.strictEqual((await request('GET', link.json.url)).status, 200);
|
||
assert.strictEqual((await request('GET', `/files/admin3++${ key }`)).status, 200);
|
||
await tick();
|
||
assert.strictEqual((await request('GET', `/files/admin3++${ key }`)).status, 404);
|
||
|
||
assert.strictEqual((await request('GET', '/admin/dl?t=garbage')).status, 403);
|
||
// a login token can not be used as download link
|
||
assert.strictEqual((await request('GET', `/admin/dl?t=${ encodeURIComponent(token) }`)).status, 403);
|
||
});
|
||
|
||
test('admin archive download of a bucket', async () => {
|
||
await upload({ sid: 'admin4', password: 'pw', name: 'a.txt', content: 'AAA' });
|
||
const link = await request('POST', '/admin/download-link', { ...ADMIN, 'Content-Type': 'application/json' }, JSON.stringify({ sid: 'admin4', format: 'tar.gz' }));
|
||
assert.strictEqual(link.status, 200);
|
||
const res = await request('GET', link.json.url);
|
||
assert.strictEqual(res.status, 200);
|
||
assert.strictEqual(res.body[0], 0x1f);
|
||
assert.strictEqual(res.body[1], 0x8b);
|
||
});
|
||
|
||
// -------------------------------------------------------------- settings
|
||
|
||
test('admin settings change the brand for everybody', async () => {
|
||
const before = await request('GET', '/brand.json');
|
||
assert.strictEqual(before.json.name, 'Dodo Transfer');
|
||
|
||
assert.strictEqual((await request('PUT', '/admin/settings', { 'Content-Type': 'application/json' }, '{"name":"x"}')).status, 401);
|
||
const bad = await request('PUT', '/admin/settings', { ...ADMIN, 'Content-Type': 'application/json' }, '{"name":""}');
|
||
assert.strictEqual(bad.status, 400);
|
||
|
||
const ok = await request('PUT', '/admin/settings', { ...ADMIN, 'Content-Type': 'application/json' },
|
||
JSON.stringify({ name: 'Local Transfer', owner: 'Dev', from: 2025 }));
|
||
assert.strictEqual(ok.status, 200);
|
||
|
||
const brand = await request('GET', '/brand.json');
|
||
assert.strictEqual(brand.json.name, 'Local Transfer');
|
||
assert.match(brand.json.footer, /^© 2025(–\d{4})? Dev$/);
|
||
|
||
// the shell page carries the new name
|
||
const page = await request('GET', '/');
|
||
assert.match(page.text, /<title>Local Transfer<\/title>/);
|
||
assert.ok(!page.text.includes('Dodo Transfer'));
|
||
|
||
// names are escaped
|
||
await request('PUT', '/admin/settings', { ...ADMIN, 'Content-Type': 'application/json' }, JSON.stringify({ name: '</script><b>x' }));
|
||
const evil = await request('GET', '/');
|
||
assert.ok(!evil.text.includes('</script><b>'));
|
||
await request('PUT', '/admin/settings', { ...ADMIN, 'Content-Type': 'application/json' }, JSON.stringify({ name: 'Dodo Transfer', owner: '아진산업(주) 전장선행개발팀', from: 2026 }));
|
||
});
|
||
|
||
test('admin logo upload, serving and reset', async () => {
|
||
const defaultLogo = await request('GET', '/assets/brand-logo');
|
||
assert.strictEqual(defaultLogo.status, 200);
|
||
assert.strictEqual(defaultLogo.headers['content-type'], 'image/svg+xml');
|
||
|
||
const text = await request('POST', '/admin/settings/logo', ADMIN, Buffer.from('not an image'));
|
||
assert.strictEqual(text.status, 400);
|
||
assert.strictEqual((await request('POST', '/admin/settings/logo', {}, PNG)).status, 401);
|
||
|
||
const big = await request('POST', '/admin/settings/logo', ADMIN, Buffer.concat([PNG, Buffer.alloc(1024 * 1024)]));
|
||
assert.strictEqual(big.status, 413);
|
||
|
||
const svg = Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><rect width="1" height="1"/></svg>');
|
||
const ok = await request('POST', '/admin/settings/logo', ADMIN, svg);
|
||
assert.strictEqual(ok.status, 200);
|
||
assert.strictEqual(ok.json.brand.customLogo, true);
|
||
const served = await request('GET', ok.json.brand.logoUrl);
|
||
assert.strictEqual(served.headers['content-type'], 'image/svg+xml');
|
||
assert.match(served.headers['content-security-policy'], /sandbox/);
|
||
assert.ok(served.body.equals(svg));
|
||
|
||
const reset = await request('DELETE', '/admin/settings/logo', ADMIN);
|
||
assert.strictEqual(reset.json.brand.customLogo, false);
|
||
assert.strictEqual((await request('GET', '/assets/brand-logo')).headers['content-type'], 'image/svg+xml');
|
||
});
|
||
|
||
// ---------------------------------------------------------------- misc
|
||
|
||
test('expiry is the earlier of retention and maxAge', async () => {
|
||
const db = require('../../lib/db');
|
||
const config = require('../../config');
|
||
const probe = new db(tmpDir, null);
|
||
const created = Date.now();
|
||
const file = (retention) => ({ metadata: { createdAt: String(created), retention } });
|
||
assert.strictEqual(probe.expiresAt(file('3600')), created + 3600_000);
|
||
assert.strictEqual(probe.expiresAt(file('one-time')), created + config.maxAge * 1000);
|
||
assert.strictEqual(probe.expiresAt(file(String(config.maxAge * 2))), created + config.maxAge * 1000);
|
||
});
|
||
|
||
test('the old misspelled lock flag is still honored', async () => {
|
||
const { fid } = await upload({ sid: 'oldlock', password: 'pw' });
|
||
const jsonFile = path.join(tmpDir, ...fid.split('++')) + '.json';
|
||
const info = JSON.parse(fs.readFileSync(jsonFile, 'utf8'));
|
||
info.metadata.buckedLocked = true;
|
||
fs.writeFileSync(jsonFile, JSON.stringify(info));
|
||
const db = require('../../lib/db');
|
||
const fresh = new db(tmpDir, new (require('../../lib/store'))(tmpDir));
|
||
fresh.init();
|
||
assert.strictEqual(fresh.isLocked('oldlock'), true);
|
||
});
|
||
|
||
test('hidden folders in the data dir are no buckets', async () => {
|
||
fs.mkdirSync(path.join(tmpDir, '.settings'), { recursive: true });
|
||
fs.writeFileSync(path.join(tmpDir, '.settings', 'settings.json'), '{}');
|
||
const db = require('../../lib/db');
|
||
const fresh = new db(tmpDir, new (require('../../lib/store'))(tmpDir));
|
||
fresh.init();
|
||
assert.strictEqual(fresh.get('.settings'), undefined);
|
||
});
|
||
|
||
test('health check answers without a login', async () => {
|
||
const res = await request('GET', '/healthz');
|
||
assert.strictEqual(res.status, 200);
|
||
assert.strictEqual(res.text, 'ok');
|
||
});
|
||
|
||
test('error pages are branded and escaped', async () => {
|
||
const res = await request('GET', '/does-not-exist');
|
||
assert.strictEqual(res.status, 404);
|
||
assert.match(res.text, /<title>Dodo Transfer<\/title>/);
|
||
});
|