# Lasttests mit k6 (PRX-36)

Drei k6-Szenarien gegen den laufenden `docker-compose`-Stack (siehe Repo-Root
`docker-compose.yml`) mit echten JWTs aus Keycloak — keine gemockte Auth, keine gemockte
Datenbank.

## k6 einfach erklärt

**k6 ist ein Lasttest-Werkzeug.** Es simuliert viele gleichzeitige Benutzer, die HTTP-Aufrufe
gegen die Anwendung senden. Dadurch prüfen wir nicht nur, ob ein Endpunkt funktioniert, sondern
auch, wie schnell und stabil er unter paralleler Nutzung bleibt.

- **VU (Virtual User):** ein virtueller Benutzer. Mehr VUs bedeuten mehr parallele Nutzer.
- **Iteration:** ein vollständiger Durchlauf eines Tests durch einen virtuellen Benutzer.
- **Request:** ein einzelner HTTP-Aufruf an das Backend.
- **Fehlerrate:** Anteil der fehlgeschlagenen HTTP-Aufrufe; Ziel ist `0 %`.
- **p95:** 95 % aller Aufrufe waren höchstens so schnell wie dieser Wert. Die langsamsten 5 %
  werden dabei nicht versteckt, sondern getrennt betrachtet.
- **Threshold:** ein automatisch geprüftes Ziel, zum Beispiel `p95 < 800 ms`.

k6 verändert den Anwendungscode nicht. Die Tests erzeugen aber bewusst echte Testdaten in der
lokalen Datenbank. Deshalb niemals gegen eine produktive Umgebung ausführen.

## Zusammenhang zwischen k6 und Keycloak

k6 ist nur der **Benutzer-Simulator**. Es ersetzt Keycloak nicht und umgeht die Security nicht.
Da die PROCUREX-REST-APIs geschützt sind, benötigt jeder API-Aufruf ein gültiges JWT von
Keycloak.

Der Ablauf eines Lasttests ist:

```text
k6-Skript
   │ 1. Benutzername + Passwort
   ▼
Keycloak (Realm: procurex, Client: procurex-loadtest)
   │ 2. Access Token / JWT
   ▼
k6-Skript
   │ 3. Authorization: Bearer <JWT>
   ▼
Traefik / PROCUREX-API
   │ 4. JWT-Signatur und Rollen werden geprüft
   ▼
Geschützter REST-Endpunkt
```

### Was passiert technisch?

1. `setup()` oder der Testablauf ruft `loadtests/lib/auth.js` auf.
2. k6 sendet Benutzername, Passwort, `client_id=procurex-loadtest` und
   `grant_type=password` an den Keycloak-Token-Endpunkt.
3. Keycloak prüft die Zugangsdaten und gibt ein kurzlebiges Access Token als JWT zurück.
4. Das Skript übergibt dieses Token bei jedem API-Aufruf im Header
   `Authorization: Bearer <JWT>`.
5. Die PROCUREX-API beziehungsweise der Security-Filter prüft Signatur, Ablaufzeit und Rollen.
   Erst danach wird der fachliche Endpunkt ausgeführt.

Der Lasttest misst damit den realistischen Zugriff eines authentifizierten Benutzers. Die
Token-Anforderung selbst ist normalerweise **nicht** Teil der Latenzmessung des fachlichen
Endpunkts: Das Token wird in `setup()` einmal pro k6-Testlauf oder pro benötigter Rolle geholt
und anschließend wiederverwendet.

### Warum gibt es einen eigenen Keycloak-Client?

Für die Lasttests wird bewusst `procurex-loadtest` verwendet und nicht der Frontend-Client
`procurex-frontend`. Der Loadtest läuft ohne Browser und nutzt deshalb Direct Access Grants
(Password Grant). Der Frontend-Client bleibt beim vorgesehenen Authorization-Code-Flow mit PKCE.
So bleibt die echte SPA-Login-Konfiguration unverändert.

Die Rollen werden durch unterschiedliche Testbenutzer abgebildet:

| Testbenutzer | Rolle im Szenario | Verwendung |
|---|---|---|
| `buyer1` | Buyer | Katalogsuche und Bestellung absenden |
| `approver1` | Approver | Bestellung genehmigen |
| `supplieradmin1` | Supplier Admin | Testlieferant und Katalogartikel vorbereiten |

Die Passwörter werden nicht im Skript dokumentiert oder fest einprogrammiert. Sie kommen über
Umgebungsvariablen wie `BUYER_PASSWORD`, `APPROVER_PASSWORD` und `SUPPLIERADMIN_PASSWORD`.
Für das Lernprojekt ist der Password Grant vertretbar, weil k6 ohne Browser arbeitet; in einer
produktiven Umgebung sollten dafür keine echten Benutzerpasswörter verwendet werden.

### Typische Fehler bei Keycloak

