perf/test: Build-Coalescing teilen, syncLibrary drosseln, API-Tests
- coalesce.js: generisches Serialisieren+Koaleszieren je Key; buildSite() in hugo.js nutzt es → Publish/Preview/Profil starten nie überlappende Hugo- Prozesse, schnelle Folge-Aufrufe lösen nur einen Trailing-Build aus - dialog-store: syncLibrary() gedrosselt (60s-TTL) statt bei jedem Forum-Read Filesystem-Walk + Upsert; Publish forciert Sync (force:true) - test/: node:test-Suite (19 Tests) für safeRel/normAuthors/urlFor/hasAccess, roleOf + lokale JWT-Verifikation, Rate-Limiter, Coalescing; npm test Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
// Env vor dem Import setzen: supabase.js bricht ohne URL/Key ab, ADMIN_EMAILS
|
||||
// und JWT_SECRET werden beim Modul-Load gelesen.
|
||||
process.env.SUPABASE_URL ||= 'http://localhost';
|
||||
process.env.SUPABASE_SERVICE_KEY ||= 'dummy';
|
||||
process.env.JWT_SECRET = 'test-secret';
|
||||
process.env.ADMIN_EMAILS = 'boss@x.ch';
|
||||
|
||||
const { roleOf, requireAuth } = await import('../src/auth.js');
|
||||
const { sign } = await import('hono/jwt');
|
||||
|
||||
test('roleOf: Admin aus ADMIN_EMAILS', () => {
|
||||
assert.equal(roleOf({ email: 'boss@x.ch' }), 'admin');
|
||||
assert.equal(roleOf({ email: 'BOSS@X.CH' }), 'admin');
|
||||
});
|
||||
|
||||
test('roleOf: Rolle aus app_metadata', () => {
|
||||
assert.equal(roleOf({ email: 'a@x.ch', app_metadata: { role: 'admin' } }), 'admin');
|
||||
assert.equal(roleOf({ email: 'a@x.ch', app_metadata: { role: 'editor' } }), 'editor');
|
||||
assert.equal(roleOf({ email: 'a@x.ch' }), 'user');
|
||||
});
|
||||
|
||||
// Minimaler Hono-Kontext-Stub.
|
||||
function fakeCtx(authHeader) {
|
||||
const store = {};
|
||||
return {
|
||||
req: { header: (h) => (h === 'Authorization' ? authHeader : undefined) },
|
||||
set: (k, v) => { store[k] = v; },
|
||||
get: (k) => store[k],
|
||||
json: (body, status = 200) => ({ __status: status, body }),
|
||||
};
|
||||
}
|
||||
|
||||
test('requireAuth: gültiges Token wird lokal verifiziert', async () => {
|
||||
const token = await sign(
|
||||
{ sub: 'u1', email: 'A@x.ch', app_metadata: { role: 'editor' }, exp: Math.floor(Date.now() / 1000) + 60 },
|
||||
'test-secret', 'HS256');
|
||||
let passed = false;
|
||||
const c = fakeCtx('Bearer ' + token);
|
||||
await requireAuth(c, async () => { passed = true; });
|
||||
assert.equal(passed, true);
|
||||
assert.equal(c.get('email'), 'a@x.ch'); // kleingeschrieben
|
||||
assert.equal(c.get('role'), 'editor');
|
||||
assert.equal(c.get('canModerate'), true);
|
||||
assert.equal(c.get('isAdmin'), false);
|
||||
});
|
||||
|
||||
test('requireAuth: fehlendes Token → 401', async () => {
|
||||
const c = fakeCtx('');
|
||||
const r = await requireAuth(c, async () => { throw new Error('darf nicht laufen'); });
|
||||
assert.equal(r.__status, 401);
|
||||
});
|
||||
|
||||
test('requireAuth: kaputtes/falsch signiertes Token → 401', async () => {
|
||||
const bad = await sign({ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 60 }, 'falsches-secret', 'HS256');
|
||||
for (const t of ['Bearer garbage', 'Bearer ' + bad]) {
|
||||
const r = await requireAuth(fakeCtx(t), async () => { throw new Error('darf nicht laufen'); });
|
||||
assert.equal(r.__status, 401);
|
||||
}
|
||||
});
|
||||
|
||||
test('requireAuth: abgelaufenes Token → 401', async () => {
|
||||
const expired = await sign({ sub: 'u1', exp: Math.floor(Date.now() / 1000) - 10 }, 'test-secret', 'HS256');
|
||||
const r = await requireAuth(fakeCtx('Bearer ' + expired), async () => { throw new Error('darf nicht laufen'); });
|
||||
assert.equal(r.__status, 401);
|
||||
});
|
||||
Reference in New Issue
Block a user