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