f97999c3c0
- 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>
69 lines
2.7 KiB
JavaScript
69 lines
2.7 KiB
JavaScript
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);
|
|
});
|