ionian/packages/server/tests/settings-router.integration.test.ts

150 lines
4.1 KiB
TypeScript

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<typeof SqliteDatabase>;
let database: Kysely<db.DB>;
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<db.DB>({ 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<AppEnv>();
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"],
});
});
});