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,288 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { after, before, describe, it } from 'node:test';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { startTestDatabase, type TestDatabase } from './harness.js';
|
||||
|
||||
type Db = typeof import('../src/db.js');
|
||||
|
||||
let database: TestDatabase;
|
||||
let app: FastifyInstance;
|
||||
let db: Db;
|
||||
|
||||
const PASSWORD = 'TestPasswort123';
|
||||
|
||||
interface Credentials {
|
||||
session: string;
|
||||
csrf: string;
|
||||
}
|
||||
|
||||
async function login(username: string, password = PASSWORD): Promise<Credentials> {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/login',
|
||||
payload: { username, password },
|
||||
});
|
||||
assert.equal(response.statusCode, 200, `Login von ${username} fehlgeschlagen`);
|
||||
|
||||
const session = response.cookies.find((c) => c.name === 'ccm_sid');
|
||||
const csrf = response.cookies.find((c) => c.name === 'ccm_csrf');
|
||||
assert.ok(session && csrf, 'Sitzungs-Cookies fehlen');
|
||||
|
||||
return { session: session.value, csrf: csrf.value };
|
||||
}
|
||||
|
||||
function authHeaders(credentials: Credentials, withCsrf = true) {
|
||||
return {
|
||||
cookie: `ccm_sid=${credentials.session}; ccm_csrf=${credentials.csrf}`,
|
||||
...(withCsrf ? { 'x-csrf-token': credentials.csrf } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
database = await startTestDatabase(54330);
|
||||
process.env.DATABASE_URL = database.url;
|
||||
process.env.SESSION_SECRET = 'test-secret-that-is-long-enough-000000';
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.COOKIE_SECURE = 'false';
|
||||
|
||||
db = await import('../src/db.js');
|
||||
const { hashPassword } = await import('../src/auth/password.js');
|
||||
const { buildApp } = await import('../src/app.js');
|
||||
|
||||
const passwordHash = await hashPassword(PASSWORD);
|
||||
await db.prisma.user.createMany({
|
||||
data: [
|
||||
{ username: 'chef', passwordHash, role: 'ADMIN' },
|
||||
{ username: 'team', passwordHash, role: 'EDITOR' },
|
||||
{ username: 'gast', passwordHash, role: 'VIEWER' },
|
||||
{ username: 'neu', passwordHash, role: 'EDITOR', mustChangePassword: true },
|
||||
{ username: 'gesperrt', passwordHash, role: 'EDITOR', isActive: false },
|
||||
],
|
||||
});
|
||||
|
||||
app = await buildApp();
|
||||
await app.ready();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
await db.prisma.$disconnect();
|
||||
await database.stop();
|
||||
});
|
||||
|
||||
describe('Anmeldung', () => {
|
||||
it('weist ein falsches Passwort ab', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/login',
|
||||
payload: { username: 'chef', password: 'falsch' },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 401);
|
||||
});
|
||||
|
||||
it('verraet nicht, ob es den Benutzernamen gibt', async () => {
|
||||
const unknownUser = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/login',
|
||||
payload: { username: 'gibtesnicht', password: 'falsch' },
|
||||
});
|
||||
const wrongPassword = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/login',
|
||||
payload: { username: 'chef', password: 'falsch' },
|
||||
});
|
||||
|
||||
assert.equal(unknownUser.statusCode, wrongPassword.statusCode);
|
||||
assert.equal(unknownUser.json().message, wrongPassword.json().message);
|
||||
});
|
||||
|
||||
it('laesst ein gesperrtes Konto nicht herein', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/login',
|
||||
payload: { username: 'gesperrt', password: PASSWORD },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 401);
|
||||
});
|
||||
|
||||
it('setzt bei Erfolg beide Cookies', async () => {
|
||||
const credentials = await login('chef');
|
||||
assert.ok(credentials.session.length > 20);
|
||||
assert.ok(credentials.csrf.length > 20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CSRF-Schutz', () => {
|
||||
it('blockt eine schreibende Anfrage ohne Token', async () => {
|
||||
const credentials = await login('team');
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/players',
|
||||
headers: authHeaders(credentials, false),
|
||||
payload: { displayName: 'Ohne Token' },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 403);
|
||||
assert.equal(response.json().error, 'FORBIDDEN');
|
||||
});
|
||||
|
||||
it('blockt einen gefaelschten Token', async () => {
|
||||
const credentials = await login('team');
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/players',
|
||||
headers: { ...authHeaders(credentials, false), 'x-csrf-token': 'a'.repeat(64) },
|
||||
payload: { displayName: 'Falscher Token' },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 403);
|
||||
});
|
||||
|
||||
it('laesst die Anfrage mit passendem Token durch', async () => {
|
||||
const credentials = await login('team');
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/players',
|
||||
headers: authHeaders(credentials),
|
||||
payload: { displayName: 'Mit Token' },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 201);
|
||||
});
|
||||
|
||||
it('laesst lesende Anfragen ohne Token zu', async () => {
|
||||
const credentials = await login('gast');
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/players',
|
||||
headers: authHeaders(credentials, false),
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rollen', () => {
|
||||
it('ein Viewer darf lesen, aber nichts anlegen', async () => {
|
||||
const credentials = await login('gast');
|
||||
|
||||
const read = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/players',
|
||||
headers: authHeaders(credentials),
|
||||
});
|
||||
const write = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/players',
|
||||
headers: authHeaders(credentials),
|
||||
payload: { displayName: 'Verboten' },
|
||||
});
|
||||
|
||||
assert.equal(read.statusCode, 200);
|
||||
assert.equal(write.statusCode, 403);
|
||||
});
|
||||
|
||||
it('ein Editor kommt nicht in den Admin-Bereich', async () => {
|
||||
const credentials = await login('team');
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/admin/users',
|
||||
headers: authHeaders(credentials),
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 403);
|
||||
});
|
||||
|
||||
it('ein Admin kommt in den Admin-Bereich', async () => {
|
||||
const credentials = await login('chef');
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/admin/users',
|
||||
headers: authHeaders(credentials),
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.ok(Array.isArray(response.json().items));
|
||||
});
|
||||
|
||||
it('ohne Anmeldung ist alles zu', async () => {
|
||||
const response = await app.inject({ method: 'GET', url: '/api/players' });
|
||||
assert.equal(response.statusCode, 401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Erstpasswort', () => {
|
||||
it('sperrt schreibende Aktionen bis zum Wechsel', async () => {
|
||||
const credentials = await login('neu');
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/players',
|
||||
headers: authHeaders(credentials),
|
||||
payload: { displayName: 'Zu frueh' },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 403);
|
||||
assert.match(response.json().message, /Passwort/);
|
||||
});
|
||||
|
||||
it('erlaubt genau den Passwortwechsel', async () => {
|
||||
const credentials = await login('neu');
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/account/password',
|
||||
headers: authHeaders(credentials),
|
||||
payload: { currentPassword: PASSWORD, newPassword: 'NeuesPasswort456' },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
|
||||
const user = await db.prisma.user.findUniqueOrThrow({ where: { username: 'neu' } });
|
||||
assert.equal(user.mustChangePassword, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Guthaben ueber die API', () => {
|
||||
it('ein CC-Paket schlaegt in beiden Toepfen auf', async () => {
|
||||
const credentials = await login('team');
|
||||
|
||||
const created = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/players',
|
||||
headers: authHeaders(credentials),
|
||||
payload: { displayName: 'API-Spieler' },
|
||||
});
|
||||
const playerId = created.json().id;
|
||||
|
||||
const booked = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/players/${playerId}/credits/cc-package`,
|
||||
headers: authHeaders(credentials),
|
||||
payload: {},
|
||||
});
|
||||
|
||||
assert.equal(booked.statusCode, 200);
|
||||
assert.deepEqual(booked.json().balances, {
|
||||
CC: 1,
|
||||
REDESIGN_PAID: 1,
|
||||
REDESIGN_KULANZ: 0,
|
||||
redesignTotal: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('eine unbekannte API-Route liefert sauberes JSON', async () => {
|
||||
const response = await app.inject({ method: 'GET', url: '/api/gibtesnicht' });
|
||||
|
||||
assert.equal(response.statusCode, 404);
|
||||
assert.equal(response.json().error, 'NOT_FOUND');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user