1import { describe, expect, test } from "bun:test";2import { mkdtemp } from "node:fs/promises";3import { tmpdir } from "node:os";4import { join } from "node:path";5import {6 addEntry,7 fileIssue,8 filingsSince,9 findIssue,10 hashToken,11 issueCounts,12 listIssues,13 type NewIssue,14 threadEntries,15} from "../src/issues/store.ts";16import { openKeprDb } from "../src/server/db.ts";1718const newIssue = (repo: string, title: string, filedAt = 1000): NewIssue => ({19 repo,20 title,21 body: "It broke.",22 type: "bug",23 version: "1.0.0",24 reporterName: "",25 contactEmail: "[email protected]",26 ipHash: "ip-a",27 filedAt,28});2930describe("issue data layer", async () => {31 const db = openKeprDb(await mkdtemp(join(tmpdir(), "kepr-issues-")));3233 test("numbers are per repo and continue after an imported number", () => {34 expect(fileIssue(db, newIssue("app", "one")).number).toBe(1);35 expect(fileIssue(db, newIssue("app", "two")).number).toBe(2);36 expect(fileIssue(db, newIssue("lib", "first")).number).toBe(1);37 db.run(38 "INSERT INTO issues (repo, number, title, body, type, filed_at) VALUES ('lib', 41, 'from kepr 2', 'x', 'idea', 5)",39 );40 expect(fileIssue(db, newIssue("lib", "next")).number).toBe(42);41 });4243 test("only the token's hash is stored, and public reads leave out contact and IP", () => {44 const { number, token } = fileIssue(db, newIssue("app", "secret"));45 expect(token).toMatch(/^[0-9a-f]{64}$/);46 const issue = findIssue(db, "app", number);47 expect(issue?.token_hash).toBe(hashToken(token));48 expect(JSON.stringify(issue)).not.toContain(token);49 expect(Object.keys(issue ?? {})).not.toContain("contact_email");50 expect(Object.keys(issue ?? {})).not.toContain("ip_hash");51 });5253 test("hidden issues vanish from lists, counts and lookups but still count against the IP", () => {54 const before = filingsSince(db, "ip-a", 0);55 const { number } = fileIssue(db, newIssue("app", "spam"));56 db.run("UPDATE issues SET hidden = 1 WHERE repo = 'app' AND number = ?", [number]);57 expect(findIssue(db, "app", number)).toBeNull();58 expect(listIssues(db, "app", "open").map((i) => i.title)).not.toContain("spam");59 expect(issueCounts(db, "app")).toEqual({ open: 3, closed: 0 });60 expect(filingsSince(db, "ip-a", 0)).toBe(before + 1);61 expect(filingsSince(db, "ip-a", 1000)).toBe(0);62 });6364 test("the thread keeps entries in order with their author kind", () => {65 const issue = findIssue(db, "app", 1);66 if (!issue) throw new Error("issue 1 missing");67 addEntry(db, issue.id, "reporter", "More detail.", 2000);68 addEntry(db, issue.id, "operator", "Thanks.", 3000);69 expect(threadEntries(db, issue.id).map((e) => [e.author, e.body])).toEqual([70 ["reporter", "More detail."],71 ["operator", "Thanks."],72 ]);73 });74});