2026-06-10 20:41:17 +09:00
|
|
|
import fs from "fs"
|
|
|
|
|
import os from "os"
|
|
|
|
|
import path from "path"
|
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
|
|
|
import {
|
|
|
|
|
_resetForTests,
|
|
|
|
|
applyToEnv,
|
|
|
|
|
getEnvFallback,
|
|
|
|
|
getValueSource,
|
|
|
|
|
isSettingsWritable,
|
|
|
|
|
loadSettings,
|
|
|
|
|
saveSettings,
|
|
|
|
|
} from "@/lib/admin/settings"
|
|
|
|
|
|
|
|
|
|
let tmpDir: string
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "admin-settings-"))
|
|
|
|
|
process.env.SETTINGS_FILE = path.join(tmpDir, "settings.json")
|
|
|
|
|
_resetForTests()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
_resetForTests()
|
|
|
|
|
delete process.env.SETTINGS_FILE
|
|
|
|
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
|
|
|
delete process.env.TEST_ADMIN_VAR
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("loadSettings", () => {
|
|
|
|
|
it("returns empty object when file does not exist", () => {
|
|
|
|
|
expect(loadSettings()).toEqual({})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("reads values from the settings file", () => {
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
process.env.SETTINGS_FILE!,
|
|
|
|
|
JSON.stringify({ version: 1, values: { TEST_ADMIN_VAR: "abc" } }),
|
|
|
|
|
)
|
|
|
|
|
expect(loadSettings()).toEqual({ TEST_ADMIN_VAR: "abc" })
|
|
|
|
|
})
|
2026-06-12 11:34:11 +09:00
|
|
|
|
|
|
|
|
it("drops non-string values from a corrupted file", () => {
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
process.env.SETTINGS_FILE!,
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
version: 1,
|
|
|
|
|
values: {
|
|
|
|
|
GOOD: "ok",
|
|
|
|
|
NUM: 5,
|
|
|
|
|
OBJ: { nested: true },
|
|
|
|
|
ARR: [1, 2],
|
|
|
|
|
NULL: null,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
expect(loadSettings()).toEqual({ GOOD: "ok" })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns empty object when values is null or an array", () => {
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
process.env.SETTINGS_FILE!,
|
|
|
|
|
JSON.stringify({ version: 1, values: null }),
|
|
|
|
|
)
|
|
|
|
|
expect(loadSettings()).toEqual({})
|
|
|
|
|
})
|
2026-06-10 20:41:17 +09:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("applyToEnv / saveSettings", () => {
|
|
|
|
|
it("overlays file values onto process.env", () => {
|
|
|
|
|
saveSettings({ TEST_ADMIN_VAR: "from-file" })
|
|
|
|
|
expect(process.env.TEST_ADMIN_VAR).toBe("from-file")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("file value wins over pre-existing env value", () => {
|
|
|
|
|
process.env.TEST_ADMIN_VAR = "from-env"
|
|
|
|
|
saveSettings({ TEST_ADMIN_VAR: "from-file" })
|
|
|
|
|
expect(process.env.TEST_ADMIN_VAR).toBe("from-file")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("deleting a key restores the original env value", () => {
|
|
|
|
|
process.env.TEST_ADMIN_VAR = "from-env"
|
|
|
|
|
saveSettings({ TEST_ADMIN_VAR: "from-file" })
|
|
|
|
|
saveSettings({ TEST_ADMIN_VAR: null })
|
|
|
|
|
expect(process.env.TEST_ADMIN_VAR).toBe("from-env")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("deleting a key unsets env when there was no original value", () => {
|
|
|
|
|
saveSettings({ TEST_ADMIN_VAR: "from-file" })
|
|
|
|
|
saveSettings({ TEST_ADMIN_VAR: null })
|
|
|
|
|
expect(process.env.TEST_ADMIN_VAR).toBeUndefined()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("persists across cache reset (file round-trip)", () => {
|
|
|
|
|
saveSettings({ TEST_ADMIN_VAR: "persisted" })
|
|
|
|
|
_resetForTests()
|
|
|
|
|
applyToEnv()
|
|
|
|
|
expect(process.env.TEST_ADMIN_VAR).toBe("persisted")
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("getValueSource / getEnvFallback", () => {
|
|
|
|
|
it("reports file source when key is in settings", () => {
|
|
|
|
|
saveSettings({ TEST_ADMIN_VAR: "x" })
|
|
|
|
|
expect(getValueSource("TEST_ADMIN_VAR")).toBe("file")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("reports env source when only env is set", () => {
|
|
|
|
|
process.env.TEST_ADMIN_VAR = "from-env"
|
|
|
|
|
applyToEnv()
|
|
|
|
|
expect(getValueSource("TEST_ADMIN_VAR")).toBe("env")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("reports default when neither is set", () => {
|
|
|
|
|
expect(getValueSource("TEST_ADMIN_VAR")).toBe("default")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns the shadowed env value as fallback", () => {
|
|
|
|
|
process.env.TEST_ADMIN_VAR = "from-env"
|
|
|
|
|
saveSettings({ TEST_ADMIN_VAR: "from-file" })
|
|
|
|
|
expect(getEnvFallback("TEST_ADMIN_VAR")).toBe("from-env")
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("isSettingsWritable", () => {
|
|
|
|
|
it("returns true for a writable temp dir", () => {
|
|
|
|
|
expect(isSettingsWritable()).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns false for an unwritable path", () => {
|
|
|
|
|
_resetForTests()
|
|
|
|
|
process.env.SETTINGS_FILE = "/nonexistent-root-dir/settings.json"
|
|
|
|
|
expect(isSettingsWritable()).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("settings file on disk", () => {
|
|
|
|
|
it("writes valid JSON with restrictive permissions", () => {
|
|
|
|
|
saveSettings({ TEST_ADMIN_VAR: "secret" })
|
|
|
|
|
const filePath = process.env.SETTINGS_FILE!
|
|
|
|
|
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"))
|
|
|
|
|
expect(parsed).toEqual({
|
|
|
|
|
version: 1,
|
|
|
|
|
values: { TEST_ADMIN_VAR: "secret" },
|
|
|
|
|
})
|
|
|
|
|
const mode = fs.statSync(filePath).mode & 0o777
|
|
|
|
|
expect(mode).toBe(0o600)
|
|
|
|
|
})
|
|
|
|
|
})
|