ionian/packages/server/tests/settings-router.integration.test.ts
monoid d28c255d21 feat: add app configuration management with Kysely integration
- Implemented `getAppConfig` and `upsertAppConfig` functions in `config.ts` for managing application settings in the database.
- Updated `mod.ts` to export the new configuration functions.
- Refactored `ComicConfig.ts` to load and update comic watch paths using the new configuration functions.
- Modified `comic_watcher.ts` to accept paths as parameters for creating watchers.
- Created a new settings router in `settings.ts` for managing application settings via HTTP requests.
- Integrated the settings router into the main server in `server.ts`.
- Updated the settings management to use the new database-backed configuration.
- Removed legacy configuration management code from `configRW.ts`.
- Added integration tests for the settings router and error handling.
- Updated `vitest` configuration for testing.
- Cleaned up unused type definitions in `pnpm-lock.yaml`.
2025-09-30 23:15:20 +09:00

156 lines
4.3 KiB
TypeScript

import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { Elysia } from "elysia";
import { Kysely, SqliteDialect } from "kysely";
import SqliteDatabase from "better-sqlite3";
import type { db } from "dbtype";
import { createSettingsRouter } from "../src/route/settings.ts";
import { error_handler } from "../src/route/error_handler.ts";
import { get_setting, refreshSetting } from "../src/SettingConfig.ts";
import { Permission } from "../src/permission/permission.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 user = { username, permission: [] as string[] };
return new Elysia({ name: `settings-test-${username}` })
.state("user", user)
.derive(() => ({ user }))
.onError((context) =>
error_handler({
code: typeof context.code === "number" ? String(context.code) : context.code,
error: normalizeError(context.error),
set: context.set,
}),
)
.use(createSettingsRouter(database));
};
it("rejects access for non-admin users", async () => {
const app = createTestApp("guest");
try {
const response = await app.handle(new Request("http://localhost/settings"));
expect(response.status).toBe(403);
} finally {
if (app.server) {
await app.stop();
}
}
});
it("returns current configuration for admin", async () => {
const app = createTestApp("admin");
try {
const response = await app.handle(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(Object.values(Permission)));
} finally {
if (app.server) {
await app.stop();
}
}
});
it("updates persisted settings and returns the new state", async () => {
const app = createTestApp("admin");
try {
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.handle(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.handle(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"],
});
} finally {
if (app.server) {
await app.stop();
}
}
});
});