- 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`.
156 lines
4.3 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
});
|
|
});
|