110 lines
4.0 KiB
JavaScript
110 lines
4.0 KiB
JavaScript
import fs from 'node:fs/promises';
|
|
import http from 'node:http';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { after, before, describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createApp } from './server.js';
|
|
|
|
let root;
|
|
let outside;
|
|
let baseUrl;
|
|
let server;
|
|
|
|
describe('U-Navigator API', () => {
|
|
before(async () => {
|
|
root = await fs.mkdtemp(path.join(os.tmpdir(), 'u-nav-root-'));
|
|
outside = await fs.mkdtemp(path.join(os.tmpdir(), 'u-nav-outside-'));
|
|
await fs.mkdir(path.join(root, 'share'));
|
|
await fs.writeFile(path.join(root, 'share', 'alpha.txt'), 'alpha');
|
|
await fs.writeFile(path.join(outside, 'secret.txt'), 'secret');
|
|
await fs.symlink(outside, path.join(root, 'share', 'escape'));
|
|
|
|
const app = await createApp({ roots: [root] });
|
|
server = http.createServer(app);
|
|
await new Promise((resolve, reject) => {
|
|
server.once('error', reject);
|
|
server.listen(0, '127.0.0.1', resolve);
|
|
});
|
|
baseUrl = `http://127.0.0.1:${server.address().port}`;
|
|
});
|
|
|
|
after(async () => {
|
|
server.closeAllConnections();
|
|
await new Promise((resolve) => server.close(resolve));
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
await fs.rm(outside, { recursive: true, force: true });
|
|
});
|
|
|
|
it('lists files inside the configured root', async () => {
|
|
const result = await json(`/api/list?path=${encodeURIComponent(path.join(root, 'share'))}`);
|
|
assert.equal(result.entries.some((entry) => entry.name === 'alpha.txt'), true);
|
|
});
|
|
|
|
it('blocks path traversal outside the root', async () => {
|
|
const response = await fetch(`${baseUrl}/api/list?path=${encodeURIComponent(outside)}`);
|
|
assert.equal(response.status, 403);
|
|
});
|
|
|
|
it('blocks symlink escapes outside the root', async () => {
|
|
const response = await fetch(`${baseUrl}/api/list?path=${encodeURIComponent(path.join(root, 'share', 'escape'))}`);
|
|
assert.equal(response.status, 403);
|
|
});
|
|
|
|
it('uploads multipart files into an allowed directory', async () => {
|
|
const form = new FormData();
|
|
form.append('files', new Blob(['hello']), 'upload.txt');
|
|
const response = await fetch(`${baseUrl}/api/upload?path=${encodeURIComponent(path.join(root, 'share'))}`, {
|
|
method: 'POST',
|
|
body: form
|
|
});
|
|
assert.equal(response.status, 201);
|
|
assert.equal(await fs.readFile(path.join(root, 'share', 'upload.txt'), 'utf8'), 'hello');
|
|
});
|
|
|
|
it('creates move jobs for internal drag and drop operations', async () => {
|
|
await fs.writeFile(path.join(root, 'share', 'move-me.txt'), 'move');
|
|
await fs.mkdir(path.join(root, 'target'));
|
|
const response = await fetch(`${baseUrl}/api/jobs/move`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
source: path.join(root, 'share', 'move-me.txt'),
|
|
destination: path.join(root, 'target', 'move-me.txt')
|
|
})
|
|
});
|
|
assert.equal(response.status, 202);
|
|
const job = await response.json();
|
|
await waitForJob(job.id);
|
|
assert.equal(await fs.readFile(path.join(root, 'target', 'move-me.txt'), 'utf8'), 'move');
|
|
});
|
|
|
|
it('rejects destinations that already exist', async () => {
|
|
const response = await fetch(`${baseUrl}/api/jobs/copy`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
source: path.join(root, 'share', 'alpha.txt'),
|
|
destination: path.join(root, 'share', 'upload.txt')
|
|
})
|
|
});
|
|
assert.equal(response.status, 409);
|
|
});
|
|
});
|
|
|
|
async function json(pathname) {
|
|
const response = await fetch(`${baseUrl}${pathname}`);
|
|
assert.equal(response.ok, true);
|
|
return response.json();
|
|
}
|
|
|
|
async function waitForJob(id) {
|
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
const job = await json(`/api/jobs/${id}`);
|
|
if (job.status === 'completed') return job;
|
|
if (job.status === 'failed') throw new Error(job.error);
|
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
}
|
|
throw new Error('Timed out waiting for job');
|
|
}
|