Files
ConvertX/tests/pages/deleteFile.test.ts
T
2026-10-13 08:41:14 +02:00

101 lines
3.6 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { createJob, createUser, jobDirs, request, uploadsDir } from "./helpers/app";
async function userWithUploads(...fileNames: string[]) {
const user = await createUser();
const job = createJob(user.id);
const dirs = jobDirs(user.id, job);
mkdirSync(dirs.uploads, { recursive: true });
for (const fileName of fileNames) {
writeFileSync(`${dirs.uploads}${fileName}`, fileName);
}
return { user, job, dirs };
}
describe("POST /delete", () => {
test("requires a session", async () => {
const response = await request("/delete", {
headers: { accept: "application/json" },
json: { filename: "a.txt" },
});
expect(response.status).toBe(401);
});
test("redirects home without a job", async () => {
const { user, dirs } = await userWithUploads("a.txt");
const response = await request("/delete", {
cookies: { auth: user.token },
json: { filename: "a.txt" },
});
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/");
expect(existsSync(`${dirs.uploads}a.txt`)).toBe(true);
});
test("refuses to delete from another user's job", async () => {
const { user } = await userWithUploads();
const other = await userWithUploads("a.txt");
const response = await request("/delete", {
cookies: { auth: user.token, jobId: other.job },
json: { filename: "a.txt" },
});
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/");
expect(existsSync(`${other.dirs.uploads}a.txt`)).toBe(true);
});
test("deletes only the given uploaded file", async () => {
const { user, job, dirs } = await userWithUploads("a.txt", "b.txt");
const response = await request("/delete", {
cookies: { auth: user.token, jobId: job },
json: { filename: "a.txt" },
});
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ message: "File deleted successfully." });
expect(existsSync(`${dirs.uploads}a.txt`)).toBe(false);
expect(existsSync(`${dirs.uploads}b.txt`)).toBe(true);
});
test("sanitizes the file name so files outside the job cannot be deleted", async () => {
const { user, job } = await userWithUploads();
const other = await userWithUploads("keep.txt");
const response = await request("/delete", {
cookies: { auth: user.token, jobId: job },
json: { filename: `../../${other.user.id}/${other.job}/keep.txt` },
});
// the request passed the session and job checks, so the file name was really used;
// the sanitized name does not exist, which runs into the bug tracked below
expect(response.status).not.toBeOneOf([302, 401]);
expect(existsSync(`${other.dirs.uploads}keep.txt`)).toBe(true);
});
// BUG: unlink errors are not handled, so deleting a file that does not exist answers
// with a 500 whose body contains the absolute upload path on the server. This also
// happens for every sanitized traversal attempt. Remove `.failing` once deleteFile.tsx
// handles the missing file.
test.failing.each(["missing.txt", "../../1/1/keep.txt"])(
"answers a missing file (%s) without a server error or server paths",
async (filename) => {
const { user, job } = await userWithUploads();
const response = await request("/delete", {
cookies: { auth: user.token, jobId: job },
json: { filename },
});
expect(response.status).toBeLessThan(500);
expect(await response.text()).not.toContain(uploadsDir);
},
);
});