import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, it } from 'node:test'; import { classifyPackage, parseProfile, type ScrapedPackage } from '../src/lib/gextension.js'; import { looksLikeSteamId, normalizeSteamId, toLegacySteamId } from '../src/lib/steamid.js'; const here = dirname(fileURLToPath(import.meta.url)); const FIXTURE = readFileSync(join(here, 'fixtures', 'profil.html'), 'utf8'); function pkg(overrides: Partial): ScrapedPackage { return { packageBoughtId: 1, packageName: 'Custom Character', serverBundle: 'SCP-RP', purchasedAt: null, amount: '0.00 EUR', gateway: '', revoked: false, ...overrides, }; } describe('Profil auslesen', () => { const profile = parseProfile(FIXTURE, '76561198000000999'); it('findet alle Pakete', () => { assert.equal(profile.packages.length, 8); }); it('liest Titel, Bereich und Kaufdatum', () => { const cc = profile.packages.find((p) => p.packageBoughtId === 9001); assert.ok(cc); assert.equal(cc.packageName, 'Custom Character'); assert.equal(cc.serverBundle, 'SCP-RP'); assert.equal(cc.purchasedAt?.getFullYear(), 2025); assert.equal(cc.purchasedAt?.getMonth(), 9, 'Oktober'); assert.equal(cc.purchasedAt?.getDate(), 6); }); it('erkennt stornierte Käufe', () => { const revoked = profile.packages.find((p) => p.packageBoughtId === 9007); assert.equal(revoked?.revoked, true); }); it('liest den Nicknamen aus der Allgemein-Box', () => { assert.equal(profile.nickname, 'Testspieler'); }); it('kommt mit kaputtem HTML klar, ohne alles zu verlieren', () => { const broken = FIXTURE.replace( 'donations_data[9004] = {"id"', 'donations_data[9004] = {kaputt"id"', ); const result = parseProfile(broken, '76561198000000999'); assert.equal(result.packages.length, 7, 'nur die kaputte Zeile fällt weg'); }); }); describe('Pakete zuordnen', () => { it('erkennt ein CC-Paket', () => { assert.deepEqual(classifyPackage(pkg({ packageName: 'Custom Character' })), { kind: 'package', target: 'CC_PACKAGE', }); }); it('erkennt die Schreibweise mit K', () => { assert.deepEqual(classifyPackage(pkg({ packageName: 'Custom Charakter-Beantragung' })), { kind: 'package', target: 'CC_PACKAGE', }); }); it('zählt "Custom Character Redesign" als Redesign, nicht als CC', () => { assert.deepEqual(classifyPackage(pkg({ packageName: 'Custom Character Redesign' })), { kind: 'package', target: 'REDESIGN_PACKAGE', }); }); it('liest den Verbrauchszähler samt Anzahl', () => { const result = classifyPackage( pkg({ packageName: 'CC-Paket oder Redesign genutzt | 3 Mal' }), ); assert.deepEqual(result, { kind: 'usage_counter', times: 3 }); }); it('ignoriert Merch und Ränge trotz SCP-RP-Tag', () => { assert.equal(classifyPackage(pkg({ packageName: 'FG-Tasse | Merch' })).kind, 'ignore'); assert.equal(classifyPackage(pkg({ packageName: '25 Ingame-Level' })).kind, 'ignore'); assert.equal(classifyPackage(pkg({ packageName: 'VIP (12 Monate)' })).kind, 'ignore'); }); it('ignoriert alles aus dem Web-Bereich', () => { const result = classifyPackage( pkg({ packageName: 'Ablassbrief (01.04.2026)', serverBundle: 'Web' }), ); assert.equal(result.kind, 'ignore'); }); it('bucht ein storniertes Paket nicht', () => { const result = classifyPackage(pkg({ packageName: 'Custom Character', revoked: true })); assert.equal(result.kind, 'ignore'); }); it('schiebt unklare CC-Titel in die Handprüfung statt zu raten', () => { assert.equal(classifyPackage(pkg({ packageName: 'CC Sonderaktion Winter' })).kind, 'unclear'); }); }); describe('SteamID-Schreibweisen', () => { // Dieselbe (erfundene) Kennung in drei Formaten. const legacy = 'STEAM_0:1:19867635'; const modern = '[U:1:39735271]'; const sixtyFour = '76561198000000999'; it('führt alle drei Formate auf dieselbe ID zusammen', () => { assert.equal(normalizeSteamId(legacy), sixtyFour); assert.equal(normalizeSteamId(modern), sixtyFour); assert.equal(normalizeSteamId(sixtyFour), sixtyFour); }); it('rechnet zurück in die Legacy-Form', () => { assert.equal(toLegacySteamId(sixtyFour), legacy); }); it('beherrscht auch das ungerade Bit', () => { const odd = 'STEAM_0:1:12345678'; const asNumber = normalizeSteamId(odd); assert.ok(asNumber); assert.equal(toLegacySteamId(asNumber), odd); }); it('weist Unsinn ab', () => { assert.equal(normalizeSteamId('Darkbrine44'), null); assert.equal(normalizeSteamId(''), null); assert.equal(normalizeSteamId('12345'), null); assert.equal(looksLikeSteamId('Darkbrine44'), false); }); }); // Der Ablauf mit Datenbank: was passiert bei einem zweiten Abgleich? describe('Mehrfacher Abgleich', () => { it('nimmt Pakete aus einer offenen Vorschau nicht erneut auf', async () => { const { startTestDatabase } = await import('./harness.js'); const database = await startTestDatabase(54334); process.env.DATABASE_URL = database.url; process.env.SESSION_SECRET = 'test-secret-that-is-long-enough-000000'; process.env.NODE_ENV = 'test'; const db = await import('../src/db.js'); const { PLANS } = await import('../src/services/imports.js'); const user = await db.prisma.user.create({ data: { username: 'abgleicher', passwordHash: 'x', role: 'ADMIN' }, }); const player = await db.prisma.player.create({ data: { displayName: 'Testspieler', steamId: '76561198000000999' }, }); // Erster Lauf: ein Paket landet in einer offenen Vorschau. const first = await db.prisma.import.create({ data: { filename: 'Abgleich 1', status: 'PENDING', rowCount: 1, importedById: user.id, rows: { create: [ { lineNo: 1, externalId: 'gext:9001', playerKey: '76561198000000999', productName: 'Custom Character', target: 'CC_PACKAGE', resolvedPlayerId: player.id, plan: PLANS.BOOK, raw: {}, }, ], }, }, }); // Genau diese Pruefung macht der zweite Abgleich. const pending = await db.prisma.importRow.findMany({ where: { externalId: { not: null }, import: { status: 'PENDING' } }, select: { externalId: true }, }); const wartend = new Set(pending.map((row) => row.externalId as string)); assert.ok(wartend.has('gext:9001'), 'das Paket wird als wartend erkannt'); assert.equal(wartend.size, 1); // Nach dem Buchen zaehlt stattdessen die Dedup ueber ImportedOrder. await db.prisma.import.update({ where: { id: first.id }, data: { status: 'COMMITTED' } }); const danach = await db.prisma.importRow.findMany({ where: { externalId: { not: null }, import: { status: 'PENDING' } }, select: { externalId: true }, }); assert.equal(danach.length, 0, 'ein gebuchter Import blockiert nicht mehr'); await db.prisma.$disconnect(); await database.stop(); }); });