Files
web-cad/backend/tests/libraryAdminGuard.test.ts
T

161 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Task H3 Rollen: library-admin (global write) vs. user (read-only).
*
* requireLibraryAdmin schützt alle schreibenden Global-Library-Routen:
* POST/PATCH/DELETE auf folders+blocks, import, import-zip. Lesen
* (GET, export) bleibt für alle authentifizierten Nutzer offen.
* library-admin ist an die bestehende admin-Rolle angebunden.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import type { FastifyInstance } from 'fastify';
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
import { createServer } from '../src/server.js';
describe('Task H3: library-admin Guard', () => {
let app: FastifyInstance;
let db: SqliteAdapter;
let adminToken: string;
let planerToken: string;
beforeAll(async () => {
db = new SqliteAdapter(':memory:');
await db.init();
app = await createServer({ db, port: 0 });
await app.ready();
const adminReg = await app.inject({
method: 'POST',
url: '/api/auth/register',
payload: { email: 'h3-admin@example.com', password: 'Password123!', name: 'H3 Admin', role: 'admin' },
});
if (adminReg.statusCode !== 201) console.error('ADMIN REG FAILED:', adminReg.statusCode, adminReg.body.slice(0, 200));
adminToken = JSON.parse(adminReg.body).session?.token ?? '';
const planerReg = await app.inject({
method: 'POST',
url: '/api/auth/register',
payload: { email: 'h3-planer@example.com', password: 'Password123!', name: 'H3 Planer', role: 'planer' },
});
if (planerReg.statusCode !== 201) console.error('PLANER REG FAILED:', planerReg.statusCode, planerReg.body.slice(0, 200));
planerToken = JSON.parse(planerReg.body).session?.token ?? '';
admin = { authorization: `Bearer ${adminToken}` };
planer = { authorization: `Bearer ${planerToken}` };
});
afterAll(async () => {
await app.close();
db.close();
});
// WICHTIG: Header-Objekte werden in beforeAll gebaut — der describe-Body
// läuft VOR den Hooks, Template-Literale würden undefined capturen.
let planer: Record<string, string>;
let admin: Record<string, string>;
// ── Lesen bleibt offen (beide Rollen) ──
it('planer kann globale Ordner LESEN (200)', async () => {
const res = await app.inject({ method: 'GET', url: '/api/global-folders', headers: planer });
expect(res.statusCode).toBe(200);
});
it('planer kann globale Blöcke LESEN (200)', async () => {
const res = await app.inject({ method: 'GET', url: '/api/global-blocks', headers: planer });
expect(res.statusCode).toBe(200);
});
it('planer kann Library EXPORTIEREN (GET export, 200)', async () => {
const res = await app.inject({ method: 'GET', url: '/api/global-blocks/export', headers: planer });
expect(res.statusCode).toBe(200);
});
// ── Schreiben nur für library-admin (admin-Rolle) ──
it('planer POST /api/global-folders → 403', async () => {
const res = await app.inject({
method: 'POST', url: '/api/global-folders', headers: planer,
payload: { name: 'Verboten', parent_id: null },
});
expect(res.statusCode).toBe(403);
expect(JSON.parse(res.body).error).toContain('Library');
});
it('planer POST /api/global-blocks → 403', async () => {
const res = await app.inject({
method: 'POST', url: '/api/global-blocks', headers: planer,
payload: { name: 'Verboten', block_data: '[]' },
});
expect(res.statusCode).toBe(403);
});
it('planer PATCH /api/global-folders/:id → 403', async () => {
const res = await app.inject({
method: 'PATCH', url: '/api/global-folders/irgendeine-id', headers: planer,
payload: { name: 'Hack' },
});
expect(res.statusCode).toBe(403);
});
it('planer PATCH /api/global-blocks/:id → 403', async () => {
const res = await app.inject({
method: 'PATCH', url: '/api/global-blocks/irgendeine-id', headers: planer,
payload: { name: 'Hack' },
});
expect(res.statusCode).toBe(403);
});
it('planer DELETE /api/global-folders/:id → 403', async () => {
const res = await app.inject({ method: 'DELETE', url: '/api/global-folders/irgendeine-id', headers: planer });
expect(res.statusCode).toBe(403);
});
it('planer DELETE /api/global-blocks/:id → 403', async () => {
const res = await app.inject({ method: 'DELETE', url: '/api/global-blocks/irgendeine-id', headers: planer });
expect(res.statusCode).toBe(403);
});
it('planer POST /api/global-blocks/import → 403', async () => {
const res = await app.inject({
method: 'POST', url: '/api/global-blocks/import', headers: planer,
payload: { format: 'wcadlib', blocks: [] },
});
expect(res.statusCode).toBe(403);
});
it('planer POST /api/global-blocks/import-zip → 403', async () => {
const res = await app.inject({
method: 'POST', url: '/api/global-blocks/import-zip', headers: { ...planer, 'content-type': 'application/zip' },
payload: Buffer.from('PK'),
});
expect(res.statusCode).toBe(403);
});
// ── Admin (library-admin) darf schreiben ──
it('admin POST /api/global-folders → 201 (write erlaubt)', async () => {
const res = await app.inject({
method: 'POST', url: '/api/global-folders', headers: admin,
payload: { name: 'Admin Set', parent_id: null },
});
expect(res.statusCode).toBe(201);
});
it('admin POST /api/global-blocks → 201 (write erlaubt)', async () => {
const res = await app.inject({
method: 'POST', url: '/api/global-blocks', headers: admin,
payload: { name: 'Admin Block', block_data: '[]', folder_id: null },
});
expect(res.statusCode).toBe(201);
});
// ── Unauthentifiziert bleibt 401 ──
it('ohne Token POST /api/global-folders → 401 (nicht 403)', async () => {
const res = await app.inject({
method: 'POST', url: '/api/global-folders',
payload: { name: 'Anon', parent_id: null },
});
expect(res.statusCode).toBe(401);
});
});