107 lines
4.0 KiB
TypeScript
107 lines
4.0 KiB
TypeScript
import { access, readFile, readdir } from "node:fs/promises";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { safeBrowserLocationNext, safeBrowserNext } from "@/lib/client/browser-auth";
|
|
import nextConfig from "../next.config";
|
|
|
|
const repositoryRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
const appRoot = join(repositoryRoot, "app");
|
|
|
|
afterEach(() => vi.unstubAllGlobals());
|
|
|
|
async function findProductionEntryFiles(directory: string): Promise<string[]> {
|
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
const nested = await Promise.all(entries.map(async (entry) => {
|
|
const path = join(directory, entry.name);
|
|
if (entry.isDirectory()) return findProductionEntryFiles(path);
|
|
return entry.isFile() && /^(?:layout|page|route)\.tsx?$/.test(entry.name) ? [path] : [];
|
|
}));
|
|
return nested.flat();
|
|
}
|
|
|
|
async function findFilesOrEmpty(directory: string): Promise<string[]> {
|
|
const entries = await readdir(directory, { withFileTypes: true }).catch((error: NodeJS.ErrnoException) => {
|
|
if (error.code === "ENOENT") return [];
|
|
throw error;
|
|
});
|
|
const nested = await Promise.all(entries.map(async (entry) => {
|
|
const path = join(directory, entry.name);
|
|
return entry.isDirectory() ? findFilesOrEmpty(path) : [path];
|
|
}));
|
|
return nested.flat();
|
|
}
|
|
|
|
describe("static frontend architecture", () => {
|
|
it("exports an image-optimizer-independent static site", () => {
|
|
expect(nextConfig.output).toBe("export");
|
|
expect(nextConfig.images?.unoptimized).toBe(true);
|
|
});
|
|
|
|
it("has no Next server runtime entry points", async () => {
|
|
expect(await findFilesOrEmpty(join(appRoot, "api"))).toEqual([]);
|
|
expect(await findFilesOrEmpty(join(appRoot, "uploads"))).toEqual([]);
|
|
expect(await findFilesOrEmpty(join(appRoot, "generated-results"))).toEqual([]);
|
|
await expect(access(join(repositoryRoot, "middleware.ts"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
|
|
const entryFiles = await findProductionEntryFiles(appRoot);
|
|
expect(entryFiles.filter((path) => path.endsWith("route.ts"))).toEqual([]);
|
|
for (const entryFile of entryFiles) {
|
|
const source = await readFile(entryFile, "utf8");
|
|
expect(source, entryFile).not.toMatch(/(?:from\s+|import\s*\()?["']@\/lib\/server(?:\/|["'])/);
|
|
expect(source, entryFile).not.toMatch(/(?:from\s+|import\s*\()?["']next\/(?:headers|server)["']/);
|
|
}
|
|
});
|
|
|
|
it("loads current identity from the same-origin Go API", async () => {
|
|
const responseBody = {
|
|
authenticated: false,
|
|
authRequired: true,
|
|
authConfigured: true,
|
|
authMode: null,
|
|
user: null
|
|
};
|
|
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
|
|
new Response(JSON.stringify(responseBody), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" }
|
|
})
|
|
);
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const { fetchBrowserAuthState } = await import("@/lib/client/browser-auth");
|
|
await expect(fetchBrowserAuthState()).resolves.toEqual(responseBody);
|
|
expect(fetchMock).toHaveBeenCalledOnce();
|
|
expect(fetchMock).toHaveBeenCalledWith("/api/auth/me", {
|
|
method: "GET",
|
|
cache: "no-store",
|
|
credentials: "same-origin",
|
|
headers: { Accept: "application/json" },
|
|
signal: undefined
|
|
});
|
|
|
|
});
|
|
|
|
it("keeps safe same-origin redirects away from auth endpoints", () => {
|
|
expect(safeBrowserNext("/billing?tab=wallet#history")).toBe("/billing?tab=wallet#history");
|
|
expect(safeBrowserLocationNext({
|
|
pathname: "/create",
|
|
search: "?mode=video",
|
|
hash: "#x"
|
|
})).toBe("/create?mode=video#x");
|
|
expect(safeBrowserNext("//evil.example/path", "/create?mode=video#x")).toBe("/create?mode=video#x");
|
|
|
|
for (const unsafe of [
|
|
"//evil.example/path",
|
|
"/\\evil.example/path",
|
|
"/api/auth/logout",
|
|
"/auth/login",
|
|
"/auth/admin-login"
|
|
]) {
|
|
expect(safeBrowserNext(unsafe), unsafe).toBe("/create");
|
|
}
|
|
});
|
|
});
|