Files
CC-Manager/server/test/imports.test.ts
T
Harlekin 751f8bfe8e Schema-Aenderungen als eigene Migration nachziehen
Die erste Migration wurde nachtraeglich mehrfach erweitert, statt Aenderungen
anzuhaengen. Auf einer bestehenden Installation gilt sie als angewendet und
wird uebersprungen - die seither hinzugekommenen Tabellen fehlten dort also
schlicht, ohne dass beim Start etwas auffiel. Die Antragsseite lief dadurch
in einen internen Fehler.

Die neue Migration ergaenzt alles Fehlende und ist durchgehend mit
IF NOT EXISTS geschrieben, weil bestehende Datenbanken je nach Zeitpunkt der
Einrichtung unterschiedliche Staende haben. Geprueft gegen eine frische
Datenbank, gegen eine auf altem Stand und auf mehrfaches Anwenden.

Der Testharness wendet jetzt alle Migrationen der Reihe nach an statt nur der
ersten - sonst faellt so etwas beim naechsten Mal wieder nicht auf.

Ausserdem scheiterte das Buchen komplett, wenn eine Bestellung zwischenzeitlich
ueber einen anderen Import gebucht worden war: die eindeutige Bestell-ID kippte
die gesamte Transaktion. Solche Zeilen werden jetzt uebersprungen und gezaehlt,
die uebrigen laufen durch.

Dazu fehlten in der Oberflaeche die Wege zum Bearbeiten und Loeschen eines
Spielers, obwohl es die Endpunkte gab. Beides ist jetzt auf der Spielerseite
erreichbar; das Loeschen verlangt den Namen als Bestaetigung und benennt
vorher, was mit verschwindet.
2026-09-18 17:43:25 +02:00

