65 lines
5.2 KiB
TypeScript
65 lines
5.2 KiB
TypeScript
import { beforeAll, describe, expect, it } from 'vitest';
|
|
import type { FastifyInstance } from 'fastify';
|
|
import { buildApp } from '../src/server.js';
|
|
import { one, query, run } from '../src/core/db.js';
|
|
import { resetMock } from '@kc/connector-mock';
|
|
import { runOnce } from '../../worker/src/jobs.js';
|
|
import { call, code, login, makeUser } from './helpers.js';
|
|
|
|
let app: FastifyInstance;
|
|
beforeAll(async () => { app = await buildApp(); await app.ready(); });
|
|
const drain = async () => { for (let i = 0; i < 30; i++) { await run("UPDATE jobs SET run_at = UTC_TIMESTAMP(3) WHERE status IN ('scheduled','retrying')"); if (!(await runOnce(() => undefined))) break; } };
|
|
async function staff(email: string, role: string) {
|
|
await makeUser({ email, kind: 'staff', staffRole: role });
|
|
const { client } = await login(app, email);
|
|
const s = (await call(app, client, 'POST', '/auth/mfa/setup')).json();
|
|
await call(app, client, 'POST', '/auth/mfa/confirm', { code: code(s.secret) });
|
|
return client;
|
|
}
|
|
async function customer(admin: any, name: string, mail: string) {
|
|
const c = (await call(app, admin, 'POST', '/admin/customers', { type: 'business', name, owner: { email: mail, name } })).json();
|
|
await app.inject({ method: 'POST', url: '/v1/auth/invite/accept', payload: { token: new URL(c.inviteLink).searchParams.get('token'), password: 'passwort-kunde-123', repeat: 'passwort-kunde-123' } });
|
|
return { org: c.id as string, client: (await login(app, mail, 'passwort-kunde-123')).client };
|
|
}
|
|
|
|
describe('Schlüssel auf Abruf', () => {
|
|
it('zeigt den vollständigen Schlüssel nur Berechtigten, nie in Listen/Details/Audit', async () => {
|
|
resetMock();
|
|
const admin = await staff('rv-adm@x.test', 'admin'); const sa = await staff('rv-sa@x.test', 'superadmin'); const sup = await staff('rv-sup@x.test', 'support');
|
|
const A = await customer(admin, 'Firma A', 'rv-a@x.test'); const B = await customer(admin, 'Firma B', 'rv-b@x.test');
|
|
// Mitglied ohne Verwaltungsrecht in A
|
|
const inv = (await call(app, A.client, 'POST', `/orgs/${A.org}/invitations`, { email: 'rv-m@x.test', name: 'M', role: 'member' })).json();
|
|
await app.inject({ method: 'POST', url: '/v1/auth/invite/accept', payload: { token: new URL(inv.inviteLink).searchParams.get('token'), password: 'member-passwort-1', repeat: 'member-passwort-1' } });
|
|
const member = (await login(app, 'rv-m@x.test', 'member-passwort-1')).client;
|
|
await call(app, sa, 'POST', '/admin/connectors', { connector: 'mock', name: 'Mock-Reveal', values: { instance: 'rv' } }); await drain();
|
|
const res = (await call(app, admin, 'GET', '/resources')).json()[0];
|
|
await call(app, admin, 'PATCH', `/admin/resources/${res.id}`, { orgId: A.org });
|
|
|
|
// Liste: Schlüsselfeld nur für Berechtigte
|
|
expect((await call(app, A.client, 'GET', '/resources')).json()[0].canReveal).toBe(true);
|
|
expect((await call(app, member, 'GET', '/resources')).json()[0].canReveal).toBe(false);
|
|
expect((await call(app, sup, 'GET', '/resources')).json()[0].canReveal).toBe(false);
|
|
const d = (await call(app, A.client, 'GET', `/resources/${res.id}`)).json();
|
|
expect(d.canReveal).toBe(true); expect((await call(app, member, 'GET', `/resources/${res.id}`)).json().canReveal).toBe(false);
|
|
expect(JSON.stringify(d)).not.toContain('MOCK-KEY'); // Details enthalten den Schlüssel nie
|
|
|
|
const ok = await call(app, A.client, 'POST', `/resources/${res.id}/reveal`);
|
|
expect(ok.statusCode).toBe(200); expect(ok.json().items).toEqual([{ label: 'Lizenzschlüssel', value: `MOCK-KEY-${res.externalRef ?? 'm-1'}` }]);
|
|
expect(ok.headers['cache-control']).toBe('no-store'); expect(ok.json().hideAfterSec).toBe(60);
|
|
expect((await call(app, admin, 'POST', `/resources/${res.id}/reveal`)).statusCode).toBe(200); // Personal mit Schreibrecht
|
|
expect((await call(app, member, 'POST', `/resources/${res.id}/reveal`)).json().error.code).toBe('REVEAL_FORBIDDEN'); // Mitglied darf nicht
|
|
expect((await call(app, sup, 'POST', `/resources/${res.id}/reveal`)).json().error.code).toBe('REVEAL_FORBIDDEN'); // Support nur lesend
|
|
expect((await call(app, B.client, 'POST', `/resources/${res.id}/reveal`)).statusCode).toBe(404); // fremde Organisation
|
|
expect((await call(app, A.client, 'POST', `/resources/${res.id}/reveal`, undefined)).statusCode).toBe(200);
|
|
const anon = await app.inject({ method: 'POST', url: `/v1/resources/${res.id}/reveal` }); expect(anon.statusCode).toBe(401);
|
|
|
|
// Audit: Abruf ist protokolliert, der Schlüssel selbst nirgends
|
|
const ev = await query("SELECT actor_id, org_id, after_json FROM audit_events WHERE action = 'resource.reveal'"); expect(ev.length).toBeGreaterThanOrEqual(3);
|
|
expect(JSON.stringify(await query('SELECT * FROM audit_events'))).not.toContain('MOCK-KEY');
|
|
expect(JSON.stringify(await query('SELECT * FROM resources'))).not.toContain('MOCK-KEY');
|
|
// Anbieter-Ausfall: verständlicher Fehler
|
|
const inst = await one('SELECT id FROM connector_instances LIMIT 1');
|
|
await run("UPDATE connector_instances SET config_json = JSON_SET(config_json, '$.simulateOutage', 'true') WHERE id = ?", [inst!.id]);
|
|
const down = await call(app, A.client, 'POST', `/resources/${res.id}/reveal`); expect(down.statusCode).toBe(502); expect(down.json().error.message).toMatch(/nicht erreichbar/);
|
|
});
|
|
});
|