import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { Hono } from "hono"; import { Kysely, SqliteDialect } from "kysely"; import SqliteDatabase from "better-sqlite3"; import type { db } from "dbtype"; import { createSettingsRouter } from "../src/route/settings.ts"; import { mapErrorToResponse } from "../src/route/error_handler.ts"; import { get_setting, refreshSetting } from "../src/SettingConfig.ts"; import { PERMISSIONS } from "../src/permission/permission.ts"; import type { AppEnv, AuthStore } from "../src/login.ts"; const normalizeError = (error: unknown): Error => { if (error instanceof Error) { return error; } if (typeof error === "string") { return new Error(error); } try { return new Error(JSON.stringify(error)); } catch (_err) { return new Error("Unknown error"); } }; describe("settings router", () => { let sqlite: InstanceType; let database: Kysely; beforeAll(async () => { process.env.SERVER_HOST = "127.0.0.1"; process.env.SERVER_PORT = "3000"; process.env.SERVER_MODE = "development"; process.env.JWT_SECRET_KEY = "test-secret"; sqlite = new SqliteDatabase(":memory:"); const dialect = new SqliteDialect({ database: sqlite }); database = new Kysely({ dialect }); await database.schema .createTable("app_config") .addColumn("key", "text", (col) => col.primaryKey()) .addColumn("value", "text") .execute(); await refreshSetting(database); }); afterAll(async () => { await database.destroy(); sqlite.close(); }); beforeEach(async () => { await database.deleteFrom("app_config").execute(); await refreshSetting(database); }); const createTestApp = (username: string) => { const app = new Hono(); const auth: AuthStore = { user: { username, permission: [] }, refreshed: false, authenticated: true, }; app.use("*", async (c, next) => { c.set("auth", auth); await next(); }); app.onError((err) => { const { status, body } = mapErrorToResponse(normalizeError(err)); return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" }, }); }); app.route("/", createSettingsRouter(database)); return app; }; it("rejects access for non-admin users", async () => { const app = createTestApp("guest"); const response = await app.fetch(new Request("http://localhost/settings")); expect(response.status).toBe(403); }); it("returns current configuration for admin", async () => { const app = createTestApp("admin"); const response = await app.fetch(new Request("http://localhost/settings")); expect(response.status).toBe(200); const payload = await response.json(); const expected = get_setting(); expect(payload).toMatchObject({ persisted: { secure: expected.secure, cli: expected.cli, forbid_remote_admin_login: expected.forbid_remote_admin_login, guest: expected.guest, }, env: { hostname: expected.hostname, port: expected.port, mode: expected.mode, }, }); expect(Array.isArray(payload.permissions)).toBe(true); expect(new Set(payload.permissions)).toEqual(new Set(PERMISSIONS)); }); it("updates persisted settings and returns the new state", async () => { const app = createTestApp("admin"); const request = new Request("http://localhost/settings", { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ secure: false, cli: true, guest: ["QueryContent"], forbid_remote_admin_login: false, }), }); const response = await app.fetch(request); expect(response.status).toBe(200); const payload = await response.json(); expect(payload.persisted).toEqual({ secure: false, cli: true, forbid_remote_admin_login: false, guest: ["QueryContent"], }); // A follow-up GET should reflect the updated values const followUp = await app.fetch(new Request("http://localhost/settings")); expect(followUp.status).toBe(200); const followUpPayload = await followUp.json(); expect(followUpPayload.persisted).toEqual({ secure: false, cli: true, forbid_remote_admin_login: false, guest: ["QueryContent"], }); }); });