import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import express from 'express'; import cors from 'cors'; import cookieParser from 'cookie-parser'; import crypto from 'crypto'; import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const TEST_DATA_DIR = join(__dirname, '..', 'data-test'); const TEST_DATA_FILE = join(TEST_DATA_DIR, 'access-requests.json'); function createTestApp() { mkdirSync(TEST_DATA_DIR, { recursive: true }); writeFileSync(TEST_DATA_FILE, '[]', 'utf-8'); const sessions = new Map(); const ADMIN_USER = 'admin'; const ADMIN_PASSWORD = 'testpass'; const app = express(); app.use(cors()); app.use(express.json()); app.use(cookieParser('test-secret')); function createSession(user) { const token = crypto.randomBytes(32).toString('hex'); sessions.set(token, { user, createdAt: Date.now() }); return token; } function getSession(token) { return sessions.get(token) || null; } function requireAuth(req, res, next) { const token = req.cookies?.kiq_session; if (!token) return res.status(401).json({ error: 'Nicht authentifiziert' }); const session = getSession(token); if (!session) return res.status(401).json({ error: 'Sitzung abgelaufen' }); req.user = session.user; next(); } app.get('/api/health', (_req, res) => { res.json({ status: 'ok', time: new Date().toISOString() }); }); app.post('/api/login', (req, res) => { const { username, password } = req.body; if (username === ADMIN_USER && password === ADMIN_PASSWORD) { const token = createSession({ username, role: 'admin' }); res.cookie('kiq_session', token, { httpOnly: true, sameSite: 'strict' }); return res.json({ user: { username, role: 'admin' } }); } return res.status(401).json({ error: 'Ungültige Anmeldedaten' }); }); app.post('/api/logout', (req, res) => { const token = req.cookies?.kiq_session; if (token) sessions.delete(token); res.clearCookie('kiq_session'); res.json({ ok: true }); }); app.get('/api/me', (req, res) => { const token = req.cookies?.kiq_session; if (!token) return res.json({ user: null }); const session = getSession(token); if (!session) return res.json({ user: null }); res.json({ user: session.user }); }); app.post('/api/access-requests', (req, res) => { const { user_data } = req.body; if (!user_data || !user_data.name || !user_data.email || !user_data.company) { return res.status(400).json({ error: 'Pflichtfelder fehlen' }); } const requests = JSON.parse(readFileSync(TEST_DATA_FILE, 'utf-8')); const newRequest = { id: crypto.randomUUID(), user_data, status: 'pending', created_at: new Date().toISOString(), }; requests.push(newRequest); writeFileSync(TEST_DATA_FILE, JSON.stringify(requests, null, 2), 'utf-8'); res.status(201).json({ ok: true, id: newRequest.id }); }); app.get('/api/access-requests', requireAuth, (_req, res) => { const requests = JSON.parse(readFileSync(TEST_DATA_FILE, 'utf-8')); requests.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); res.json(requests); }); app.put('/api/access-requests/:id', requireAuth, (req, res) => { const { id } = req.params; const { status } = req.body; if (!['pending', 'approved', 'rejected'].includes(status)) { return res.status(400).json({ error: 'Ungültiger Status' }); } const requests = JSON.parse(readFileSync(TEST_DATA_FILE, 'utf-8')); const idx = requests.findIndex(r => r.id === id); if (idx === -1) return res.status(404).json({ error: 'Anfrage nicht gefunden' }); requests[idx].status = status; writeFileSync(TEST_DATA_FILE, JSON.stringify(requests, null, 2), 'utf-8'); res.json({ ok: true }); }); return app; } // --- Test Setup --- let server; let baseUrl; beforeAll(async () => { const app = createTestApp(); await new Promise((resolve) => { server = app.listen(0, () => { const addr = server.address(); baseUrl = `http://localhost:${addr.port}`; resolve(); }); }); }); afterAll(() => { if (server) server.close(); if (existsSync(TEST_DATA_DIR)) rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); beforeEach(() => { writeFileSync(TEST_DATA_FILE, '[]', 'utf-8'); }); // --- Tests --- describe('KIQ Server API', () => { describe('GET /api/health', () => { it('returns ok status', async () => { const res = await fetch(`${baseUrl}/api/health`); expect(res.status).toBe(200); const data = await res.json(); expect(data.status).toBe('ok'); expect(data.time).toBeTruthy(); }); }); describe('POST /api/login', () => { it('accepts valid credentials', async () => { const res = await fetch(`${baseUrl}/api/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'admin', password: 'testpass' }), }); expect(res.status).toBe(200); const data = await res.json(); expect(data.user.username).toBe('admin'); expect(data.user.role).toBe('admin'); }); it('rejects invalid credentials', async () => { const res = await fetch(`${baseUrl}/api/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'admin', password: 'wrong' }), }); expect(res.status).toBe(401); }); it('sets session cookie on success', async () => { const res = await fetch(`${baseUrl}/api/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'admin', password: 'testpass' }), }); const setCookie = res.headers.get('set-cookie'); expect(setCookie).toContain('kiq_session'); }); }); describe('GET /api/me', () => { it('returns null user without session', async () => { const res = await fetch(`${baseUrl}/api/me`); expect(res.status).toBe(200); const data = await res.json(); expect(data.user).toBeNull(); }); }); describe('POST /api/access-requests', () => { it('creates a new access request', async () => { const res = await fetch(`${baseUrl}/api/access-requests`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_data: { name: 'Max Mustermann', email: 'max@example.com', company: 'Test GmbH', role: 'Developer', purpose: 'Testing', }, }), }); expect(res.status).toBe(201); const data = await res.json(); expect(data.ok).toBe(true); expect(data.id).toBeTruthy(); }); it('rejects when name is missing', async () => { const res = await fetch(`${baseUrl}/api/access-requests`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_data: { email: 'x@x.com', company: 'X' } }), }); expect(res.status).toBe(400); }); it('rejects when email is missing', async () => { const res = await fetch(`${baseUrl}/api/access-requests`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_data: { name: 'Max', company: 'X' } }), }); expect(res.status).toBe(400); }); it('rejects when company is missing', async () => { const res = await fetch(`${baseUrl}/api/access-requests`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_data: { name: 'Max', email: 'x@x.com' } }), }); expect(res.status).toBe(400); }); it('rejects empty body', async () => { const res = await fetch(`${baseUrl}/api/access-requests`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }); expect(res.status).toBe(400); }); }); describe('GET /api/access-requests (admin)', () => { it('rejects without auth', async () => { const res = await fetch(`${baseUrl}/api/access-requests`); expect(res.status).toBe(401); }); }); describe('PUT /api/access-requests/:id (admin)', () => { it('rejects without auth', async () => { const res = await fetch(`${baseUrl}/api/access-requests/fake-id`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'approved' }), }); expect(res.status).toBe(401); }); }); });