kohrelease.test.ts

5.1 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, platform } 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";2627type Latest = {28	version: string;29	notes?: string;30	platforms: Record<31		string,32		{33			url: string;34			blake3: string;35			sha256: string;36			provenance: Record<string, string>;37			signature: string;38		}39	>;40};4142describe("a release published by koh3 offer --builds", () => {43	let app: Hono;44	let bucketDir: string;45	let expected = { blake3: "", sha256: "" };46	const platformName = platform();47	const fileName = `demo-${VERSION}-${platformName}`;4849	beforeAll(async () => {50		const space = await kohWorkspace();51		bucketDir = space.bucketDir;52		await writeFile(join(space.repoDir, "VERSION"), `${VERSION}\n`);53		await writeFile(54			join(space.repoDir, "koh.build"),55			[56				"# what koh publishes in this test",57				`command = mkdir -p out && printf '${ARTIFACT}' > out/${fileName}`,58				"output = out/demo-{version}-{platform}",59				`platforms = ${platformName}`,60				"",61			].join("\n"),62		);63		space.run("init");64		space.run("remote", "folder", bucketDir);65		space.run("save", "first");66		space.run("offer", "--builds", "--notes", NOTES);6768		const bytes = new TextEncoder().encode(ARTIFACT);69		expected = {70			blake3: bytesToHex(blake3(bytes)),71			sha256: new Bun.CryptoHasher("sha256").update(bytes).digest("hex"),72		};7374		const dataDir = await mkdtemp(join(tmpdir(), "kepr-kohrelease-"));75		const kepr = openKeprDb(dataDir);76		await syncAll({77			bucket: openBucket({ kind: "folder", path: bucketDir }),78			kepr,79			mirrorDir: join(dataDir, "mirror"),80		});81		kepr.run(`UPDATE repos SET public = 1 WHERE name = '${REPO}'`);82		app = createApp(83			{84				domain: "kepr.test",85				dataDir,86				adminPasswordHash: "$argon2id$x",87				bucket: { kind: "folder", path: bucketDir },88			},89			kepr,90		);91	});9293	test("the newest release is the one koh built, with the hashes of its bytes", async () => {94		const res = await app.request(`/api/${REPO}/releases/latest`);95		expect(res.status).toBe(200);96		const latest = (await res.json()) as Latest;97		expect(latest.version).toBe(VERSION);98		expect(latest.notes).toBe(NOTES);99		const file = latest.platforms[platformName] as Latest["platforms"][string];100		expect(file).toBeDefined();101		expect(file.blake3).toBe(expected.blake3);102		expect(file.sha256).toBe(expected.sha256);103		expect(file.signature).toBe("");104		expect(file.provenance.version).toBe(VERSION);105		expect(file.provenance.platform).toBe(platformName);106	});107108	test("latest.json, history, version.txt and checksums.txt agree with the record", async () => {109		const record = JSON.parse(110			await Bun.file(join(bucketDir, REPO, "releases", VERSION, "release.json")).text(),111		) as { version: string; notes: string; files: { name: string; size: number }[] };112		expect(record.version).toBe(VERSION);113		expect(record.notes).toBe(NOTES);114		expect(record.files.map((file) => file.name)).toEqual([fileName]);115		expect(record.files[0]?.size).toBe(ARTIFACT.length);116117		const json = (await (await app.request(`/${REPO}/releases/latest.json`)).json()) as Latest;118		expect(json.version).toBe(VERSION);119		expect(json.platforms[platformName]?.blake3).toBe(expected.blake3);120121		const history = (await (await app.request(`/api/${REPO}/releases/history`)).json()) as {122			version: string;123			notes: string;124		}[];125		expect(history).toEqual([{ version: VERSION, notes: NOTES }]);126127		const versionText = await app.request(`/${REPO}/releases/latest/version.txt`);128		expect(await versionText.text()).toBe(VERSION);129130		const checksums = await app.request(`/${REPO}/releases/${VERSION}/checksums.txt`);131		const text = await checksums.text();132		expect(text).toContain(`sha256:${expected.sha256}  ${REPO}-${platformName}`);133		expect(text).toContain(`blake3:${expected.blake3}  ${REPO}-${platformName}`);134	});135136	test("the download redirect serves the bytes koh built", async () => {137		const res = await app.request(`/${REPO}/releases/${VERSION}/${platformName}`, {138			redirect: "manual",139		});140		expect(res.status).toBe(302);141		const target = res.headers.get("location") ?? "";142		expect(target).toContain(fileName);143		const path = target.replace(/^file:\/\//, "");144		expect(await Bun.file(path).text()).toBe(ARTIFACT);145	});146});