| Symptom | Wahrscheinliche Ursache |
|---|---|
| `401` beim Token-Abruf | Benutzer, Passwort, Realm oder Client fehlen/falsch |
| `401` bei der API | JWT fehlt, ist abgelaufen oder wurde für den falschen Realm ausgestellt |
| `403` bei der API | Token ist gültig, aber die erforderliche Rolle fehlt |
| Token-Endpunkt nicht erreichbar | Keycloak oder der Docker-Stack läuft nicht |

Bei Problemen zuerst prüfen:

```powershell
docker compose ps
k6 version
```

Danach kontrollieren, ob der Client `procurex-loadtest` im Realm `procurex` vorhanden ist und
die Testbenutzer die erwarteten Rollen besitzen. Die vollständige Keycloak-Konfiguration steht
in `keycloak/README.md`.

## Installation auf diesem Rechner

k6 ist auf diesem Windows-Rechner **direkt über Scoop installiert**, nicht als Docker-Container:

```powershell
k6 version
# k6.exe v2.1.0
```

Der lokale Aufruf wird über `C:\Users\aydin\scoop\shims\k6.exe` bereitgestellt. Docker ist
separat installiert und wird nur für den PROCUREX-Anwendungsstack verwendet.

Falls k6 auf einem anderen Rechner fehlt, kann es mit Scoop installiert werden:

```powershell
scoop install k6
```

## Ein Testlauf für Einsteiger

1. PowerShell im Projektverzeichnis öffnen.
2. Den benötigten Docker-Stack starten:

```powershell
docker compose up -d
docker compose ps
```

3. Prüfen, ob k6 erreichbar ist:

```powershell
k6 version
```

4. Einen einzelnen Test starten:

```powershell
k6 run --dns "policy=onlyIPv4" loadtests/01-katalogsuche.js
```

5. Am Ende die Zusammenfassung lesen. Wichtig sind `http_req_failed`, `http_req_duration` und
die Ausgabe `thresholds`. Für die drei Szenarien existieren fertige Skripte; man muss keine
JavaScript-Datei schreiben, um den ersten Test auszuführen.

Beenden kann man einen laufenden Test mit `Ctrl+C`. Die bereits erzeugten Testdaten bleiben
dabei in der Datenbank und müssen anschließend bei Bedarf manuell bereinigt werden.

## Voraussetzungen

- Stack läuft: `docker compose up -d` im Repo-Root (Backend erreichbar unter
  `http://procurex-api.localhost`, Keycloak unter `http://keycloak.localhost`).