244 lines
8.4 KiB
TypeScript

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 Credits = typeof import('../src/services/credits.js');
type Db = typeof import('../src/db.js');
let database: TestDatabase;
let imports: Imports;
let credits: Credits;
let db: Db;
let ctx: { actorId: number; actorName: string };
const CSV = [
'Bestellnummer;Steam-ID;Spielername;Produkt;Kaufdatum',
'A-1001;76561198000000001;Rabe;CC-Paket Groß;2025-01-15',
'A-1002;76561198000000002;Fuchs;Redesign Gutschein;2025-01-16',
'A-1003;76561198000000001;Rabe;Spielerfarbe Blau;2025-01-17',
'A-1004;76561198000000003;Dachs;CC-Paket Klein;2025-01-18',
].join('\n');
const MAPPING = {
playerKey: 'Steam-ID',
playerName: 'Spielername',
product: 'Produkt',
orderId: 'Bestellnummer',
purchasedAt: 'Kaufdatum',
createMissingPlayers: true,
};
before(async () => {
database = await startTestDatabase(54332);
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');
credits = await import('../src/services/credits.js');
const user = await db.prisma.user.create({
data: { username: 'importeur', passwordHash: 'x', role: 'ADMIN' },
});
ctx = { actorId: user.id, actorName: user.username };
await db.prisma.productMapping.createMany({
data: [
{ matchPattern: 'cc-paket', target: 'CC_PACKAGE', priority: 10 },
{ matchPattern: 'redesign', target: 'REDESIGN_PACKAGE', priority: 20 },
{ matchPattern: 'spielerfarbe', target: 'IGNORE', priority: 30 },
],
});
});
after(async () => {
await db.prisma.$disconnect();
await database.stop();
});
describe('Datei einlesen', () => {
it('erkennt Semikolon als Trennzeichen', () => {
const parsed = imports.parseUpload('export.csv', Buffer.from(CSV, 'utf8'));
assert.equal(parsed.rows.length, 4);
assert.deepEqual(parsed.columns, [
'Bestellnummer',
'Steam-ID',
'Spielername',
'Produkt',
'Kaufdatum',
]);
});
it('liest auch JSON-Listen', () => {
const json = JSON.stringify([{ order: 'B-1', product: 'CC-Paket', user: '765' }]);
const parsed = imports.parseUpload('export.json', Buffer.from(json, 'utf8'));
assert.equal(parsed.rows.length, 1);
assert.equal(parsed.rows[0]?.product, 'CC-Paket');
});
it('weist eine leere Datei ab', () => {
assert.throws(() => imports.parseUpload('leer.csv', Buffer.from('', 'utf8')));
});
});
describe('Vorschau und Buchung', () => {
it('ordnet jede Zeile ihrem Plan zu', async () => {
const parsed = imports.parseUpload('export.csv', Buffer.from(CSV, 'utf8'));
const importId = await imports.createImport('export.csv', parsed, ctx);
const summary = await imports.applyMapping(importId, MAPPING, ctx);
assert.equal(summary.CREATE_PLAYER_AND_BOOK, 3, 'drei neue Spieler mit gültigem Produkt');
assert.equal(summary.SKIP_IGNORED, 1, 'die Spielerfarbe wird ignoriert');
assert.equal(summary.SKIP_DUPLICATE, 0);
});
it('bucht CC-Pakete mit beiden Guthaben und legt Spieler an', async () => {
const parsed = imports.parseUpload('export.csv', Buffer.from(CSV, 'utf8'));
const importId = await imports.createImport('lauf1.csv', parsed, ctx);
await imports.applyMapping(importId, MAPPING, ctx);
const result = await imports.commitImport(importId, ctx);
assert.equal(result.booked, 3);
assert.equal(result.createdPlayers, 3);
const rabe = await db.prisma.player.findUniqueOrThrow({
where: { steamId: '76561198000000001' },
});
const balances = await credits.getBalances(rabe.id);
assert.equal(balances.CC, 1, 'CC-Paket gibt einen CC');
assert.equal(balances.REDESIGN_PAID, 1, 'und das inklusive Redesign');
const fuchs = await db.prisma.player.findUniqueOrThrow({
where: { steamId: '76561198000000002' },
});
const fuchsBalances = await credits.getBalances(fuchs.id);
assert.equal(fuchsBalances.CC, 0, 'ein Redesign-Paket gibt keinen CC');
assert.equal(fuchsBalances.REDESIGN_PAID, 1);
});
it('bucht dieselbe Bestell-ID kein zweites Mal', async () => {
const parsed = imports.parseUpload('export.csv', Buffer.from(CSV, 'utf8'));
const importId = await imports.createImport('lauf2.csv', parsed, ctx);
const summary = await imports.applyMapping(importId, MAPPING, ctx);
assert.equal(summary.SKIP_DUPLICATE, 3, 'alle bereits gebuchten Bestellungen');
assert.equal(summary.BOOK, 0);
assert.equal(summary.CREATE_PLAYER_AND_BOOK, 0);
await assert.rejects(
() => imports.commitImport(importId, ctx),
(error: Error & { code?: string }) => error.code === 'NOTHING_TO_BOOK',
);
});
it('erkennt Dubletten auch innerhalb einer Datei', async () => {
const doubled = [
'Bestellnummer;Steam-ID;Produkt',
'C-1;76561198000000009;CC-Paket',
'C-1;76561198000000009;CC-Paket',
].join('\n');
const parsed = imports.parseUpload('doppelt.csv', Buffer.from(doubled, 'utf8'));
const importId = await imports.createImport('doppelt.csv', parsed, ctx);
const summary = await imports.applyMapping(
importId,
{
playerKey: 'Steam-ID',
product: 'Produkt',
orderId: 'Bestellnummer',
createMissingPlayers: true,
},
ctx,
);
assert.equal(summary.CREATE_PLAYER_AND_BOOK, 1);
assert.equal(summary.SKIP_DUPLICATE, 1);
});
it('überspringt unbekannte Spieler, wenn sie nicht angelegt werden sollen', async () => {
const csv = ['Bestellnummer;Steam-ID;Produkt', 'D-1;76561198000009999;CC-Paket'].join('\n');
const parsed = imports.parseUpload('fremd.csv', Buffer.from(csv, 'utf8'));
const importId = await imports.createImport('fremd.csv', parsed, ctx);
const summary = await imports.applyMapping(
importId,
{
playerKey: 'Steam-ID',
product: 'Produkt',
orderId: 'Bestellnummer',
createMissingPlayers: false,
},
ctx,
);
assert.equal(summary.SKIP_NO_PLAYER, 1);
});
it('ein abgeschlossener Import lässt sich nicht erneut buchen', async () => {
const csv = ['Bestellnummer;Steam-ID;Produkt', 'E-1;76561198000000001;CC-Paket'].join('\n');
const parsed = imports.parseUpload('nochmal.csv', Buffer.from(csv, 'utf8'));
const importId = await imports.createImport('nochmal.csv', parsed, ctx);
await imports.applyMapping(
importId,
{ playerKey: 'Steam-ID', product: 'Produkt', orderId: 'Bestellnummer', createMissingPlayers: false },
ctx,
);
await imports.commitImport(importId, ctx);
await assert.rejects(
() => imports.commitImport(importId, ctx),
(error: Error & { code?: string }) => error.code === 'NOT_PENDING',
);
});
});
describe('Kollisionen beim Buchen', () => {
it('überspringt eine Bestellung, die inzwischen anderswo gebucht wurde', async () => {
const csv = ['Bestellnummer;Steam-ID;Produkt', 'K-1;76561198000000077;CC-Paket'].join('\n');
const mapping = {
playerKey: 'Steam-ID',
product: 'Produkt',
orderId: 'Bestellnummer',
createMissingPlayers: true,
};
// Zwei Vorschauen mit derselben Bestellung - so entstanden durch mehrfachen Abgleich.
const ersteVorschau = await imports.createImport(
'erste.csv',
imports.parseUpload('erste.csv', Buffer.from(csv, 'utf8')),
ctx,
);
const zweiteVorschau = await imports.createImport(
'zweite.csv',
imports.parseUpload('zweite.csv', Buffer.from(csv, 'utf8')),
ctx,
);
await imports.applyMapping(ersteVorschau, mapping, ctx);
await imports.applyMapping(zweiteVorschau, mapping, ctx);
const erste = await imports.commitImport(ersteVorschau, ctx);
assert.equal(erste.booked, 1);
// Früher scheiterte hier der gesamte Import an der eindeutigen Bestell-ID.
const zweite = await imports.commitImport(zweiteVorschau, ctx);
assert.equal(zweite.booked, 0);
assert.equal(zweite.skipped, 1, 'die Zeile wird übersprungen, nicht der Import abgebrochen');
const player = await db.prisma.player.findUniqueOrThrow({
where: { steamId: '76561198000000077' },
});
const balances = await credits.getBalances(player.id);
assert.equal(balances.CC, 1, 'genau einmal gebucht, nicht doppelt');
});
});