Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions src/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { test, expect, vi, afterEach } from "vitest";
import { fetchAnalyzerConfig, DEFAULT_CONFIG } from "./config.ts";

afterEach(() => {
vi.restoreAllMocks();
});

test("returns parsed config from successful response", async () => {
const config = {
minimumCost: 100,
regressionThreshold: 0.5,
ignoredQueryHashes: ["abc123"],
lastSeenQueries: ["hash1"],
};
vi.spyOn(globalThis, "fetch").mockResolvedValue(
Response.json(config, { status: 200 }),
);

const result = await fetchAnalyzerConfig("https://api.example.com", "my/repo");
expect(result).toEqual(config);
});

test("returns defaults when response is not ok", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response("Not Found", { status: 404 }),
);

const result = await fetchAnalyzerConfig("https://api.example.com", "my/repo");
expect(result).toEqual(DEFAULT_CONFIG);
});

test("returns defaults when fetch throws", async () => {
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("network error"));

const result = await fetchAnalyzerConfig("https://api.example.com", "my/repo");
expect(result).toEqual(DEFAULT_CONFIG);
});

test("constructs correct URL with trailing slash stripped", async () => {
const mockFetch = vi.spyOn(globalThis, "fetch").mockResolvedValue(
Response.json(DEFAULT_CONFIG, { status: 200 }),
);

await fetchAnalyzerConfig("https://api.example.com/", "org/repo");
expect(mockFetch).toHaveBeenCalledWith(
"https://api.example.com/ci/repos/org%2Frepo/config",
expect.any(Object),
);
});

test("encodes repo name in URL", async () => {
const mockFetch = vi.spyOn(globalThis, "fetch").mockResolvedValue(
Response.json(DEFAULT_CONFIG, { status: 200 }),
);

await fetchAnalyzerConfig("https://api.example.com", "org/repo with spaces");
expect(mockFetch).toHaveBeenCalledWith(
"https://api.example.com/ci/repos/org%2Frepo%20with%20spaces/config",
expect.any(Object),
);
});

test("passes through partial response with missing optional fields", async () => {
const partial = {
minimumCost: 50,
regressionThreshold: 0.1,
ignoredQueryHashes: [],
// lastSeenQueries intentionally omitted
};
vi.spyOn(globalThis, "fetch").mockResolvedValue(
Response.json(partial, { status: 200 }),
);

const result = await fetchAnalyzerConfig("https://api.example.com", "my/repo");
expect(result.minimumCost).toBe(50);
expect(result.regressionThreshold).toBe(0.1);
expect(result.ignoredQueryHashes).toEqual([]);
expect(result.lastSeenQueries).toBeUndefined();
});

test("preserves lastSeenQueries when present in response", async () => {
const config = {
minimumCost: 0,
regressionThreshold: 0,
ignoredQueryHashes: [],
lastSeenQueries: ["q1", "q2"],
};
vi.spyOn(globalThis, "fetch").mockResolvedValue(
Response.json(config, { status: 200 }),
);

const result = await fetchAnalyzerConfig("https://api.example.com", "my/repo");
expect(result.lastSeenQueries).toEqual(["q1", "q2"]);
});
38 changes: 38 additions & 0 deletions src/sanitize.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { test, expect, vi } from "vitest";

test("returns original URL when HOSTED is false", async () => {
vi.stubEnv("HOSTED", "false");
vi.resetModules();
const { sanitizePostgresUrl } = await import("./sanitize.ts");
const url = "postgres://user:pass@host:5432/db";
expect(sanitizePostgresUrl(url)).toBe(url);
});

test("returns hashed URL when HOSTED is true", async () => {
vi.stubEnv("HOSTED", "true");
vi.resetModules();
const { sanitizePostgresUrl } = await import("./sanitize.ts");
const url = "postgres://user:pass@host:5432/db";
const result = sanitizePostgresUrl(url);
expect(result).toMatch(/^omitted__[a-f0-9]{8}$/);
expect(result).not.toContain("user");
expect(result).not.toContain("pass");
expect(result).not.toContain("host");
});

test("same input produces same hash", async () => {
vi.stubEnv("HOSTED", "true");
vi.resetModules();
const { sanitizePostgresUrl } = await import("./sanitize.ts");
const url = "postgres://user:pass@host:5432/db";
expect(sanitizePostgresUrl(url)).toBe(sanitizePostgresUrl(url));
});

test("different inputs produce different hashes", async () => {
vi.stubEnv("HOSTED", "true");
vi.resetModules();
const { sanitizePostgresUrl } = await import("./sanitize.ts");
const a = sanitizePostgresUrl("postgres://a@host/db1");
const b = sanitizePostgresUrl("postgres://b@host/db2");
expect(a).not.toBe(b);
});
75 changes: 75 additions & 0 deletions src/sql/json.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { test, expect } from "vitest";
import { preprocessEncodedJson } from "./json.ts";

test("returns parsed JSON object from clean input", () => {
const input = '{"key": "value"}';
const result = preprocessEncodedJson(input);
expect(result).toBe('{"key": "value"}');
});

test("skips leading whitespace before opening brace", () => {
const input = ' \t {"key": "value"}';
const result = preprocessEncodedJson(input);
expect(result).toBe('{"key": "value"}');
});

test("skips leading escaped newlines before opening brace", () => {
const input = '\\n\\n{"key": "value"}';
const result = preprocessEncodedJson(input);
expect(result).toBe('{"key": "value"}');
});

test("returns undefined for non-JSON input", () => {
expect(preprocessEncodedJson("hello world")).toBeUndefined();
});

test("returns undefined for empty string", () => {
expect(preprocessEncodedJson("")).toBeUndefined();
});

test("round-trips \\n: unescapes then re-escapes newlines", () => {
// \\n (literal backslash-n) → real newline → \\n (control char handler re-escapes)
const input = '{"key":\\n"value"}';
const result = preprocessEncodedJson(input);
expect(result).toBe('{"key":\\n"value"}');
});

test("strips control characters but preserves escaped \\n, \\r, \\t", () => {
// A real newline (from unescaping) should be preserved as \\n in the output
const input = '{"key": "val\\nue"}';
const result = preprocessEncodedJson(input);
// \\n becomes real \n, then the control char replacement turns \n back to \\n
expect(result).toBe('{"key": "val\\nue"}');
});

test("strips NUL and other low control characters", () => {
const input = '{"key": "val\x01\x02ue"}';
const result = preprocessEncodedJson(input);
expect(result).toBe('{"key": "value"}');
});

test("handles mixed leading whitespace and escaped newlines", () => {
const input = ' \\n \\n {"data": 1}';
const result = preprocessEncodedJson(input);
expect(result).toBe('{"data": 1}');
});

test("preserves \\r as escaped sequence after unescaping", () => {
const input = '{"key": "val\rue"}';
const result = preprocessEncodedJson(input);
expect(result).toBe('{"key": "val\\rue"}');
});

test("preserves \\t as escaped sequence after unescaping", () => {
const input = '{"key": "val\tue"}';
const result = preprocessEncodedJson(input);
expect(result).toBe('{"key": "val\\tue"}');
});

test("skips non-whitespace characters before opening brace", () => {
expect(preprocessEncodedJson('abc{"key": 1}')).toBe('{"key": 1}');
});

test("returns undefined for whitespace-only input", () => {
expect(preprocessEncodedJson(" \\n\\n ")).toBeUndefined();
});
Loading
Loading