Die Profilseiten sind ohne Login abrufbar und betten die Kaeufe als JSON ins HTML ein. Gelesen wird dieses JSON statt der gerenderten Tabelle: das ist unempfindlich gegen Layout-Aenderungen und liefert zusaetzlich Kaufdatum, Betrag und den Storno-Status. Ein Headless-Browser ist nicht noetig. Der Abgleich bucht nichts von sich aus. Eindeutige Treffer landen als regulaerer Import in der bestehenden Vorschau, die per package_bought_id dedupliziert. Titel, die nach CC klingen, sich aber keiner Guthaben-Art sicher zuordnen lassen, gehen in eine Handpruefung statt geraten zu werden - sichtbar als Hinweis auf der Uebersicht und als Box beim Spieler. Beachtet werden dabei: stornierte Kaeufe werden uebersprungen, der Web- Bereich enthaelt nie CC-Pakete, und ein Redesign-Titel zaehlt als Redesign, auch wenn Custom Character darin vorkommt. Letzteres war auch in den Standard-Mappings des Datei-Imports falsch herum einsortiert. Der Lauf geht bewusst nur ueber bereits erfasste Spieler und haelt eine Sekunde Abstand zwischen den Anfragen, da die Seite fremde Infrastruktur ist. Voraussetzung dafuer ist, dass SteamIDs auf eine Schreibweise gebracht werden: die Legacy-Form, die Steam3-Form und die 64-Bit-Zahl bezeichnen dieselbe Person, wurden bisher aber als verschiedene Spieler gefuehrt. Gespeichert wird jetzt immer die 64-Bit-Form, Eingabe und Suche akzeptieren alle drei.
143 lines
4.8 KiB
TypeScript
143 lines
4.8 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 Kaeufe', () => {
|
|
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 faellt 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('zaehlt "Custom Character Redesign" als Redesign, nicht als CC', () => {
|
|
assert.deepEqual(classifyPackage(pkg({ packageName: 'Custom Character Redesign' })), {
|
|
kind: 'package',
|
|
target: 'REDESIGN_PACKAGE',
|
|
});
|
|
});
|
|
|
|
it('liest den Verbrauchszaehler 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 Raenge 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 Handpruefung 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('fuehrt alle drei Formate auf dieselbe ID zusammen', () => {
|
|
assert.equal(normalizeSteamId(legacy), sixtyFour);
|
|
assert.equal(normalizeSteamId(modern), sixtyFour);
|
|
assert.equal(normalizeSteamId(sixtyFour), sixtyFour);
|
|
});
|
|
|
|
it('rechnet zurueck 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);
|
|
});
|
|
});
|