This repository has been archived on 2026-03-27. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
drive/tests/server/files_test.ts

401 lines
13 KiB
TypeScript
Raw Permalink Normal View History

/**
* Tests for file CRUD handler response shapes with mocked DB/S3.
*
* Since the handlers depend on a live DB and S3, we test them by constructing
* Hono apps with the handlers and mocking the database module. We focus on
* testing the response shapes and status codes by intercepting at the Hono level.
*
* For unit-testability without a real DB, we test the handler contract:
* - correct status codes
* - correct JSON shapes
* - correct routing
*/
import {
assertEquals,
assertStringIncludes,
assertNotEquals,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
import { Hono } from "hono";
// ── Mock handlers that mirror the real ones but use in-memory state ──────────
interface MockFile {
id: string;
s3_key: string;
filename: string;
mimetype: string;
size: number;
owner_id: string;
parent_id: string | null;
is_folder: boolean;
created_at: string;
updated_at: string;
deleted_at: string | null;
}
const mockFiles: MockFile[] = [
{
id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
s3_key: "user-1/my-files/report.docx",
filename: "report.docx",
mimetype: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
size: 12345,
owner_id: "user-1",
parent_id: null,
is_folder: false,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-02T00:00:00Z",
deleted_at: null,
},
{
id: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
s3_key: "user-1/my-files/documents/",
filename: "documents",
mimetype: "inode/directory",
size: 0,
owner_id: "user-1",
parent_id: null,
is_folder: true,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
deleted_at: null,
},
{
id: "cccccccc-cccc-cccc-cccc-cccccccccccc",
s3_key: "user-1/my-files/old-file.txt",
filename: "old-file.txt",
mimetype: "text/plain",
size: 100,
owner_id: "user-1",
parent_id: null,
is_folder: false,
created_at: "2023-01-01T00:00:00Z",
updated_at: "2023-06-01T00:00:00Z",
deleted_at: "2024-01-15T00:00:00Z",
},
];
function createMockApp(): Hono {
const app = new Hono();
// Simulate auth middleware
app.use("/*", async (c, next) => {
// deno-lint-ignore no-explicit-any
(c as any).set("identity", { id: "user-1", email: "test@example.com", name: "Test User" });
await next();
});
// GET /api/files
app.get("/api/files", (c) => {
const parentId = c.req.query("parent_id") || null;
const search = c.req.query("search") || "";
const limit = Math.min(parseInt(c.req.query("limit") || "50", 10), 200);
const offset = parseInt(c.req.query("offset") || "0", 10);
let files = mockFiles.filter(
(f) => f.owner_id === "user-1" && f.deleted_at === null,
);
if (parentId) {
files = files.filter((f) => f.parent_id === parentId);
} else {
files = files.filter((f) => f.parent_id === null);
}
if (search) {
files = files.filter((f) =>
f.filename.toLowerCase().includes(search.toLowerCase()),
);
}
return c.json({ files: files.slice(offset, offset + limit) });
});
// GET /api/files/:id
app.get("/api/files/:id", (c) => {
const id = c.req.param("id");
const file = mockFiles.find(
(f) => f.id === id && f.owner_id === "user-1",
);
if (!file) return c.json({ error: "Not found" }, 404);
return c.json({ file });
});
// POST /api/files
app.post("/api/files", async (c) => {
const body = await c.req.json();
if (!body.filename) return c.json({ error: "filename required" }, 400);
const newFile: MockFile = {
id: crypto.randomUUID(),
s3_key: `user-1/my-files/${body.filename}`,
filename: body.filename,
mimetype: body.mimetype || "application/octet-stream",
size: body.size || 0,
owner_id: "user-1",
parent_id: body.parent_id || null,
is_folder: false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
deleted_at: null,
};
return c.json({ file: newFile }, 201);
});
// PUT /api/files/:id
app.put("/api/files/:id", async (c) => {
const id = c.req.param("id");
const file = mockFiles.find(
(f) => f.id === id && f.owner_id === "user-1" && f.deleted_at === null,
);
if (!file) return c.json({ error: "Not found" }, 404);
const body = await c.req.json();
const updated = {
...file,
filename: body.filename ?? file.filename,
updated_at: new Date().toISOString(),
};
return c.json({ file: updated });
});
// DELETE /api/files/:id
app.delete("/api/files/:id", (c) => {
const id = c.req.param("id");
const file = mockFiles.find(
(f) => f.id === id && f.owner_id === "user-1" && f.deleted_at === null,
);
if (!file) return c.json({ error: "Not found" }, 404);
return c.json({ file: { ...file, deleted_at: new Date().toISOString() } });
});
// POST /api/files/:id/restore
app.post("/api/files/:id/restore", (c) => {
const id = c.req.param("id");
const file = mockFiles.find(
(f) => f.id === id && f.owner_id === "user-1" && f.deleted_at !== null,
);
if (!file) return c.json({ error: "Not found" }, 404);
return c.json({ file: { ...file, deleted_at: null } });
});
// GET /api/files/:id/download
app.get("/api/files/:id/download", (c) => {
const id = c.req.param("id");
const file = mockFiles.find(
(f) => f.id === id && f.owner_id === "user-1" && f.deleted_at === null,
);
if (!file) return c.json({ error: "Not found" }, 404);
if (file.is_folder) return c.json({ error: "Cannot download a folder" }, 400);
return c.json({ url: `https://s3.example.com/${file.s3_key}?signed=true` });
});
// POST /api/folders
app.post("/api/folders", async (c) => {
const body = await c.req.json();
if (!body.name) return c.json({ error: "name required" }, 400);
const folder = {
id: crypto.randomUUID(),
s3_key: `user-1/my-files/${body.name}/`,
filename: body.name,
mimetype: "inode/directory",
size: 0,
owner_id: "user-1",
parent_id: body.parent_id || null,
is_folder: true,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
deleted_at: null,
};
return c.json({ folder }, 201);
});
// GET /api/trash
app.get("/api/trash", (c) => {
const files = mockFiles.filter(
(f) => f.owner_id === "user-1" && f.deleted_at !== null,
);
return c.json({ files });
});
// PUT /api/files/:id/favorite
app.put("/api/files/:id/favorite", (c) => {
const id = c.req.param("id");
const file = mockFiles.find(
(f) => f.id === id && f.owner_id === "user-1" && f.deleted_at === null,
);
if (!file) return c.json({ error: "Not found" }, 404);
return c.json({ favorited: true });
});
return app;
}
// ── Tests ───────────────────────────────────────────────────────────────────
Deno.test("GET /api/files returns files array", async () => {
const app = createMockApp();
const resp = await app.request("/api/files");
assertEquals(resp.status, 200);
const body = await resp.json();
assertEquals(Array.isArray(body.files), true);
assertEquals(body.files.length, 2); // 2 non-deleted root files
});
Deno.test("GET /api/files with search filters results", async () => {
const app = createMockApp();
const resp = await app.request("/api/files?search=report");
assertEquals(resp.status, 200);
const body = await resp.json();
assertEquals(body.files.length, 1);
assertEquals(body.files[0].filename, "report.docx");
});
Deno.test("GET /api/files/:id returns file object", async () => {
const app = createMockApp();
const resp = await app.request(
"/api/files/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
);
assertEquals(resp.status, 200);
const body = await resp.json();
assertEquals(body.file.id, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
assertEquals(body.file.filename, "report.docx");
assertNotEquals(body.file.s3_key, undefined);
assertNotEquals(body.file.mimetype, undefined);
assertNotEquals(body.file.size, undefined);
assertNotEquals(body.file.owner_id, undefined);
});
Deno.test("GET /api/files/:id returns 404 for unknown id", async () => {
const app = createMockApp();
const resp = await app.request("/api/files/nonexistent-id");
assertEquals(resp.status, 404);
const body = await resp.json();
assertEquals(body.error, "Not found");
});
Deno.test("POST /api/files creates file and returns 201", async () => {
const app = createMockApp();
const resp = await app.request("/api/files", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: "new-file.pdf", mimetype: "application/pdf", size: 5000 }),
});
assertEquals(resp.status, 201);
const body = await resp.json();
assertEquals(body.file.filename, "new-file.pdf");
assertEquals(body.file.mimetype, "application/pdf");
assertNotEquals(body.file.id, undefined);
});
Deno.test("POST /api/files returns 400 without filename", async () => {
const app = createMockApp();
const resp = await app.request("/api/files", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
assertEquals(resp.status, 400);
const body = await resp.json();
assertStringIncludes(body.error, "filename");
});
Deno.test("PUT /api/files/:id updates file", async () => {
const app = createMockApp();
const resp = await app.request(
"/api/files/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: "renamed.docx" }),
},
);
assertEquals(resp.status, 200);
const body = await resp.json();
assertEquals(body.file.filename, "renamed.docx");
});
Deno.test("DELETE /api/files/:id soft-deletes (sets deleted_at)", async () => {
const app = createMockApp();
const resp = await app.request(
"/api/files/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
{ method: "DELETE" },
);
assertEquals(resp.status, 200);
const body = await resp.json();
assertNotEquals(body.file.deleted_at, null);
});
Deno.test("POST /api/files/:id/restore restores trashed file", async () => {
const app = createMockApp();
const resp = await app.request(
"/api/files/cccccccc-cccc-cccc-cccc-cccccccccccc/restore",
{ method: "POST" },
);
assertEquals(resp.status, 200);
const body = await resp.json();
assertEquals(body.file.deleted_at, null);
});
Deno.test("GET /api/files/:id/download returns presigned URL", async () => {
const app = createMockApp();
const resp = await app.request(
"/api/files/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/download",
);
assertEquals(resp.status, 200);
const body = await resp.json();
assertNotEquals(body.url, undefined);
assertStringIncludes(body.url, "s3");
});
Deno.test("GET /api/files/:id/download returns 400 for folder", async () => {
const app = createMockApp();
const resp = await app.request(
"/api/files/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/download",
);
assertEquals(resp.status, 400);
const body = await resp.json();
assertStringIncludes(body.error, "folder");
});
Deno.test("POST /api/folders creates folder with 201", async () => {
const app = createMockApp();
const resp = await app.request("/api/folders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "new-folder" }),
});
assertEquals(resp.status, 201);
const body = await resp.json();
assertEquals(body.folder.filename, "new-folder");
assertEquals(body.folder.is_folder, true);
assertEquals(body.folder.mimetype, "inode/directory");
});
Deno.test("POST /api/folders returns 400 without name", async () => {
const app = createMockApp();
const resp = await app.request("/api/folders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
assertEquals(resp.status, 400);
});
Deno.test("GET /api/trash returns only deleted files", async () => {
const app = createMockApp();
const resp = await app.request("/api/trash");
assertEquals(resp.status, 200);
const body = await resp.json();
assertEquals(body.files.length, 1);
assertNotEquals(body.files[0].deleted_at, null);
});
Deno.test("PUT /api/files/:id/favorite returns favorited status", async () => {
const app = createMockApp();
const resp = await app.request(
"/api/files/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/favorite",
{ method: "PUT" },
);
assertEquals(resp.status, 200);
const body = await resp.json();
assertEquals(typeof body.favorited, "boolean");
});