Rollen Chunk 6: Team-Tab mit Matrix-Kalender und Genehmigungen
Docker Image / test (push) Canceled after 0s
Docker Image / image (push) Canceled after 0s

This commit is contained in:
loki committed 2026-10-11 20:19:05 +02:00
1 parent f35afca3f5
commit 8fe7700256
1 file changed
+228
@@ -0,0 +1,228 @@
import "../../../test-helpers";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { getAuth } from "@/lib/auth";
import { getDb, initializeDatabase } from "@/lib/db";
import { POST } from "./route";
import { saveVacationRequest, saveYearSettings } from "@/lib/repository";
beforeEach(() => {
process.env.AUTH_MODE = "password";
process.env.BETTER_AUTH_SECRET = "test-secret-mindestens-32-zeichen-lang!";
});
afterEach(() => {
delete process.env.AUTH_MODE;
delete process.env.BETTER_AUTH_SECRET;
});
async function signUp(name: string, email: string): Promise<void> {
const { POST: authPost } = await import("../../../auth/[...all]/route");
const response = await authPost(
new Request("http://localhost/api/auth/sign-up/email", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name, email, password: "geheim-geheim!" }),
}),
);
if (response.status !== 200) {
throw new Error(`Registrierung: ${response.status}`);
}
}
async function cookieFor(email: string): Promise<string> {
const auth = getAuth();
if (!auth) {
throw new Error("Auth erwartet");
}
const response = await auth.api.signInEmail({
body: { email, password: "geheim-geheim!" },
asResponse: true,
});
const setCookie = response.headers
.getSetCookie()
.find((entry) => entry.startsWith("better-auth.session_token="));
if (!setCookie) {
throw new Error("Sitzungscookie erwartet");
}
return setCookie.split(";")[0];
}
async function adminPost(
path: string,
cookie: string,
body: unknown,
): Promise<Response> {
const { POST: authPost } = await import("../../../auth/[...all]/route");
return authPost(
new Request(`http://localhost${path}`, {
method: "POST",
headers: { "content-type": "application/json", cookie },
body: JSON.stringify(body),
}),
);
}
async function adminGet(path: string, cookie: string): Promise<Response> {
const { GET: authGet } = await import("../../../auth/[...all]/route");
return authGet(new Request(`http://localhost${path}`, { headers: { cookie } }));
}
async function setup(): Promise<{ leiter: string; benutzer: string }> {
await signUp("Admina", "admina@example.org");
const admin = await cookieFor("admina@example.org");
await adminPost("/api/auth/admin/create-user", admin, {
name: "Leitung",
email: "leitung@example.org",
password: "geheim-geheim!",
role: "benutzer",
});
const listed = (await (
await adminGet("/api/auth/admin/list-users?limit=10", admin)
).json()) as { users?: { id: string; email: string }[] };
const leiterId = listed.users?.find(
(user) => user.email === "leitung@example.org",
)?.id;
if (!leiterId) {
throw new Error("Leiter-ID erwartet");
}
await adminPost("/api/auth/admin/set-role", admin, {
userId: leiterId,
role: "teamleiter",
});
await adminPost("/api/auth/admin/create-user", admin, {
name: "Benutzer",
email: "benutzer@example.org",
password: "geheim-geheim!",
role: "benutzer",
});
return {
leiter: await cookieFor("leitung@example.org"),
benutzer: await cookieFor("benutzer@example.org"),
};
}
function postJsonWithCookie(url: string, cookie: string, body: unknown): Request {
return new Request(url, {
method: "POST",
headers: { "content-type": "application/json", cookie },
body: JSON.stringify(body),
});
}
describe("POST /api/vacation/[id]/status", () => {
test("Teamleiter bestätigt eine Anfrage", async () => {
const { leiter, benutzer } = await setup();
const db = getDb();
initializeDatabase(db);
const benutzerId = db
.prepare('SELECT id FROM "user" WHERE email = ?')
.get("benutzer@example.org") as { id: string };
const saved = saveVacationRequest(db, benutzerId.id, {
startDate: "2026-07-06",
endDate: "2026-07-06",
dayFraction: "full",
status: "beantragt",
days: [{ date: "2026-07-06", units: 2 }],
});
const response = await POST(
postJsonWithCookie(
`http://localhost/api/vacation/${saved.id}/status`,
leiter,
{ action: "genehmigen" },
),
{ params: Promise.resolve({ id: String(saved.id) }) },
);
expect(response.status).toBe(200);
expect(((await response.json()) as { status: string }).status).toBe(
"bestätigt",
);
void benutzer;
});
test("Benutzer erhalten 403, Unbekannte Anfrage 404, falsche Aktion 400", async () => {
const { leiter, benutzer } = await setup();
const db = getDb();
initializeDatabase(db);
const benutzerId = db
.prepare('SELECT id FROM "user" WHERE email = ?')
.get("benutzer@example.org") as { id: string };
const saved = saveVacationRequest(db, benutzerId.id, {
startDate: "2026-07-06",
endDate: "2026-07-06",
dayFraction: "full",
status: "beantragt",
days: [{ date: "2026-07-06", units: 2 }],
});
const denied = await POST(
postJsonWithCookie(
`http://localhost/api/vacation/${saved.id}/status`,
benutzer,
{ action: "genehmigen" },
),
{ params: Promise.resolve({ id: String(saved.id) }) },
);
expect(denied.status).toBe(403);
const wrongAction = await POST(
postJsonWithCookie(
`http://localhost/api/vacation/${saved.id}/status`,
leiter,
{ action: "verschieben" },
),
{ params: Promise.resolve({ id: String(saved.id) }) },
);
expect(wrongAction.status).toBe(400);
const missing = await POST(
postJsonWithCookie("http://localhost/api/vacation/999/status", leiter, {
action: "genehmigen",
}),
{ params: Promise.resolve({ id: "999" }) },
);
expect(missing.status).toBe(404);
});
test("ohne Bestätigung würde der Anspruch überzogen: 422", async () => {
const { leiter, benutzer } = await setup();
const db = getDb();
initializeDatabase(db);
const benutzerId = db
.prepare('SELECT id FROM "user" WHERE email = ?')
.get("benutzer@example.org") as { id: string };
saveVacationRequest(db, benutzerId.id, {
startDate: "2026-01-05",
endDate: "2026-01-05",
dayFraction: "full",
status: "beantragt",
days: [{ date: "2026-01-05", units: 2 }],
});
const second = saveVacationRequest(db, benutzerId.id, {
startDate: "2026-01-06",
endDate: "2026-01-06",
dayFraction: "full",
status: "beantragt",
days: [{ date: "2026-01-06", units: 2 }],
});
saveVacationRequest(db, benutzerId.id, {
startDate: "2026-01-07",
endDate: "2026-01-07",
dayFraction: "full",
status: "beantragt",
days: [{ date: "2026-01-07", units: 2 }],
});
// Kontingent absenken:
saveYearSettings(db, benutzerId.id, 2026, {
entitlementUnits: 3,
carryoverUnits: 0,
});
const response = await POST(
postJsonWithCookie(
`http://localhost/api/vacation/${second.id}/status`,
leiter,
{ action: "genehmigen" },
),
{ params: Promise.resolve({ id: String(second.id) }) },
);
expect(response.status).toBe(422);
expect((await response.json()).error.code).toBe("vacationExceedsBalance");
});
});