/** * App-spezifische Sicherheitstests (Leistungsbilanz): die Rollen-Grenze admin/user und die * Pflicht zum Passwortwechsel – über echte HTTP-Requests gegen createApp(). * Ergänzt baseline.test.ts, das nur Login-Pflicht und Objekt-Zugriff kennt. */ import assert from "node:assert/strict"; import type { AddressInfo } from "node:net"; import { after, before, describe, it } from "node:test"; import config, { START_PASSWORD, WRITE_HEADER, signIn } from "./security.config.ts"; type Headers = Record; let server: import("node:http").Server; let baseUrl = ""; let admin: Headers = {}; let user: Headers = {}; let userId = ""; async function call(method: string, path: string, headers: Headers, body?: unknown) { return fetch(baseUrl + path, { method, headers: { ...headers, "content-type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), }); } before(async () => { const app = await config.createApp(); const { userService } = await import("../../src/server/composition/auth.js"); await userService.createUser({ username: "chefin-admin", name: "chefin-admin", role: "admin", initialPassword: START_PASSWORD, }); await userService.createUser({ username: "neu-ohne-wechsel", name: "neu-ohne-wechsel", role: "user", initialPassword: START_PASSWORD, }); server = app.listen(0, "127.0.0.1"); await new Promise((resolve) => server.once("listening", resolve)); baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; admin = await signIn(baseUrl, "chefin-admin"); user = await signIn(baseUrl, "planer-a"); const list = (await (await call("GET", "/api/users", admin)).json()) as { id: string; username: string }[]; userId = list.find((entry) => entry.username === "planer-b")!.id; }); after(() => new Promise((resolve) => server.close(() => resolve()))); describe("Rollen: Benutzerverwaltung nur für admin", () => { it("normaler Nutzer bekommt 403 auf jede /api/users-Route", async () => { const cases: [string, string, unknown?][] = [ ["GET", "/api/users"], ["POST", "/api/users", { username: "eingeschleust", name: "x", role: "admin", initialPassword: "lang-genug-123" }], ["PATCH", `/api/users/${userId}`, { role: "admin" }], ["POST", `/api/users/${userId}/reset-password`, { newPassword: "lang-genug-123" }], ]; const failures: string[] = []; for (const [method, path, body] of cases) { const res = await call(method, path, user, body); if (res.status !== 403) failures.push(`${method} ${path} als user -> ${res.status}`); } assert.deepEqual(failures, [], failures.join("\n")); }); it("admin erreicht die Benutzerverwaltung (Gegenprobe)", async () => { const res = await call("GET", "/api/users", admin); assert.equal(res.status, 200); }); }); describe("Startpasswort", () => { it("ohne Passwortwechsel ist außer Konto-Endpunkten alles gesperrt", async () => { const login = await call("POST", "/api/auth/login", WRITE_HEADER, { username: "neu-ohne-wechsel", password: START_PASSWORD, }); assert.equal(login.status, 200); const cookie = login.headers.getSetCookie().find((c) => c.startsWith("lb_session="))!.split(";")[0]; const headers = { ...WRITE_HEADER, cookie }; assert.equal((await call("GET", "/api/projects", headers)).status, 403); assert.equal((await call("POST", "/api/projects", headers, { name: "x" })).status, 403); assert.equal((await call("GET", "/api/auth/me", headers)).status, 200, "Gegenprobe: /auth/me bleibt erreichbar"); }); });