Files
Unraid-Navigator/server/server.test.js
T

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');
}