import { afterAll, describe, expect, test } from "bun:test"; import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { configPath, DEFAULT_URL, deleteStoredKey, type Env, readConfig, resolveApiKey, resolveApiKeyState, resolveUrl, writeConfig, } from "./config"; // Every test runs against a throwaway HOME/XDG dir — the real one is never touched. const root = mkdtempSync(join(tmpdir(), "taleseal-config-")); afterAll(() => rmSync(root, { recursive: true, force: true })); let caseId = 0; function freshHome(): string { caseId += 1; const home = join(root, `case-${caseId}`); mkdirSync(home, { recursive: true }); return home; } function freshEnv(extra: Env = {}): Env { return { HOME: freshHome(), ...extra }; } describe("configPath", () => { test("defaults to ~/.config/taleseal/config.json", () => { const home = freshHome(); expect(configPath({ HOME: home })).toBe(join(home, ".config", "taleseal", "config.json")); }); test("respects XDG_CONFIG_HOME when set", () => { const home = freshHome(); const xdg = join(home, "xdg"); expect(configPath({ HOME: home, XDG_CONFIG_HOME: xdg })).toBe(join(xdg, "taleseal", "config.json")); }); test("ignores a blank XDG_CONFIG_HOME", () => { const home = freshHome(); expect(configPath({ HOME: home, XDG_CONFIG_HOME: " " })).toBe(join(home, ".config", "taleseal", "config.json")); }); }); describe("readConfig / writeConfig", () => { test("reads back what was written, creating directories as needed", () => { const env = freshEnv(); const path = writeConfig({ apiKey: "tk_abc", url: "https://example.test" }, env); expect(path).toBe(configPath(env)); expect(readConfig(env)).toEqual({ apiKey: "tk_abc", url: "https://example.test" }); }); test("the file is chmod 0600, including on overwrite", () => { const env = freshEnv(); const path = writeConfig({ apiKey: "tk_one" }, env); expect(statSync(path).mode & 0o777).toBe(0o600); writeConfig({ apiKey: "tk_two" }, env); expect(statSync(path).mode & 0o777).toBe(0o600); expect(readConfig(env)).toEqual({ apiKey: "tk_two" }); }); test("a missing file reads as an empty config", () => { expect(readConfig(freshEnv())).toEqual({}); }); test("malformed JSON and non-string fields read as an empty config", () => { const env = freshEnv(); const path = configPath(env); mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, "not json at all"); expect(readConfig(env)).toEqual({}); writeFileSync(path, JSON.stringify({ apiKey: 42, url: ["nope"] })); expect(readConfig(env)).toEqual({}); }); }); describe("resolveApiKey", () => { test("TALESEAL_API_KEY env var wins over the stored config", () => { const env = freshEnv({ TALESEAL_API_KEY: "tk_env" }); writeConfig({ apiKey: "tk_stored" }, env); expect(resolveApiKey(env)).toBe("tk_env"); }); test("falls back to the stored config, then undefined", () => { const env = freshEnv(); expect(resolveApiKey(env)).toBeUndefined(); writeConfig({ apiKey: "tk_stored" }, env); expect(resolveApiKey(env)).toBe("tk_stored"); }); test("a blank env var does not shadow the stored key", () => { const env = freshEnv({ TALESEAL_API_KEY: " " }); writeConfig({ apiKey: "tk_stored" }, env); expect(resolveApiKey(env)).toBe("tk_stored"); }); }); describe("resolveApiKeyState", () => { test("a resolvable key is state key — env wins over stored, same order as resolveApiKey", () => { const env = freshEnv({ TALESEAL_API_KEY: "tk_env" }); writeConfig({ apiKey: "tk_stored" }, env); expect(resolveApiKeyState(env)).toEqual({ state: "key", key: "tk_env" }); }); test("nothing set anywhere is absent — the only state auto-anonymous may fire from", () => { expect(resolveApiKeyState(freshEnv())).toEqual({ state: "absent" }); }); test("a blank env var with no stored key is empty, never absent", () => { expect(resolveApiKeyState(freshEnv({ TALESEAL_API_KEY: "" }))).toEqual({ state: "empty", source: "env" }); expect(resolveApiKeyState(freshEnv({ TALESEAL_API_KEY: " " }))).toEqual({ state: "empty", source: "env" }); }); test("a blank env var still falls through to a stored key — today's behaviour, pinned", () => { const env = freshEnv({ TALESEAL_API_KEY: "" }); writeConfig({ apiKey: "tk_stored" }, env); expect(resolveApiKeyState(env)).toEqual({ state: "key", key: "tk_stored" }); }); test('a config file carrying apiKey: "" is empty, never absent — readConfig drops it, the state must not', () => { const env = freshEnv(); const path = configPath(env); mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, JSON.stringify({ apiKey: "" })); expect(resolveApiKeyState(env)).toEqual({ state: "empty", source: "config" }); }); }); describe("resolveUrl", () => { test("TALESEAL_URL env var → config url → default", () => { const env = freshEnv(); expect(resolveUrl(env)).toBe(DEFAULT_URL); writeConfig({ url: "https://stored.test" }, env); expect(resolveUrl(env)).toBe("https://stored.test"); expect(resolveUrl({ ...env, TALESEAL_URL: "https://env.test" })).toBe("https://env.test"); }); test("the default is taleseal.com", () => { expect(DEFAULT_URL).toBe("https://taleseal.com"); }); }); describe("deleteStoredKey", () => { test("removes the key and the file when nothing else is stored", () => { const env = freshEnv(); const path = writeConfig({ apiKey: "tk_gone" }, env); expect(deleteStoredKey(env)).toBe(true); expect(() => readFileSync(path, "utf8")).toThrow(); }); test("keeps a stored url when removing the key", () => { const env = freshEnv(); writeConfig({ apiKey: "tk_gone", url: "https://stored.test" }, env); expect(deleteStoredKey(env)).toBe(true); expect(readConfig(env)).toEqual({ url: "https://stored.test" }); }); test("returns false when no key is stored", () => { expect(deleteStoredKey(freshEnv())).toBe(false); }); });