Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

68 lines · 3.7 KBRaw History
1import { Hono } from "hono";
2import { describe, expect, it } from "vitest";
3import type { Env } from "../../src/env.js";
4import { errorResponse } from "../../src/http/common.js";
5import { HttpError } from "../../src/lib/util.js";
6import { errorRecord, KEEP_ERRORS, pathToKeep, recordServerError, redact, serverErrors } from "../../src/lib/server-errors.js";
7import { testD1 } from "./stubs/d1.js";
8
9const settle = () => new Promise((r) => setTimeout(r, 0));
10
11describe("server errors", () => {
12 it("mask tokens and keep secret-carrying paths as their route", () => {
13 expect(redact("push to https://x-token:dfy_abcdefghijklmnop@diffinery.com/a/b.git failed")).toBe("push to https://…@diffinery.com/a/b.git failed");
14 expect(redact("token dfy_abcdefghijklmnop and ghs_0123456789abcdef")).toBe("token dfy_… and ghs_…");
15 expect(redact("Authorization: Bearer abc.def.ghijklmnop")).toBe("Authorization: Bearer …");
16 expect(pathToKeep("/signin/s3cr3t", "/signin/:token")).toBe("/signin/:token");
17 expect(pathToKeep("/api/v3/repos/a/b/actions/secrets/X", "/api/v3/repos/:owner/:repo/actions/secrets/:name")).toBe("/api/v3/repos/a/b/actions/secrets/X");
18 });
19
20 it("a 5xx answer carries a request id that finds its record; other answers aren't recorded", async () => {
21 const env = { DB: testD1() } as unknown as Env;
22 const app = new Hono<{ Bindings: Env }>();
23 app.onError((e, c) => errorResponse(e, c as never));
24 app.put("/api/v3/repos/:owner/:repo/actions/secrets/:name", () => {
25 throw new Error("D1_ERROR: storage operation exceeded timeout, using dfy_abcdefghijklmnop");
26 });
27 app.get("/signin/:token", () => {
28 throw new Error("boom");
29 });
30 app.get("/missing", () => {
31 throw new HttpError(404, "Not Found");
32 });
33
34 const res = await app.request("/api/v3/repos/acme/app/actions/secrets/CLOUDFLARE_API_TOKEN?x=1", { method: "PUT", headers: { "cf-ray": "8d2f3a1b4c5d6e7f-FRA" }, body: "{\"encrypted_value\":\"secret\"}" }, env);
35 expect(res.status).toBe(500);
36 const body = (await res.json()) as { message: string; request_id: string };
37 expect(body).toEqual({ message: "internal error", request_id: "8d2f3a1b4c5d6e7f-FRA" });
38 expect(res.headers.get("x-request-id")).toBe("8d2f3a1b4c5d6e7f-FRA");
39 expect((await app.request("/signin/s3cr3t", {}, env)).status).toBe(500);
40 expect((await app.request("/missing", {}, env)).status).toBe(404);
41 await settle();
42
43 const [e] = await serverErrors(env, { id: body.request_id });
44 expect(e).toMatchObject({
45 method: "PUT",
46 route: "/api/v3/repos/:owner/:repo/actions/secrets/:name",
47 path: "/api/v3/repos/acme/app/actions/secrets/CLOUDFLARE_API_TOKEN",
48 status: 500,
49 message: "Error: D1_ERROR: storage operation exceeded timeout, using dfy_…",
50 });
51 expect(e!.stack).toContain("D1_ERROR");
52 expect(JSON.stringify(await serverErrors(env))).not.toMatch(/s3cr3t|encrypted_value|x=1|abcdefghijklmnop/);
53 expect((await serverErrors(env)).map((r) => r.path)).toEqual(expect.arrayContaining(["/signin/:token"]));
54 expect(await serverErrors(env)).toHaveLength(2);
55 });
56
57 it("keep only the newest records", async () => {
58 const env = { DB: testD1() } as unknown as Env;
59 for (let i = 0; i < KEEP_ERRORS + 5; i++) {
60 const r = errorRecord(new Error(`e${i}`), { id: `r${i}`, method: "GET", route: "/x", path: "/x", status: 500 });
61 await recordServerError(env, { ...r, at: new Date(Date.UTC(2026, 9, 14, 0, 0, i)).toISOString() });
62 }
63 const all = await serverErrors(env, { limit: 1000 });
64 expect(all).toHaveLength(KEEP_ERRORS);
65 expect(all[0]!.id).toBe(`r${KEEP_ERRORS + 4}`);
66 expect(await serverErrors(env, { id: "r4" })).toEqual([]);
67 });
68});