import { describe, expect, test } from "bun:test"; import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { addEntry, fileIssue, filingsSince, findIssue, hashToken, issueCounts, listIssues, type NewIssue, threadEntries, } from "../src/issues/store.ts"; import { openKeprDb } from "../src/server/db.ts"; const newIssue = (repo: string, title: string, filedAt = 1000): NewIssue => ({ repo, title, body: "It broke.", type: "bug", version: "1.0.0", reporterName: "", contactEmail: "me@example.com", ipHash: "ip-a", filedAt, }); describe("issue data layer", async () => { const db = openKeprDb(await mkdtemp(join(tmpdir(), "kepr-issues-"))); test("numbers are per repo and continue after an imported number", () => { expect(fileIssue(db, newIssue("app", "one")).number).toBe(1); expect(fileIssue(db, newIssue("app", "two")).number).toBe(2); expect(fileIssue(db, newIssue("lib", "first")).number).toBe(1); db.run( "INSERT INTO issues (repo, number, title, body, type, filed_at) VALUES ('lib', 41, 'from kepr 2', 'x', 'idea', 5)", ); expect(fileIssue(db, newIssue("lib", "next")).number).toBe(42); }); test("only the token's hash is stored, and public reads leave out contact and IP", () => { const { number, token } = fileIssue(db, newIssue("app", "secret")); expect(token).toMatch(/^[0-9a-f]{64}$/); const issue = findIssue(db, "app", number); expect(issue?.token_hash).toBe(hashToken(token)); expect(JSON.stringify(issue)).not.toContain(token); expect(Object.keys(issue ?? {})).not.toContain("contact_email"); expect(Object.keys(issue ?? {})).not.toContain("ip_hash"); }); test("hidden issues vanish from lists, counts and lookups but still count against the IP", () => { const before = filingsSince(db, "ip-a", 0); const { number } = fileIssue(db, newIssue("app", "spam")); db.run("UPDATE issues SET hidden = 1 WHERE repo = 'app' AND number = ?", [number]); expect(findIssue(db, "app", number)).toBeNull(); expect(listIssues(db, "app", "open").map((i) => i.title)).not.toContain("spam"); expect(issueCounts(db, "app")).toEqual({ open: 3, closed: 0 }); expect(filingsSince(db, "ip-a", 0)).toBe(before + 1); expect(filingsSince(db, "ip-a", 1000)).toBe(0); }); test("the thread keeps entries in order with their author kind", () => { const issue = findIssue(db, "app", 1); if (!issue) throw new Error("issue 1 missing"); addEntry(db, issue.id, "reporter", "More detail.", 2000); addEntry(db, issue.id, "operator", "Thanks.", 3000); expect(threadEntries(db, issue.id).map((e) => [e.author, e.body])).toEqual([ ["reporter", "More detail."], ["operator", "Thanks."], ]); }); });