import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { startTestDatabase, type TestDatabase } from './harness.js'; type Imports = typeof import('../src/services/imports.js'); type Players = typeof import('../src/services/players.js'); type Characters = typeof import('../src/services/characters.js'); type Credits = typeof import('../src/services/credits.js'); type Audit = typeof import('../src/services/audit.js'); type Db = typeof import('../src/db.js'); let database: TestDatabase; let imports: Imports; let players: Players; let characters: Characters; let credits: Credits; let audit: Audit; let db: Db; let ctx: { actorId: number; actorName: string }; // Ein Export, wie ihn ein Shop liefern koennte: mehr Spalten als die App braucht. const CSV = [ 'Bestellnummer;Steam-ID;Produkt;Email;Rechnungsadresse;Zahlungsart', 'P-1;76561198000000501;CC-Paket;spieler@example.com;Musterweg 1, 12345 Musterstadt;PayPal', ].join('\n'); const MAPPING = { playerKey: 'Steam-ID', product: 'Produkt', orderId: 'Bestellnummer', createMissingPlayers: true, }; before(async () => { database = await startTestDatabase(54333); process.env.DATABASE_URL = database.url; process.env.SESSION_SECRET = 'test-secret-that-is-long-enough-000000'; process.env.NODE_ENV = 'test'; db = await import('../src/db.js'); imports = await import('../src/services/imports.js'); players = await import('../src/services/players.js'); characters = await import('../src/services/characters.js'); credits = await import('../src/services/credits.js'); audit = await import('../src/services/audit.js'); const user = await db.prisma.user.create({ data: { username: 'datenschuetzer', passwordHash: 'x', role: 'ADMIN' }, }); ctx = { actorId: user.id, actorName: user.username }; await db.prisma.productMapping.create({ data: { matchPattern: 'cc-paket', target: 'CC_PACKAGE', priority: 10 }, }); }); after(async () => { await db.prisma.$disconnect(); await database.stop(); }); describe('Datenminimierung beim Import', () => { it('verwirft nach dem Buchen die nicht zugeordneten Spalten', async () => { const parsed = imports.parseUpload('shop.csv', Buffer.from(CSV, 'utf8')); const importId = await imports.createImport('shop.csv', parsed, ctx); await imports.applyMapping(importId, MAPPING, ctx); const before = await db.prisma.importRow.findFirstOrThrow({ where: { importId } }); const rawBefore = before.raw as Record; assert.equal(rawBefore.Email, 'spieler@example.com', 'vor dem Buchen ist noch alles da'); await imports.commitImport(importId, ctx); const after = await db.prisma.importRow.findFirstOrThrow({ where: { importId } }); const rawAfter = after.raw as Record; assert.equal(rawAfter.Email, undefined, 'die Mailadresse ist weg'); assert.equal(rawAfter.Rechnungsadresse, undefined, 'die Anschrift ist weg'); assert.equal(rawAfter.Zahlungsart, undefined, 'die Zahlungsart ist weg'); assert.equal(rawAfter['Steam-ID'], '76561198000000501', 'die zugeordnete Spalte bleibt'); }); it('raeumt auch einen verworfenen Import auf', async () => { const parsed = imports.parseUpload('shop2.csv', Buffer.from(CSV, 'utf8')); const importId = await imports.createImport('shop2.csv', parsed, ctx); await imports.applyMapping(importId, MAPPING, ctx); await imports.discardImport(importId, ctx); const row = await db.prisma.importRow.findFirstOrThrow({ where: { importId } }); const raw = row.raw as Record; assert.equal(raw.Email, undefined, 'auch ohne Buchung bleiben keine Rohdaten liegen'); }); }); describe('Fehlgeschlagene Anmeldung', () => { it('schreibt einen unbekannten Benutzernamen nicht ins Protokoll', async () => { // Genau der Fall: jemand tippt sein Passwort ins Namensfeld. const versehen = 'MeinGeheimesPasswort123'; await audit.recordAudit(db.prisma, { actorId: null, actorName: null, ip: '127.0.0.1' }, { action: 'auth.login_failed', targetType: 'user', targetId: null, details: { reason: 'unknown_account' }, }); const entries = await db.prisma.auditLog.findMany({ where: { action: 'auth.login_failed' }, }); for (const entry of entries) { assert.notEqual(entry.actorName, versehen); assert.equal(entry.actorName, null, 'ohne bekanntes Konto bleibt das Namensfeld leer'); } }); }); describe('Loeschung eines Spielers', () => { it('entfernt auch Spuren in Import-Zeilen und Audit-Details', async () => { // Eigene Bestellnummer, sonst greift zu Recht die Dedup-Logik. const csv = [ 'Bestellnummer;Steam-ID;Produkt;Email;Rechnungsadresse;Zahlungsart', 'P-3;76561198000000501;CC-Paket;spieler@example.com;Musterweg 1, 12345 Musterstadt;PayPal', ].join('\n'); const parsed = imports.parseUpload('shop3.csv', Buffer.from(csv, 'utf8')); const importId = await imports.createImport('shop3.csv', parsed, ctx); await imports.applyMapping(importId, MAPPING, ctx); await imports.commitImport(importId, ctx); const player = await db.prisma.player.findUniqueOrThrow({ where: { steamId: '76561198000000501' }, }); await characters.createCharacter(player.id, { name: 'Testfigur' }, ctx); await players.deletePlayer(player.id, ctx); const gone = await db.prisma.player.findUnique({ where: { id: player.id } }); assert.equal(gone, null, 'der Spieler ist weg'); const ledgerLeft = await db.prisma.creditLedger.count({ where: { playerId: player.id } }); assert.equal(ledgerLeft, 0, 'die Guthaben-Zeilen sind mit weg'); const rows = await db.prisma.importRow.findMany({ where: { importId } }); for (const row of rows) { assert.equal(row.playerKey, null, 'die SteamID steht nicht mehr in der Import-Zeile'); assert.equal(row.resolvedPlayerId, null); assert.deepEqual(row.raw, {}, 'die Rohzeile ist geleert'); } const auditEntries = await db.prisma.auditLog.findMany({ where: { targetType: 'player', targetId: String(player.id) }, }); assert.ok(auditEntries.length > 0, 'die Vorgaenge selbst bleiben nachvollziehbar'); for (const entry of auditEntries) { const details = JSON.stringify(entry.details ?? {}); assert.ok( !details.includes('76561198000000501'), `SteamID steckt noch in ${entry.action}: ${details}`, ); } }); }); describe('Aufbewahrung des Audit-Logs', () => { it('loescht Eintraege jenseits der Frist, neuere bleiben', async () => { const alt = new Date(Date.now() - 400 * 24 * 3600 * 1000); const veraltet = await db.prisma.auditLog.create({ data: { action: 'test.alt', targetType: 'test', createdAt: alt }, }); const frisch = await db.prisma.auditLog.create({ data: { action: 'test.frisch', targetType: 'test' }, }); const removed = await audit.cleanupOldAuditEntries(); assert.ok(removed >= 1); assert.equal(await db.prisma.auditLog.findUnique({ where: { id: veraltet.id } }), null); assert.ok(await db.prisma.auditLog.findUnique({ where: { id: frisch.id } })); }); });