clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/prf-provision-custody
lofi / package / tooling / project_test.ts
project_test.ts 6.1K
1import { join } from "node:path";2import {3 duplicateBuildAssets,4 precacheUrls,5 scanSecrets,6 shellWeightBytes,7 sourceFingerprint,8 walkFiles,9} from "./project.ts";1011function assert(condition: unknown, message: string): asserts condition {12 if (!condition) throw new Error(message);13}1415function makeTestRoot(): Promise<string> {16 return Deno.makeTempDir({ dir: ".", prefix: ".lofi-project-test-" });17}1819Deno.test("source fingerprint is stable across file creation order and changes with content", async () => {20 const left = await makeTestRoot();21 const right = await makeTestRoot();22 try {23 await Deno.mkdir(join(left, "src"));24 await Deno.writeTextFile(join(left, "src", "b.ts"), "export const b = 2;\n");25 await Deno.writeTextFile(join(left, "src", "a.ts"), "export const a = 1;\n");26 await Deno.mkdir(join(right, "src"));27 await Deno.writeTextFile(join(right, "src", "a.ts"), "export const a = 1;\n");28 await Deno.writeTextFile(join(right, "src", "b.ts"), "export const b = 2;\n");29 assert(30 await sourceFingerprint(left) === await sourceFingerprint(right),31 "fingerprint depends on file creation order",32 );33 await Deno.writeTextFile(join(right, "src", "b.ts"), "export const b = 3;\n");34 assert(35 await sourceFingerprint(left) !== await sourceFingerprint(right),36 "fingerprint ignored source content",37 );38 } finally {39 await Deno.remove(left, { recursive: true });40 await Deno.remove(right, { recursive: true });41 }42});4344Deno.test("source fingerprint includes authored public JavaScript", async () => {45 const root = await makeTestRoot();46 try {47 await Deno.mkdir(join(root, "public"));48 await Deno.writeTextFile(join(root, "public", "app.js"), "const revision = 'one';\n");49 const first = await sourceFingerprint(root);50 await Deno.writeTextFile(join(root, "public", "app.js"), "const revision = 'two';\n");51 assert(first !== await sourceFingerprint(root), "fingerprint ignored authored public source");52 } finally {53 await Deno.remove(root, { recursive: true });54 }55});5657Deno.test("precache URLs are portable app-shell URLs", () => {58 const urls = precacheUrls([59 "sw.js",60 "index.html",61 "lofi-build.json",62 "lofi-precache.json",63 "assets\\client.js",64 "manifest.webmanifest",65 "screenshot-wide.png",66 ], ["screenshot-wide.png"]);67 assert(68 JSON.stringify(urls) === JSON.stringify([69 "./",70 "./assets/client.js",71 "./manifest.webmanifest",72 ]),73 `unexpected precache URLs: ${JSON.stringify(urls)}`,74 );75});7677Deno.test({78 name: "project walking names unsupported symbolic links and the remediation",79 ignore: Deno.build.os === "windows",80 async fn() {81 const root = await makeTestRoot();82 try {83 await Deno.writeTextFile(join(root, "target.ts"), "export const target = true;\n");84 const link = await new Deno.Command("ln", {85 args: ["-s", "target.ts", join(root, "linked.ts")],86 stdout: "null",87 stderr: "piped",88 }).output();89 assert(90 link.success,91 `failed to create test symlink: ${new TextDecoder().decode(link.stderr)}`,92 );93 let message = "";94 try {95 await walkFiles(root);96 } catch (error) {97 message = error instanceof Error ? error.message : String(error);98 }99 assert(message.includes("linked.ts"), `error did not name the symbolic link: ${message}`);100 assert(101 message.includes("replace it with a regular file or directory"),102 `error did not include remediation: ${message}`,103 );104 } finally {105 await Deno.remove(root, { recursive: true });106 }107 },108});109110Deno.test("secret scan ignores the local env source but catches source and build leaks", async () => {111 const root = await makeTestRoot();112 const secret = "generated-command-secret-fixture";113 try {114 await Deno.mkdir(join(root, "src"));115 await Deno.mkdir(join(root, "dist"));116 await Deno.writeTextFile(join(root, ".env"), `BACKEND_SECRET=${secret}\n`);117 await Deno.writeTextFile(join(root, "src", "safe.ts"), "export const safe = true;\n");118 await Deno.writeTextFile(119 join(root, "dist", "client.js"),120 `const leaked = ${JSON.stringify(secret)};`,121 );122 const result = await scanSecrets({ BACKEND_SECRET: secret }, root);123 assert(result.leaks.length === 1, `expected one build leak, received ${result.leaks.length}`);124 assert(result.leaks[0].path === "dist/client.js", "secret scan reported the wrong file");125 } finally {126 await Deno.remove(root, { recursive: true });127 }128});129130Deno.test("duplicate build assets are grouped by content and scoped to _astro", async () => {131 const root = await makeTestRoot();132 try {133 await Deno.mkdir(join(root, "_astro"));134 await Deno.writeTextFile(join(root, "_astro", "engine_bg.HASH.wasm"), "same payload");135 await Deno.writeTextFile(join(root, "_astro", "engine_bg-HASH.wasm"), "same payload");136 await Deno.writeTextFile(join(root, "_astro", "one.js"), "payload one");137 await Deno.writeTextFile(join(root, "_astro", "two.js"), "payload two");138 await Deno.writeTextFile(join(root, "copy.txt"), "same payload");139 const groups = await duplicateBuildAssets([140 "_astro/engine_bg.HASH.wasm",141 "_astro/engine_bg-HASH.wasm",142 "_astro/one.js",143 "_astro/two.js",144 "copy.txt",145 ], root);146 assert(groups.length === 1, `expected one duplicate group, received ${groups.length}`);147 assert(148 groups[0].join(", ") === "_astro/engine_bg-HASH.wasm, _astro/engine_bg.HASH.wasm",149 `same-size files were grouped by name, not content: ${groups[0].join(", ")}`,150 );151 } finally {152 await Deno.remove(root, { recursive: true });153 }154});155156Deno.test("shell weight sums the bytes of the precached set", async () => {157 const root = await makeTestRoot();158 try {159 await Deno.writeTextFile(join(root, "index.html"), "12345");160 await Deno.mkdir(join(root, "_astro"));161 await Deno.writeTextFile(join(root, "_astro", "client.js"), "1234567");162 const total = await shellWeightBytes(["index.html", "_astro/client.js"], root);163 assert(total === 12, `expected 12 bytes, received ${total}`);164 } finally {165 await Deno.remove(root, { recursive: true });166 }167});