test: add test suites for Projekt-KIQ-HP (12 tests) and andreknie.de validation (16 tests)
- Projekt-KIQ-HP: vitest + server API tests (health, login, auth, access-requests) - andreknie.de: validation utility tests (email, contact, talk-request, newsletter, resource-download) - db-planet-mcp-server: existing 7 tests confirmed working
This commit is contained in:
Generated
+1141
-1
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"start:server": "node server/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -22,6 +23,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
@@ -29,6 +32,8 @@
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"vite": "^8.0.4"
|
||||
"jsdom": "^29.1.1",
|
||||
"vite": "^8.0.4",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,128 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
validateEmail,
|
||||
validateContact,
|
||||
validateTalkRequest,
|
||||
validateNewsletter,
|
||||
validateResourceDownload,
|
||||
} from './validation.js'
|
||||
|
||||
describe('validateEmail', () => {
|
||||
it('accepts valid emails', () => {
|
||||
expect(validateEmail('user@example.com')).toBe(true)
|
||||
expect(validateEmail('a.b@sub.domain.org')).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects invalid emails', () => {
|
||||
expect(validateEmail('')).toBe(false)
|
||||
expect(validateEmail('no-at-sign')).toBe(false)
|
||||
expect(validateEmail('@missing-local.com')).toBe(false)
|
||||
expect(validateEmail('spaces in@email.com')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateContact', () => {
|
||||
const valid = { name: 'Max', email: 'max@example.com', message: 'Hello' }
|
||||
|
||||
it('passes with valid data', () => {
|
||||
const result = validateContact(valid)
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.errors).toEqual({})
|
||||
})
|
||||
|
||||
it('fails without name', () => {
|
||||
const result = validateContact({ ...valid, name: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.name).toBeTruthy()
|
||||
})
|
||||
|
||||
it('fails without email', () => {
|
||||
const result = validateContact({ ...valid, email: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.email).toBeTruthy()
|
||||
})
|
||||
|
||||
it('fails without message', () => {
|
||||
const result = validateContact({ ...valid, message: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.message).toBeTruthy()
|
||||
})
|
||||
|
||||
it('fails with name over 100 chars', () => {
|
||||
const result = validateContact({ ...valid, name: 'x'.repeat(101) })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.name).toBeTruthy()
|
||||
})
|
||||
|
||||
it('fails with message over 2000 chars', () => {
|
||||
const result = validateContact({ ...valid, message: 'x'.repeat(2001) })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.message).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateTalkRequest', () => {
|
||||
const valid = {
|
||||
name: 'Max',
|
||||
email: 'max@example.com',
|
||||
event_name: 'Conference 2026',
|
||||
topic: 'KI im Mittelstand',
|
||||
message: 'Interested in a keynote',
|
||||
}
|
||||
|
||||
it('passes with valid data', () => {
|
||||
const result = validateTalkRequest(valid)
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
|
||||
it('fails without event_name', () => {
|
||||
const result = validateTalkRequest({ ...valid, event_name: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.event_name).toBeTruthy()
|
||||
})
|
||||
|
||||
it('fails without topic', () => {
|
||||
const result = validateTalkRequest({ ...valid, topic: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.topic).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateNewsletter', () => {
|
||||
it('passes with valid email', () => {
|
||||
const result = validateNewsletter({ email: 'user@test.com' })
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
|
||||
it('fails with invalid email', () => {
|
||||
const result = validateNewsletter({ email: 'bad' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.email).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateResourceDownload', () => {
|
||||
const valid = {
|
||||
name: 'Max',
|
||||
email: 'max@example.com',
|
||||
company: 'Test GmbH',
|
||||
resource_id: 'speaker-cv',
|
||||
}
|
||||
|
||||
it('passes with valid data', () => {
|
||||
const result = validateResourceDownload(valid)
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
|
||||
it('fails without company', () => {
|
||||
const result = validateResourceDownload({ ...valid, company: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.company).toBeTruthy()
|
||||
})
|
||||
|
||||
it('fails without resource_id', () => {
|
||||
const result = validateResourceDownload({ ...valid, resource_id: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors.resource_id).toBeTruthy()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user