161 lines
5.9 KiB
TypeScript
161 lines
5.9 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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);
|
|||
|
|
});
|
|||
|
|
});
|