kohrelease.test.ts

5.4 KB · raw

1import { beforeAll, describe, expect, test } from "bun:test";2import { mkdtemp, writeFile } from "node:fs/promises";3import { tmpdir } from "node:os";4import { join } from "node:path";5import { blake3 } from "@noble/hashes/blake3.js";6import { bytesToHex } from "@noble/hashes/utils.js";7import type { Hono } from "hono";8import { openBucket } from "../src/bucket/reader.ts";9import { syncAll } from "../src/bucket/sync.ts";10import { createApp } from "../src/server/app.ts";11import { openKeprDb } from "../src/server/db.ts";12import { kohWorkspace } from "./fixture.ts";1314/**15 * The joint contract (§3 item 4): `koh3 offer --builds` publishes a release into a folder remote16 * with the real koh3 binary, Kepr syncs it, and every route that reads a release agrees with the17 * bytes the build produced.18 */1920const REPO = "shipped";21const VERSION = "2.4.6";22const NOTES =23	"a release note long enough to clear forty characters and the twelve word rule easily";24// No newline: a real newline inside koh.build's command would end the line there.25const ARTIFACT = "the artifact";2627/** This machine's platform, which is what `koh build` builds for with no flags. */28function platform(): string {29	const os = process.platform === "darwin" ? "macos" : "linux";30	const arch = process.arch === "arm64" ? "aarch64" : "x86_64";31	return `${os}-${arch}`;32}3334type Latest = {35	version: string;36	notes?: string;37	platforms: Record<38		string,39		{40			url: string;41			blake3: string;42			sha256: string;43			provenance: Record<string, string>;44			signature: string;45		}46	>;47};4849describe("a release published by koh3 offer --builds", () => {50	let app: Hono;51	let bucketDir: string;52	let expected = { blake3: "", sha256: "" };53	const platformName = platform();54	const fileName = `demo-${VERSION}-${platformName}`;5556	beforeAll(async () => {57		const space = await kohWorkspace();58		bucketDir = space.bucketDir;59		await writeFile(join(space.repoDir, "VERSION"), `${VERSION}\n`);60		await writeFile(61			join(space.repoDir, "koh.build"),62			[63				"# what koh publishes in this test",64				`command = mkdir -p out && printf '${ARTIFACT}' > out/${fileName}`,65				"output = out/demo-{version}-{platform}",66				`platforms = ${platformName}`,67				"",68			].join("\n"),69		);70		space.run("init");71		space.run("remote", "folder", bucketDir);72		space.run("save", "first");73		space.run("offer", "--builds", "--notes", NOTES);7475		const bytes = new TextEncoder().encode(ARTIFACT);76		expected = {77			blake3: bytesToHex(blake3(bytes)),78			sha256: new Bun.CryptoHasher("sha256").update(bytes).digest("hex"),79		};8081		const dataDir = await mkdtemp(join(tmpdir(), "kepr-kohrelease-"));82		const kepr = openKeprDb(dataDir);83		await syncAll({84			bucket: openBucket({ kind: "folder", path: bucketDir }),85			kepr,86			mirrorDir: join(dataDir, "mirror"),87			tarballDir: join(dataDir, "tarballs"),88		});89		kepr.run(`UPDATE repos SET public = 1 WHERE name = '${REPO}'`);90		app = createApp(91			{92				domain: "kepr.test",93				dataDir,94				adminPasswordHash: "$argon2id$x",95				bucket: { kind: "folder", path: bucketDir },96			},97			kepr,98		);99	});100101	test("the newest release is the one koh built, with the hashes of its bytes", async () => {102		const res = await app.request(`/api/${REPO}/releases/latest`);103		expect(res.status).toBe(200);104		const latest = (await res.json()) as Latest;105		expect(latest.version).toBe(VERSION);106		expect(latest.notes).toBe(NOTES);107		const file = latest.platforms[platformName] as Latest["platforms"][string];108		expect(file).toBeDefined();109		expect(file.blake3).toBe(expected.blake3);110		expect(file.sha256).toBe(expected.sha256);111		expect(file.signature).toBe("");112		expect(file.provenance.version).toBe(VERSION);113		expect(file.provenance.platform).toBe(platformName);114	});115116	test("latest.json, history, version.txt and checksums.txt agree with the record", async () => {117		const record = JSON.parse(118			await Bun.file(join(bucketDir, REPO, "releases", VERSION, "release.json")).text(),119		) as { version: string; notes: string; files: { name: string; size: number }[] };120		expect(record.version).toBe(VERSION);121		expect(record.notes).toBe(NOTES);122		expect(record.files.map((file) => file.name)).toEqual([fileName]);123		expect(record.files[0]?.size).toBe(ARTIFACT.length);124125		const json = (await (await app.request(`/${REPO}/releases/latest.json`)).json()) as Latest;126		expect(json.version).toBe(VERSION);127		expect(json.platforms[platformName]?.blake3).toBe(expected.blake3);128129		const history = (await (await app.request(`/api/${REPO}/releases/history`)).json()) as {130			version: string;131			notes: string;132		}[];133		expect(history).toEqual([{ version: VERSION, notes: NOTES }]);134135		const versionText = await app.request(`/${REPO}/releases/latest/version.txt`);136		expect(await versionText.text()).toBe(VERSION);137138		const checksums = await app.request(`/${REPO}/releases/${VERSION}/checksums.txt`);139		const text = await checksums.text();140		expect(text).toContain(`sha256:${expected.sha256}  ${REPO}-${platformName}`);141		expect(text).toContain(`blake3:${expected.blake3}  ${REPO}-${platformName}`);142	});143144	test("the download redirect serves the bytes koh built", async () => {145		const res = await app.request(`/${REPO}/releases/${VERSION}/${platformName}`, {146			redirect: "manual",147		});148		expect(res.status).toBe(302);149		const target = res.headers.get("location") ?? "";150		expect(target).toContain(fileName);151		const path = target.replace(/^file:\/\//, "");152		expect(await Bun.file(path).text()).toBe(ARTIFACT);153	});154});