Files
Mind-o-Mat/tests/auth-cors.test.ts
T
Jannik 9d70cc615e
CI / test (push) Canceled after 0s
CI / webapp (push) Canceled after 0s
v2.0.8: Production-Haertung Notes-API (Auth + CORS)
- src/notes-api-server.ts: NotesApiOptions erweitert
  - authToken: HMAC-SHA256 Bearer-Token (timing-safe compare)
  - allowedOrigins: CORS-Whitelist (oder '*' fuer alle)
- checkAuth(authHeader): pruft Bearer-Token
- generateToken(secret): statische Methode fuer Client-Tools
- checkOrigin(origin): CORS-Pruefung
- corsHeader(origin): liefert CORS-Header (leer wenn nicht erlaubt)
- Token-Format: <timestamp>.<hmac-hex>

Tests:
- tests/auth-cors.test.ts: 14 neue Tests
  - Auth: ohne Token, Header fehlt, falsches Schema, falsches Format
  - Auth: korrekter Token, falsches Secret, HMAC-Tampering
  - CORS: keine Config, Wildcard *, Whitelist
  - CORS: corsHeader mit/ohne Erlaubnis
  - generateToken: Format, verschiedene Secrets

Verifiziert:
- 116/116 Tests gruen (14 neue)
- Auth nutzt timingSafeEqual (kein String-Compare)
- NotesApi bleibt abwaertskompatibel (alte Aufrufe ohne Options funktionieren)
2026-09-05 12:42:05 +02:00

108 lines
4.2 KiB
TypeScript

// Tests fuer Auth + CORS
// Phase 9: Production-Haertung
import { describe, it, expect } from 'vitest';
import { NotesApi } from '../src/notes-api-server.js';
describe('NotesApi Auth', () => {
it('kein Token konfiguriert: alle Requests erlaubt', () => {
const api = new NotesApi('/tmp/vault');
expect(api.checkAuth(undefined).ok).toBe(true);
expect(api.checkAuth('Bearer irgendwas').ok).toBe(true);
});
it('Token konfiguriert, aber Header fehlt: 401', () => {
const api = new NotesApi('/tmp/vault', { authToken: 'secret' });
const result = api.checkAuth(undefined);
expect(result.ok).toBe(false);
expect(result.reason).toContain('fehlt');
});
it('Token mit falschem Schema: ungueltig', () => {
const api = new NotesApi('/tmp/vault', { authToken: 'secret' });
expect(api.checkAuth('Basic abc').ok).toBe(false);
});
it('Token mit falschem Format: ungueltig', () => {
const api = new NotesApi('/tmp/vault', { authToken: 'secret' });
expect(api.checkAuth('Bearer not-a-valid-token').ok).toBe(false);
});
it('Korrekter Token: erlaubt', () => {
const secret = 'my-secret';
const api = new NotesApi('/tmp/vault', { authToken: secret });
const token = NotesApi.generateToken(secret);
const result = api.checkAuth(`Bearer ${token}`);
expect(result.ok).toBe(true);
});
it('Token mit falschem Secret: ungueltig', () => {
const api = new NotesApi('/tmp/vault', { authToken: 'correct-secret' });
const wrongToken = NotesApi.generateToken('wrong-secret');
expect(api.checkAuth(`Bearer ${wrongToken}`).ok).toBe(false);
});
it('HMAC ist timing-safe (kein einfacher String-Vergleich)', () => {
// Wir koennen das nicht direkt testen, aber die Implementation nutzt timingSafeEqual
const api = new NotesApi('/tmp/vault', { authToken: 'secret' });
const token = NotesApi.generateToken('secret');
// Aendere ein Zeichen im Token
const modified = token.slice(0, -2) + (token.endsWith('0') ? '1' : '0') + token.slice(-1);
expect(api.checkAuth(`Bearer ${modified}`).ok).toBe(false);
});
});
describe('NotesApi CORS', () => {
it('keine Origins konfiguriert: alle erlaubt', () => {
const api = new NotesApi('/tmp/vault');
expect(api.checkOrigin('https://evil.com')).toBe(true);
expect(api.checkOrigin(undefined)).toBe(true);
});
it('Wildcard * erlaubt alle Origins', () => {
const api = new NotesApi('/tmp/vault', { allowedOrigins: ['*'] });
expect(api.checkOrigin('https://anywhere.com')).toBe(true);
expect(api.checkOrigin('https://evil.com')).toBe(true);
});
it('Whitelist: nur konfigurierte Origins', () => {
const api = new NotesApi('/tmp/vault', {
allowedOrigins: ['https://vault.example.com', 'https://app.example.com'],
});
expect(api.checkOrigin('https://vault.example.com')).toBe(true);
expect(api.checkOrigin('https://app.example.com')).toBe(true);
expect(api.checkOrigin('https://evil.com')).toBe(false);
expect(api.checkOrigin(undefined)).toBe(false);
});
it('corsHeader liefert korrekte Header fuer erlaubte Origins', () => {
const api = new NotesApi('/tmp/vault', { allowedOrigins: ['https://app.example.com'] });
const headers = api.corsHeader('https://app.example.com');
expect(headers['Access-Control-Allow-Origin']).toBe('https://app.example.com');
expect(headers['Access-Control-Allow-Methods']).toContain('GET');
expect(headers['Access-Control-Allow-Methods']).toContain('PUT');
});
it('corsHeader liefert leere Header fuer nicht erlaubte Origins', () => {
const api = new NotesApi('/tmp/vault', { allowedOrigins: ['https://app.example.com'] });
expect(api.corsHeader('https://evil.com')).toEqual({});
});
});
describe('NotesApi.generateToken', () => {
it('Token-Format: <timestamp>.<hmac>', () => {
const token = NotesApi.generateToken('secret');
expect(token).toMatch(/^\d+\.[a-f0-9]+$/);
});
it('Verschiedene Secrets ergeben verschiedene Tokens', () => {
const t1 = NotesApi.generateToken('secret-a');
const t2 = NotesApi.generateToken('secret-b');
expect(t1).not.toBe(t2);
// Aber gleicher Timestamp-Teil moeglich
const hmac1 = t1.split('.')[1];
const hmac2 = t2.split('.')[1];
expect(hmac1).not.toBe(hmac2);
});
});