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:
2026-07-10 18:43:41 +02:00
parent 6ae0c91539
commit 18f6e2732e
5 changed files with 1555 additions and 2 deletions
+1141 -1
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -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);
});
});
});
+10
View File
@@ -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()
})
})