Files
CC-Manager/server/test/scraper.test.ts
T
Harlekin 6b49b34eef Offene Vorschauen sichtbar machen und doppelt aufnehmen verhindern
Der Abgleich bucht bewusst nichts, sondern legt eine Vorschau zur Bestaetigung
an. Nur fuehrte von dort kein Weg weiter: die Import-Seite zeigte allein den
Datei-Upload, und die Rueckmeldung sagte lediglich "abgeglichen". Die
gefundenen Pakete lagen damit unsichtbar herum und der Knopf wirkte, als tue
er nichts.

Die Import-Seite listet jetzt alle Importe, die noch auf Bestaetigung warten,
und oeffnet sie in derselben Vorschau wie einen frischen Upload. Die Meldung
nach dem Abgleich nennt die Zahl der Funde und den naechsten Schritt.

Ausserdem beruecksichtigt der Abgleich jetzt offene Vorschauen: bisher pruefte
er nur gegen bereits gebuchte Pakete. Ein zweiter Lauf legte dieselben Zeilen
deshalb ein weiteres Mal an, und beim Buchen kollidierten beide Importe an der
eindeutigen Bestell-ID - der zweite schlug komplett fehl.
2026-09-18 17:12:06 +02:00

209 lines
7.1 KiB
TypeScript

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>): 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();
});
});