Skip to content
278 linesCodeBlameRaw
1/**
2 * A container build through the whole service, on Node: a D1 made of
3 * node:sqlite with the service's migrations, the other services as fakes,
4 * and Cloudflare's API (and its registry) as a recorded fetch. Run with
5 * `--experimental-transform-types` (see package.json).
6 */
7
8import assert from "node:assert/strict";
9import { createHash } from "node:crypto";
10import { readdirSync, readFileSync } from "node:fs";
11import { registerHooks } from "node:module";
12import { DatabaseSync } from "node:sqlite";
13import { beforeEach, test } from "node:test";
14
15registerHooks({
16 resolve(specifier, context, next) {
17 try {
18 return next(specifier, context);
19 } catch (error) {
20 if (specifier.startsWith(".")) return next(`${specifier}.ts`, context);
21 throw error;
22 }
23 },
24});
25const { default: worker } = await import("./index.ts");
26
27function d1(db: DatabaseSync) {
28 const value = (v: unknown) => (v === undefined ? null : typeof v === "boolean" ? Number(v) : v);
29 const statement = (sql: string, params: unknown[] = []) => ({
30 sql,
31 params,
32 bind: (...args: unknown[]) => statement(sql, args.map(value)),
33 async first<T>() {
34 return (db.prepare(sql).get(...(params as never[])) as T) ?? null;
35 },
36 async all<T>() {
37 return { results: db.prepare(sql).all(...(params as never[])) as T[], success: true };
38 },
39 async run() {
40 const r = db.prepare(sql).run(...(params as never[]));
41 return { success: true, meta: { changes: Number(r.changes) } };
42 },
43 });
44 return {
45 prepare: (sql: string) => statement(sql),
46 async batch(statements: ReturnType<typeof statement>[]) {
47 return statements.map((s) => ({ results: db.prepare(s.sql).all(...(s.params as never[])) }));
48 },
49 };
50}
51
52const PROJECT = "prj_web";
53const SCRIPT = "web-acme";
54const TOKEN = "build-token";
55const DIGEST = "b".repeat(64);
56const IMAGE = `registry.cloudflare.com/acct/g1t-${SCRIPT}@sha256:${DIGEST}`;
57
58function world(containers: string) {
59 const sqlite = new DatabaseSync(":memory:");
60 const dir = new URL("../migrations/", import.meta.url);
61 for (const file of readdirSync(dir).sort()) sqlite.exec(readFileSync(new URL(file, dir), "utf8"));
62 const project = {
63 id: PROJECT,
64 workspace: "acme",
65 slug: "web",
66 name: "web",
67 primary: true,
68 source: { kind: "hosted", repoId: "rep_web", repo: { namespace: "acme", name: "web" }, defaultBranch: "main", rootDir: null },
69 };
70 const ok = (value: unknown) => ({ ok: true, value });
71 const service = (answer: (method: string, args: any) => unknown) => ({
72 async fetch(input: string | Request, init?: RequestInit) {
73 const url = typeof input === "string" ? input : input.url;
74 const method = new URL(url).pathname.split("/").pop()!;
75 const args = init?.body ? JSON.parse(String(init.body)) : {};
76 return Response.json((await answer(method, args)) ?? ok(null));
77 },
78 });
79 const state = { sqlite, charged: [] as any[], pending: [] as any[] };
80 const env = {
81 DB: d1(sqlite),
82 CLOUDFLARE_API_TOKEN: "test",
83 CLOUDFLARE_ACCOUNT_ID: "acct",
84 DISPATCH_NAMESPACE: "g1t-deployments",
85 SITE: "https://g1t.sh",
86 CONTAINERS: containers,
87 REPOS: service(() => ok({ isPrivate: true })),
88 PROJECTS: service((method) => (method === "by_repo" ? [project] : ok(project))),
89 IDENTITY: service(() => ok(null)),
90 BILLING: service((method, args) => {
91 if (method === "prices") return { prices: [] };
92 if (method === "charge_feature") state.charged.push(args);
93 if (method === "note_pending") state.pending.push(args);
94 return ok(true);
95 }),
96 WORK: service(() => ok(null)),
97 ACTIONS: service(() => ({ secrets: { DATABASE_URL: "postgres://db" }, variables: { NODE_ENV: "production" } })),
98 RUNNER: service(() => ok(true)),
99 };
100 return { state, env };
101}
102
103let w: ReturnType<typeof world>;
104let calls: string[] = [];
105let uploads: Record<string, any>[] = [];
106let applications: { id: string; name: string }[] = [];
107let registryHas = true;
108const realFetch = globalThis.fetch;
109
110function setUp(containers: string) {
111 w = world(containers);
112 calls = [];
113 uploads = [];
114 applications = [];
115 registryHas = true;
116 globalThis.fetch = (async (input: string | Request, init?: RequestInit) => {
117 const url = new URL(typeof input === "string" ? input : input.url);
118 const method = init?.method ?? "GET";
119 calls.push(`${method} ${url.host === "api.cloudflare.com" ? url.pathname.replace("/client/v4/accounts/acct", "") : url.host + url.pathname}`);
120 const result = (value: unknown) => Response.json({ success: true, result: value });
121 if (url.host === "registry.cloudflare.com") return new Response(null, { status: registryHas ? 200 : 404 });
122 if (url.pathname.endsWith("/credentials")) return result({ username: "v1", password: "push-secret", registry_host: "registry.cloudflare.com" });
123 if (method === "PUT" && url.pathname.includes("/scripts/")) {
124 const form = init?.body as FormData;
125 uploads.push(JSON.parse(String(form.get("metadata"))));
126 return result({});
127 }
128 if (url.pathname.endsWith("/durable_objects/namespaces")) {
129 return result([{ id: "ns1", class: "G1tApp", script: SCRIPT, dispatch_namespace: "g1t-deployments" }]);
130 }
131 if (url.pathname.endsWith("/containers/applications") && method === "GET") return result(applications);
132 if (url.pathname.endsWith("/containers/applications") && method === "POST") {
133 applications.push({ id: "app1", name: JSON.parse(String(init?.body)).name });
134 return result({ id: "app1" });
135 }
136 if (url.pathname.endsWith("/graphql")) return Response.json({ data: { viewer: { accounts: [{}] } } });
137 return result([]);
138 }) as typeof fetch;
139 const db = w.state.sqlite;
140 db.prepare(
141 `INSERT INTO settings (project_id, repo_id, workspace, slug, enabled, previews, production, idle_days, updated_at, build_as, instance_type)
142 VALUES (?, 'rep_web', 'acme', 'web', 1, 1, 1, 7, '2026-10-12T00:00:00Z', 'docker', 'basic')`,
143 ).run(PROJECT);
144 open("dpl_1");
145}
146
147/** A build under way, as `open` leaves it. */
148function open(id: string) {
149 w.state.sqlite
150 .prepare(
151 `INSERT INTO deployments (id, project_id, workspace, slug, repo_id, repo, kind, commit_sha, script, status, token_hash,
152 trusted, created_by, created_at)
153 VALUES (?, ?, 'acme', 'web', 'rep_web', 'acme/web', 'production', 'abc1234', ?, 'building', ?, 1, 'g1t', ?)`,
154 )
155 .run(id, PROJECT, SCRIPT, createHash("sha256").update(TOKEN).digest("hex"), new Date().toISOString());
156}
157
158beforeEach(() => setUp("on"));
159
160process.on("exit", () => {
161 globalThis.fetch = realFetch;
162});
163
164async function post(path: string, body: unknown): Promise<any> {
165 const response = await worker.fetch(
166 new Request(`https://deployments${path}`, { method: "POST", body: JSON.stringify(body) }),
167 w.env as never,
168 {} as never,
169 );
170 return { status: response.status, ...(await response.json()) };
171}
172
173const rows = (sql: string, ...params: unknown[]) =>
174 (w.state.sqlite.prepare(sql).all(...(params as never[])) as any[]).map((row) => ({ ...row }));
175
176const finish = (container: unknown) =>
177 post("/jobs/dpl_1/finish", { token: TOKEN, worker: {}, detected: "container", framework: "node", container, buildSeconds: 20 });
178
179test("while containers are off, the registry step says so in its own words", async () => {
180 setUp("off");
181 const answer = await post("/jobs/dpl_1/registry", { token: TOKEN });
182 assert.equal(answer.status, 409);
183 assert.equal(answer.error.code, "not_enabled");
184 assert.ok(!calls.some((call) => call.includes("credentials")), "no login is minted");
185});
186
187test("a build gets a push-only login for its app's repository, held to its size", async () => {
188 const answer = await post("/jobs/dpl_1/registry", { token: TOKEN });
189 assert.deepEqual(answer.value, {
190 registry: "registry.cloudflare.com",
191 repository: `acct/g1t-${SCRIPT}`,
192 tag: "dpl_1",
193 username: "v1",
194 password: "push-secret",
195 maxImageBytes: 4_000_000_000,
196 instanceType: "basic",
197 });
198 // Another build's token gets nothing.
199 assert.equal((await post("/jobs/dpl_1/registry", { token: "nope" })).status, 404);
200});
201
202test("an image outside the app's repository, or not in the registry, is never run", async () => {
203 const other = await finish({ image: `registry.cloudflare.com/acct/g1t-other@sha256:${DIGEST}`, port: 3000 });
204 assert.deepEqual(other.value, false);
205 assert.match(rows("SELECT error FROM deployments WHERE id = 'dpl_1'")[0].error, /did not report an image by digest/);
206 assert.equal(uploads.length, 0);
207 open("dpl_2");
208 registryHas = false;
209 const missing = await post("/jobs/dpl_2/finish", { token: TOKEN, detected: "container", container: { image: IMAGE } });
210 assert.deepEqual(missing.value, false);
211 assert.match(rows("SELECT error FROM deployments WHERE id = 'dpl_2'")[0].error, /is not in the registry/);
212});
213
214test("a container app runs behind g1t's Worker, is metered by its own token, and comes down whole", async () => {
215 const done = await finish({ image: IMAGE, port: 3000, sizeBytes: 123 });
216 assert.deepEqual(done.value, true);
217 // g1t's Worker, its class, and the project's entries bound to it.
218 const [metadata] = uploads;
219 assert.deepEqual(metadata.exports, { G1tApp: { type: "durable-object", storage: "sqlite" } });
220 const binding = (name: string) => metadata.bindings.find((b: any) => b.name === name);
221 assert.equal(binding("G1T_PORT").text, "3000");
222 assert.equal(binding("G1T_INSTANCE").text, "basic");
223 assert.equal(binding("G1T_METER_URL").text, `https://api.g1t.sh/deployments/apps/${SCRIPT}/meter`);
224 assert.deepEqual(binding("DATABASE_URL"), { type: "secret_text", name: "DATABASE_URL", text: "postgres://db" });
225 assert.ok(calls.includes("POST /containers/applications"));
226 assert.deepEqual(rows("SELECT runtime, container_app_id, instance_type FROM apps"), [{ runtime: "container", container_app_id: "app1", instance_type: "basic" }]);
227 assert.deepEqual(rows("SELECT detected, framework, image, status FROM deployments WHERE id = 'dpl_1'"), [
228 { detected: "container", framework: "node", image: IMAGE, status: "ready" },
229 ]);
230 // The next deploy rolls the application out to the new image.
231 open("dpl_2");
232 await post("/jobs/dpl_2/finish", { token: TOKEN, detected: "container", container: { image: IMAGE, port: 3000 } });
233 assert.ok(calls.includes("POST /containers/applications/app1/rollouts"));
234
235 // Its reports: only with the token it was deployed with, each counter only raised.
236 const token = uploads.at(-1)!.bindings.find((b: any) => b.name === "G1T_METER_TOKEN").text;
237 const month = new Date().toISOString().slice(0, 7);
238 const report = { token, epoch: "epoch-0001", month, awakeSeconds: 3600, egressBytes: 1_000_000_000, instanceType: "basic" };
239 assert.equal((await post(`/apps/${SCRIPT}/meter`, { ...report, token: "wrong" })).status, 404);
240 assert.equal((await post(`/apps/${SCRIPT}/meter`, report)).status, 200);
241 await post(`/apps/${SCRIPT}/meter`, { ...report, awakeSeconds: 60 });
242 assert.deepEqual(rows("SELECT awake_seconds, egress_bytes FROM container_meters"), [{ awake_seconds: 3600, egress_bytes: 1_000_000_000 }]);
243
244 // It is noted for the spend limit as it runs: basic for an hour, and 1 GB at $0.05.
245 w.state.sqlite.exec("INSERT OR IGNORE INTO meters (namespace, month) VALUES ('acme', strftime('%Y-%m', 'now'))");
246 await worker.scheduled({} as never, w.env as never);
247 const pending = w.state.pending.find((p) => p.workspace === "acme");
248 const second = 0.25 * 20 + 1 * 2.5 + 4 * 0.07 + 1.5625;
249 assert.equal(pending.costMicros, Math.ceil(second * 3600 + 50_000));
250 assert.match(pending.detail, /1 container hours and 1 GB sent/);
251
252 // Taken down: its container first, then its Worker.
253 calls = [];
254 await post("/rpc/take_down", { actor: { id: "usr_1", username: "a", kind: "user", workspaces: [{ slug: "acme", role: "owner" }] }, project: { workspace: "acme", slug: "web" }, branch: null });
255 const deleteApp = calls.indexOf("DELETE /containers/applications/app1");
256 const deleteScript = calls.findIndex((call) => call.startsWith("DELETE /workers/dispatch/namespaces/g1t-deployments/scripts/web-acme"));
257 assert.ok(deleteApp >= 0 && deleteScript > deleteApp, calls.join("\n"));
258});
259
260test("a paused container app keeps its class and stops serving", async () => {
261 await finish({ image: IMAGE, port: 3000 });
262 uploads = [];
263 // Its workspace reached its limit.
264 w.env.BILLING = {
265 async fetch(input: string | Request, init?: RequestInit) {
266 const method = new URL(typeof input === "string" ? input : input.url).pathname.split("/").pop();
267 if (method === "check_limit") return Response.json({ ok: true, value: { state: "stopped" } });
268 if (method === "prices") return Response.json({ prices: [] });
269 return Response.json({ ok: true, value: true, init: !!init });
270 },
271 } as never;
272 await worker.scheduled({} as never, w.env as never);
273 const paused = uploads.find((u) => u.bindings.some((b: any) => b.name === "G1T_MODE" && b.text === "paused"));
274 assert.ok(paused, "the Worker was put back paused");
275 assert.deepEqual(paused.exports, { G1tApp: { type: "durable-object", storage: "sqlite" } });
276 assert.ok(!paused.bindings.some((b: any) => b.name === "DATABASE_URL"), "a paused app holds no secrets");
277 assert.ok(rows("SELECT paused_at FROM apps")[0].paused_at);
278});