- 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();
 | 
						|
			}
 | 
						|
		}
 | 
						|
	});
 | 
						|
});
 |