feat: add extensive test suite

This commit is contained in:
2026-08-30 11:33:53 +07:00 Unverified
parent 061cce00dc
commit 8359c7272e
131 changed files with 8717 additions and 2200 deletions
+10 -26
View File
@@ -1,39 +1,23 @@
"use server";
import { eq } from "drizzle-orm";
import type { PgDatabase } from "drizzle-orm/pg-core";
import { revalidatePath } from "next/cache";
import { actionLog, cdnify } from "@/lib/api";
import { adminCheck } from "@/lib/auth";
import { db } from "@/lib/db";
import { cdn } from "@/lib/db/schema";
import { fetchToCdnService } from "./service";
export async function fetchToCdn(urls: string[]) {
if (!(await adminCheck())) throw "Unauthorized";
async function toFile(res: Response) {
if (!res.ok) throw "File failed to download";
return new File(
[await res.blob()],
res.headers
.get("Content-Disposition")
?.match(/^attachment;.*filename="([a-zA-Z0-9-_ .]+)"/)?.[1] ||
decodeURIComponent(new URL(res.url).pathname).match(
/\/([a-zA-Z0-9-_ .]+)$/,
)?.[1] ||
`file.${res.headers.get("Content-Type")?.split("/")[1] || "dat"}`,
);
}
return db
.transaction(async (tx) =>
Promise.all(
urls.map((url) =>
fetch(url, { redirect: "follow" })
.then(toFile)
.then((f) => cdnify(f, { tx })),
),
),
)
.finally(() => revalidatePath("/admin/cdn"));
// biome-ignore lint/suspicious/noExplicitAny: Drizzle transaction type parameters
return fetchToCdnService<PgDatabase<any, any, any>, string>(urls, {
adminCheck,
fetch: (url) => fetch(url, { redirect: "follow" }),
transaction: (callback) => db.transaction(callback),
importFile: (file, tx) => cdnify(file, { tx }),
afterImport: () => revalidatePath("/admin/cdn"),
});
}
export async function rename(id: string, name: string) {
+110
View File
@@ -0,0 +1,110 @@
import { describe, expect, mock, test } from "bun:test";
import { fetchToCdnService, responseToFile } from "./service";
function response(
headers: HeadersInit = {},
options: { status?: number; url?: string } = {},
) {
const result = new Response("file-data", {
status: options.status ?? 200,
headers,
});
if (options.url) Object.defineProperty(result, "url", { value: options.url });
return result;
}
describe("responseToFile", () => {
test("prefers the content-disposition filename", async () => {
const file = await responseToFile(
response({
"Content-Disposition": 'attachment; filename="report final.pdf"',
}),
);
expect(file.name).toBe("report final.pdf");
});
test("uses the decoded final URL filename after redirects", async () => {
const file = await responseToFile(
response({}, { url: "https://cdn.test/assets/my%20image.png" }),
);
expect(file.name).toBe("my image.png");
});
test("falls back to the response content type", async () => {
const file = await responseToFile(
response({ "Content-Type": "image/webp; charset=binary" }),
);
expect(file.name).toBe("file.webp");
expect(file.type).toBe("image/webp");
});
test("rejects non-success status responses", async () => {
expect(responseToFile(response({}, { status: 404 }))).rejects.toThrow(
"404",
);
});
});
describe("fetchToCdnService", () => {
test("requires admin before fetching or opening a transaction", async () => {
const fetch = mock(async (_url: string) => response());
let transactionCalled = false;
expect(
fetchToCdnService(["https://cdn.test/file"], {
adminCheck: async () => false,
fetch,
transaction: async (callback) => {
transactionCalled = true;
return callback({});
},
importFile: async () => "id",
afterImport: async () => {},
}),
).rejects.toBe("Unauthorized");
expect(fetch).not.toHaveBeenCalled();
expect(transactionCalled).toBe(false);
});
test("imports every file in one transaction and refreshes afterward", async () => {
const transactionObject = { id: "tx" };
const imported: string[] = [];
let refreshed = false;
const result = await fetchToCdnService(
["https://cdn.test/a", "https://cdn.test/b"],
{
adminCheck: async () => true,
fetch: async (url) =>
response({
"Content-Disposition": `attachment; filename=${url.at(-1)}.txt`,
}),
transaction: async (callback) => callback(transactionObject),
importFile: async (file, tx) => {
expect(tx).toBe(transactionObject);
imported.push(file.name);
return file.name;
},
afterImport: async () => {
refreshed = true;
},
},
);
expect(result).toEqual(["a.txt", "b.txt"]);
expect(imported).toEqual(["a.txt", "b.txt"]);
expect(refreshed).toBe(true);
});
test("refreshes after a failed download", async () => {
const afterImport = mock(async () => {});
expect(
fetchToCdnService(["https://cdn.test/missing"], {
adminCheck: async () => true,
fetch: async () => response({}, { status: 503 }),
transaction: async (callback) => callback({}),
importFile: async () => "id",
afterImport,
}),
).rejects.toThrow("503");
expect(afterImport).toHaveBeenCalledTimes(1);
});
});
+62
View File
@@ -0,0 +1,62 @@
export interface CdnImportDependencies<TTransaction, TResult> {
adminCheck: () => Promise<unknown>;
fetch: (url: string) => Promise<Response>;
transaction: <T>(
callback: (transaction: TTransaction) => Promise<T>,
) => Promise<T>;
importFile: (file: File, transaction: TTransaction) => Promise<TResult>;
afterImport: () => Promise<void> | void;
}
function filenameFromDisposition(value: string | null) {
return value?.match(/filename="?([^";]+)"?/i)?.[1];
}
export async function responseToFile(response: Response) {
if (!response.ok) {
throw new Error(
`File failed to download (${response.status} ${response.statusText})`,
);
}
const dispositionName = filenameFromDisposition(
response.headers.get("Content-Disposition"),
);
const pathName = response.url
? decodeURIComponent(new URL(response.url).pathname).match(
/\/([^/]+)$/,
)?.[1]
: undefined;
const contentType = response.headers.get("Content-Type")?.split(";")[0];
const extension = contentType?.split("/")[1];
const blob = await response.blob();
return new File(
[blob],
dispositionName || pathName || `file.${extension || "dat"}`,
{ type: contentType || blob.type },
);
}
export async function fetchToCdnService<TTransaction, TResult>(
urls: string[],
dependencies: CdnImportDependencies<TTransaction, TResult>,
) {
if (!(await dependencies.adminCheck())) throw "Unauthorized";
try {
return await dependencies.transaction((transaction) =>
Promise.all(
urls.map(async (url) => {
const response = await dependencies.fetch(url);
return dependencies.importFile(
await responseToFile(response),
transaction,
);
}),
),
);
} finally {
await dependencies.afterImport();
}
}