- [k6](https://k6.io/) installiert (`k6 version`).
- Keycloak-Client `procurex-loadtest` existiert im Realm `procurex` — siehe
  `keycloak/README.md`, Abschnitt „`procurex-loadtest` manuell anlegen“. Ohne diesen Client
  schlägt der Token-Abruf mit `401` fehl.

## Wichtig: `--dns policy=onlyIPv4`

`*.localhost`-Hostnamen lösen unter Windows sowohl zu `::1` (IPv6) als auch `127.0.0.1`
(IPv4) auf. Traefik (der Reverse-Proxy vor `procurex-app`) nimmt aber nur IPv4-Verbindungen
an — k6 versucht ohne dieses Flag trotzdem zuerst IPv6, wartet dort auf einen TCP-Timeout
(~20–40 s) und fällt erst danach auf IPv4 zurück. Das macht jeden einzelnen Request absurd
langsam und verfälscht die Latenz-Messung komplett. **Immer mit diesem Flag ausführen:**

```bash
k6 run --dns "policy=onlyIPv4" loadtests/01-katalogsuche.js
k6 run --dns "policy=onlyIPv4" loadtests/02-bestellabsendung.js
k6 run --dns "policy=onlyIPv4" loadtests/03-genehmigungsentscheidung.js
```

Optional konfigurierbar per Umgebungsvariable (Defaults passen zum lokalen `docker-compose`-
Setup): `API_BASE`, `KEYCLOAK_TOKEN_URL`, `LOADTEST_CLIENT_ID`, `BUYER_PASSWORD`,
`APPROVER_PASSWORD`, `SUPPLIERADMIN_PASSWORD`.

## Szenarien

| Skript | Endpunkt | Ablauf |
|---|---|---|
| `01-katalogsuche.js` | `GET /api/v1/catalog-items` (PRX-4) | Reiner Lese-Endpunkt, daher höhere Last (bis 20 VUs). `setup()` holt einmal ein `buyer1`-Token, jede Iteration sucht mit zufälligem Suchbegriff. |
| `02-bestellabsendung.js` | `POST /api/v1/cart/submit` (PRX-2) | `setup()` legt einmal einen aktiven Test-Lieferanten mit Katalogartikel an (als `supplieradmin1`). Jede Iteration sendet als `buyer1` einen neuen Warenkorb ab — erzeugt bewusst viele `PENDING_APPROVAL`-Bestellungen (unkritisch für ein Lernprojekt). |
| `03-genehmigungsentscheidung.js` | `POST /api/v1/orders/{id}/approval/approve` (PRX-5/PRX-8) | Eine Bestellung darf laut PRX-8 nur **einmal** entschieden werden — deshalb erzeugt jede Iteration zuerst eine frische Bestellung (als `buyer1`) und genehmigt genau diese (als `approver1`). Die Absende-Anfrage ist Vorbereitung, nicht Teil der eigentlichen Messung (separates `name:approve`-Tag im Threshold). |

## Ergebnisse (Lauf vom 2026-08-10, lokaler Docker-Compose-Stack, PRX-36)

### Aktueller Nachlauf mit Podman/enterprise-infrastructure

Erneuter Lauf am 2026-08-29 (Podman-Machine neu gestartet, `podman-compose` per `pip`
nachinstalliert, Rootless-Port-80-Fix für Traefik) bestand ebenfalls alle drei Szenarien mit 0 %
Fehlern: Katalogsuche 1643 Requests / p95 7,64 ms, Bestellabsendung 816 Requests / p95 18,85 ms
und Genehmigungsentscheidung 536 Requests / Approve-p95 16,06 ms — alle gesetzten Thresholds
deutlich unterschritten. Vorher lief der Lauf am 2026-08-11 gegen dieselbe Podman-Machine
ebenfalls fehlerfrei (Katalogsuche 1612 Requests / p95 46,57 ms, Bestellabsendung 783 Requests /
p95 107,67 ms, Genehmigungsentscheidung 524 Requests / Approve-p95 40,89 ms). Die darunter
dokumentierten Werte sind der historische Docker-Desktop-Lauf und bleiben als Vergleichsnachweis
erhalten.

Rohausgaben liegen in `loadtests/results/*.txt`.

| Szenario | Requests | Fehlerrate | p95 Latenz | Ziel-Threshold | Ergebnis |
|---|---|---|---|---|---|
| Katalogsuche (bis 20 VUs, 1m45s) | 1566 | 0,00 % | 130,9 ms | < 500 ms | ✅ erreicht |
| Bestellabsendung (bis 10 VUs, 1m45s) | 575 | 0,00 % | 869,1 ms | < 800 ms | ⚠️ knapp verfehlt (~9 %) |
| Genehmigungsentscheidung (bis 5 VUs, 1m10s), nur `approve`-Aufruf | 204 | 0,00 % | 318,7 ms | < 800 ms | ✅ erreicht |

**Fazit:** Alle drei Endpunkte sind unter der getesteten Last funktional stabil — **0 %
Fehlerrate** über alle 2185 HTTP-Requests hinweg, kein einziger fehlgeschlagener Request.
Die Katalogsuche und die Genehmigungsentscheidung liegen deutlich innerhalb der gesetzten
Latenzbudgets. Die Bestellabsendung (schreibt in drei Tabellen: Order, Order-Lines,
Outbox-Event für PRX-17) verfehlt das mit 800 ms bewusst knapp gesetzte Ziel geringfügig
(869 ms p95 bei 10 parallelen Nutzern) — funktional unauffällig, aber ein Kandidat für
gezielte Optimierung (z. B. Indexe, Verbindungspool-Größe), falls striktere Latenzvorgaben
gefordert werden. Kein Blocker für den aktuellen Lernprojekt-Rahmen.

## Aufräumen

Die Testläufe legen echte Datensätze in der geteilten Postgres-Instanz an (Test-Lieferanten,
Katalogartikel, Bestellungen mit `k6-Lasttest-*`-Namen bzw. `loadtest@procurex.example`
als Kontakt-E-Mail). Für ein Lernprojekt unkritisch und absichtlich nicht automatisiert
aufgeräumt — bei Bedarf manuell über die Postgres-Instanz oder per Datenbank-Reset
entfernen.

## Jenkins-Ausführung (PRX-36)

Das Jenkinsfile enthält ein optionales Stage `k6: Lasttests`. Es wird nur mit dem Build-Parameter
`RUN_K6=true` aktiviert. Der Standard-Build führt k6 nicht automatisch aus, weil die Tests echte
Bestellungen und Katalogdaten erzeugen und deshalb eine bewusst gestartete Lernumgebung benötigen.

Vor der Aktivierung müssen auf dem Jenkins-Agent erfüllt sein:

- `k6` ist installiert und über `k6 version` erreichbar.
- Docker-Compose-Stack, Keycloak, PostgreSQL, Kafka und Traefik sind erreichbar.
- Die Passwörter und optionalen URLs kommen aus Jenkins-Credentials oder geschützten
  Environment-Variablen.
- Der Agent darf keine produktive Umgebung als `API_BASE` verwenden.

Das Stage führt alle drei vorhandenen Skripte mit `--dns "policy=onlyIPv4"` aus. Ein Jenkins-
Nachweis gilt erst als vollständig, wenn die drei k6-Ausgaben archiviert und die erzeugten
Testdaten beziehungsweise deren Bereinigung dokumentiert sind.
