/** * 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; let admin: Record; // ── 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); }); });