lambda.test.ts

7.2 kB · typescript · 166 lines

1import { expect, test } from "bun:test";2import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";3import { tmpdir } from "node:os";4import { join } from "node:path";5import { lambda, modules, type Get, type Head, type Wake } from "./lambda.ts";67/* FIXTURES */89const SHA = "0123456789abcdef0123456789abcdef01234567";10const SHELF = "fedcba9876543210fedcba9876543210fedcba98";11const HOLD = mkdtempSync(join(tmpdir(), "head-"));1213process.env.SHELF_REPO = "owner/shelf";1415const had = process.env.DRY_DIR;16delete process.env.DRY_DIR;1718const net = lambda({19  source: "owner/net",20  head: "build/net/head",21  dir: "/tmp/net",22  agent: "net-builder",23  bucket: "NET_BUCKET",24  folder: "site",25  shelf: "SHELF_REPO",26  sources: ["push", "schedule", "automator", "manual"],27  local: join(HOLD, "net"),28});2930const shop = lambda({31  source: "owner/shop",32  head: "data/build/head",33  dir: "/tmp/shop",34  agent: "shop-builder",35  bucket: "SHOP_BUCKET",36  folder: "site",37  local: join(HOLD, "shop"),38  hash: () => "d0",39});4041if (had) process.env.DRY_DIR = had;4243const stored: Head = { sha: "aaaaaaabbbbbbbcccccccdddddddeeeeeeefffff", etag: "old", shelf: SHELF, shelfEtag: "shelf-old", data: "" };4445const reply = (body: unknown, opts: { ok?: boolean; status?: number; etag?: string } = {}) => ({46  ok: opts.ok ?? true,47  status: opts.status ?? 200,48  json: async () => body,49  text: async () => JSON.stringify(body),50  headers: { get: (name: string) => (name === "etag" ? (opts.etag ?? null) : null) },51});5253const github = (plan: { compare?: string; code?: number; poll?: number; sha?: string; etag?: string }): Get => async (url) => {54  if (url.includes("/compare/")) return plan.compare ? reply({ status: plan.compare }) : reply({}, { ok: false, status: plan.code ?? 404 });55  if (plan.poll === 304) return reply({}, { ok: false, status: 304 });56  return reply({ sha: plan.sha }, { etag: plan.etag });57};5859const wake = (on: Wake["on"], sha: string): Wake => ({ source: "push", on, sha });6061/* EVENT */6263test("the payload parser reads a schedule tick and both push wakes", () => {64  expect(net.readEvent(JSON.stringify({ source: "schedule" }))).toEqual({ source: "schedule", on: "", sha: "" });65  expect(net.readEvent(JSON.stringify({ source: "push", repo: "owner/net", sha: SHA }))).toEqual({ source: "push", on: "source", sha: SHA });66  expect(net.readEvent(JSON.stringify({ source: "push", repo: "owner/shelf", sha: SHA }))).toEqual({ source: "push", on: "shelf", sha: SHA });67});6869test("the payload parser survives junk, an unknown source and a source word the site does not take", () => {70  expect(net.readEvent("")).toEqual({ source: "", on: "", sha: "" });71  expect(net.readEvent("not json")).toEqual({ source: "", on: "", sha: "" });72  expect(net.readEvent(JSON.stringify({ source: "nonsense", repo: "owner/net" }))).toEqual({ source: "", on: "source", sha: "" });73  expect(shop.readEvent(JSON.stringify({ source: "automator", repo: "owner/shop" }))).toEqual({ source: "", on: "source", sha: "" });74});7576test("the payload parser keeps a good sha, lowercases it and drops a malformed one", () => {77  const pin = (sha: string) => net.readEvent(JSON.stringify({ source: "manual", repo: "owner/net", sha })).sha;78  expect(pin(SHA)).toBe(SHA);79  expect(pin(SHA.toUpperCase())).toBe(SHA);80  expect(pin("nope")).toBe("");81  expect(pin("../../x/y/tar.gz/main")).toBe("");82  expect(pin(`${SHA}0`)).toBe("");83});8485test("a sha with no repo, or another repo's, names no target", () => {86  expect(net.readEvent(JSON.stringify({ body: JSON.stringify({ source: "manual", sha: SHA }) }))).toEqual({ source: "manual", on: "", sha: SHA });87  expect(net.readEvent(JSON.stringify({ source: "push", repo: "owner/fork", sha: SHA }))).toEqual({ source: "push", on: "", sha: SHA });88});8990/* GITHUB */9192test("the ancestor check accepts a sha main is ahead of or identical to", async () => {93  expect(await net.onMain("owner/net", SHA, github({ compare: "ahead" }))).toBe(true);94  expect(await net.onMain("owner/shelf", SHA, github({ compare: "identical" }))).toBe(true);95});9697test("the ancestor check refuses another status, a refused compare and a dead fetch", async () => {98  expect(await net.onMain("owner/net", SHA, github({ compare: "behind" }))).toBe(false);99  expect(await net.onMain("owner/net", SHA, github({ compare: "diverged" }))).toBe(false);100  expect(await net.onMain("owner/net", SHA, github({ code: 403 }))).toBe(false);101  expect(102    await net.onMain("owner/net", SHA, async () => {103      throw new Error("getaddrinfo ENOTFOUND api.github.com");104    }),105  ).toBe(false);106});107108/* PLAN */109110test("a wake for a sha already built is a no-op, and an unfinished head never answers seen", () => {111  const done: Head = { ...stored, sha: SHA };112  expect(net.seen(wake("source", SHA), done)).toBe(SHA.slice(0, 7));113  expect(net.seen(wake("shelf", SHELF), done)).toBe(`shelf ${SHELF.slice(0, 7)}`);114  expect(net.seen(wake("source", SHA), { ...done, shelf: "" })).toBe("");115  expect(net.seen(wake("source", SHA), { ...done, sha: "" })).toBe("");116});117118test("a site with a data hash never answers seen, because the data moves without the sha", () => {119  expect(shop.seen(wake("source", SHA), { ...stored, sha: SHA, data: "d0" })).toBe("");120});121122test("a sha wake stores an empty etag", async () => {123  const out = await net.freshen(wake("source", SHA), stored, github({ compare: "ahead", sha: "polled", etag: "fresh" }));124  expect(out.sha).toBe(SHA);125  expect(out.etag).toBe("");126});127128test("a sha with no repo beside it falls to the etag poll", async () => {129  const out = await net.freshen(wake("", SHA), stored, github({ compare: "ahead", sha: "polled", etag: "fresh" }));130  expect(out.sha).toBe("polled");131  expect(out.etag).toBe("fresh");132});133134test("a refused compare falls to the etag poll", async () => {135  const out = await net.freshen(wake("source", SHA), stored, github({ code: 403, sha: "polled", etag: "fresh" }));136  expect(out.sha).toBe("polled");137});138139test("a 304 from the etag poll keeps the stored mark", async () => {140  const out = await net.freshen({ source: "schedule", on: "", sha: "" }, stored, github({ poll: 304 }));141  expect(out).toEqual(stored);142});143144/* HEAD */145146test("the head file carries only the lines the site uses", async () => {147  await net.writeHead(null, { sha: SHA, etag: "e", shelf: SHELF, shelfEtag: "se", data: "ignored" });148  expect(readFileSync(join(HOLD, "net", "head"), "utf8")).toBe(`${SHA}\ne\n${SHELF}\nse\n`);149  expect(await net.readHead(null)).toEqual({ sha: SHA, etag: "e", shelf: SHELF, shelfEtag: "se", data: "" });150  await shop.writeHead(null, { sha: SHA, etag: "e", shelf: "no", shelfEtag: "no", data: "d0" });151  expect(readFileSync(join(HOLD, "shop", "head"), "utf8")).toBe(`${SHA}\ne\nd0\n`);152  expect(await shop.readHead(null)).toEqual({ sha: SHA, etag: "e", shelf: "", shelfEtag: "", data: "d0" });153});154155/* MODULES */156157test("the modules step takes the layer only when it holds this lockfile", () => {158  const site = mkdtempSync(join(tmpdir(), "site-"));159  const layer = mkdtempSync(join(tmpdir(), "layer-"));160  writeFileSync(join(site, "bun.lock"), "lock");161  expect(modules(site, layer)).toBe("absent");162  writeFileSync(join(layer, "bun.lock.sha256"), "nope\n");163  expect(modules(site, layer)).toBe("stale");164  writeFileSync(join(layer, "bun.lock.sha256"), `${new Bun.CryptoHasher("sha256").update("lock").digest("hex")}\n`);165  expect(modules(site, layer)).toBe("layer");166});