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";1314151617181920const 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";2425const 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 koh 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});