Integrationstests gegen eine Postgres-Instanz im Speicher
Die Tests starten Postgres per PGlite und sprechen es ueber einen lokalen Port an, laufen also ohne Docker und ohne installierte Datenbank. Abgedeckt sind die Guthaben-Regeln, der CC-Lebenszyklus samt Reaktivierung, Stornos und Ruecknahmen, Anmeldung mit CSRF- und Rollenpruefung sowie der Import mit Dedup.
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
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 Gross;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 gueltigem 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('ueberspringt 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 laesst 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user