clone

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

feat/141-effect-library
lofi / package / tooling / provision_test.ts
provision_test.ts 5.8K
1import {2 environmentForApp,3 generateJazzApp,4 JAZZ_MANAGED_SERVER_URL,5 mergeEnv,6 ProvisionError,7 provisionJazzApp,8} from "./provision.ts";910function assert(condition: unknown, message: string): asserts condition {11 if (!condition) throw new Error(message);12}1314function jsonResponse(body: unknown, status = 200): Response {15 return new Response(JSON.stringify(body), {16 status,17 headers: { "content-type": "application/json" },18 });19}2021const sampleApp = {22 appId: "00000000-0000-4000-8000-000000000000",23 adminSecret: "admin-secret-value",24 backendSecret: "backend-secret-value",25};2627Deno.test("generateJazzApp returns the three secrets from a well-formed response", async () => {28 const app = await generateJazzApp(() => Promise.resolve(jsonResponse(sampleApp)));29 assert(app.appId === sampleApp.appId, "appId should round-trip");30 assert(app.adminSecret === sampleApp.adminSecret, "adminSecret should round-trip");31 assert(app.backendSecret === sampleApp.backendSecret, "backendSecret should round-trip");32});3334Deno.test("generateJazzApp rejects a response missing a secret", async () => {35 let threw = false;36 try {37 await generateJazzApp(() => Promise.resolve(jsonResponse({ appId: "only-id" })));38 } catch (error) {39 threw = error instanceof ProvisionError;40 }41 assert(threw, "a missing adminSecret/backendSecret should raise ProvisionError");42});4344Deno.test("generateJazzApp surfaces a non-2xx status as a ProvisionError", async () => {45 let threw = false;46 try {47 await generateJazzApp(() => Promise.resolve(jsonResponse({}, 503)));48 } catch (error) {49 threw = error instanceof ProvisionError;50 }51 assert(threw, "an HTTP error should raise ProvisionError");52});5354Deno.test("environmentForApp maps onto the four lofi names", () => {55 const env = environmentForApp(sampleApp);56 assert(env.JAZZ_APP_ID === sampleApp.appId, "JAZZ_APP_ID");57 assert(env.JAZZ_SERVER_URL === JAZZ_MANAGED_SERVER_URL, "JAZZ_SERVER_URL defaults to managed");58 assert(env.JAZZ_ADMIN_SECRET === sampleApp.adminSecret, "JAZZ_ADMIN_SECRET");59 assert(env.BACKEND_SECRET === sampleApp.backendSecret, "BACKEND_SECRET");60});6162Deno.test("mergeEnv updates existing keys in place and preserves the rest", () => {63 const existing = [64 "# lofi config",65 "JAZZ_APP_ID=",66 "JAZZ_SERVER_URL=",67 "",68 "# unrelated",69 "OTHER_KEY=keep-me",70 "",71 ].join("\n");72 const merged = mergeEnv(existing, environmentForApp(sampleApp));73 assert(merged.includes(`JAZZ_APP_ID=${sampleApp.appId}`), "app id is filled in place");74 assert(merged.includes("OTHER_KEY=keep-me"), "unrelated key is preserved");75 assert(merged.includes("# unrelated"), "comments are preserved");76 assert(merged.includes(`JAZZ_ADMIN_SECRET=${sampleApp.adminSecret}`), "missing key is appended");77 assert((merged.match(/JAZZ_APP_ID=/g) ?? []).length === 1, "no duplicate JAZZ_APP_ID");78});7980Deno.test("mergeEnv appends all four keys to an empty file", () => {81 const merged = mergeEnv("", environmentForApp(sampleApp));82 for (const name of ["JAZZ_APP_ID", "JAZZ_SERVER_URL", "JAZZ_ADMIN_SECRET", "BACKEND_SECRET"]) {83 assert(merged.includes(`${name}=`), `${name} should be written`);84 }85});8687Deno.test("provisionJazzApp writes an env file and reports the app", async () => {88 let written = "";89 const result = await provisionJazzApp({90 path: ".env",91 fetchImpl: () => Promise.resolve(jsonResponse(sampleApp)),92 readFile: () => Promise.resolve(""),93 writeFile: (_path, contents) => {94 written = contents;95 return Promise.resolve();96 },97 });98 assert(result.app.appId === sampleApp.appId, "returns the generated app");99 assert(result.replaced === false, "a fresh file is not a replacement");100 assert(written.includes(`JAZZ_APP_ID=${sampleApp.appId}`), "env file received the app id");101});102103Deno.test("provisionJazzApp refuses to clobber a configured app without force", async () => {104 let fetched = false;105 let threw = false;106 try {107 await provisionJazzApp({108 path: ".env",109 fetchImpl: () => {110 fetched = true;111 return Promise.resolve(jsonResponse(sampleApp));112 },113 readFile: () => Promise.resolve("JAZZ_APP_ID=already-set\n"),114 writeFile: () => Promise.resolve(),115 });116 } catch (error) {117 threw = error instanceof ProvisionError;118 }119 assert(threw, "an existing JAZZ_APP_ID should block without --force");120 assert(!fetched, "it should refuse before generating a throwaway app");121});122123Deno.test("provisionJazzApp replaces a configured app when forced", async () => {124 let written = "";125 const result = await provisionJazzApp({126 path: ".env",127 force: true,128 fetchImpl: () => Promise.resolve(jsonResponse(sampleApp)),129 readFile: () => Promise.resolve("JAZZ_APP_ID=already-set\nOTHER=keep\n"),130 writeFile: (_path, contents) => {131 written = contents;132 return Promise.resolve();133 },134 });135 assert(result.replaced === true, "an existing app is reported as replaced");136 assert(written.includes(`JAZZ_APP_ID=${sampleApp.appId}`), "the app id is overwritten");137 assert(written.includes("OTHER=keep"), "unrelated keys survive a forced replace");138});139140Deno.test("mergeEnv rewrites every duplicate so the parsed value can never go stale", () => {141 // parseDotenv treats the LAST occurrence as authoritative; updating only the142 // first duplicate reported success while commands kept the old value.143 const existing = [144 "JAZZ_APP_ID=old-first",145 "# duplicate below wins during parsing",146 "JAZZ_APP_ID=old-last",147 "",148 ].join("\n");149 const merged = mergeEnv(existing, { JAZZ_APP_ID: "fresh" });150 const values = merged.split("\n").filter((line) => line.startsWith("JAZZ_APP_ID="));151 assert(values.length === 2, "both duplicate assignments must remain in place");152 assert(153 values.every((line) => line === "JAZZ_APP_ID=fresh"),154 "every duplicate assignment must carry the new value",155 );156});