mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 10:09:35 +01:00
test(masterdata): Integrationstests für alle vier Masterdata-Aggregate
70 MockMvc-Integrationstests für ProductCategory, Supplier, Article und Customer Controller. Abgedeckte Szenarien: Happy Paths, Validierung (400), Duplikate (409), Not Found (404), Autorisierung (403/401) sowie aggregate-spezifische Operationen (Zertifikate, Bewertungen, Verkaufs- einheiten, Lieferadressen, Präferenzen, Rahmenverträge). Außerdem: Manuelle Testfallbeschreibung unter backend/docs/MASTERDATA_MANUAL_TESTS.md
This commit is contained in:
parent
2059718a5c
commit
2ace5be394
5 changed files with 2071 additions and 0 deletions
328
backend/docs/MASTERDATA_MANUAL_TESTS.md
Normal file
328
backend/docs/MASTERDATA_MANUAL_TESTS.md
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
# Manuelle Testfälle – Masterdata BC
|
||||
|
||||
## Kontext
|
||||
Der Masterdata Bounded Context ist vollständig implementiert und umfasst vier Aggregate:
|
||||
- **ProductCategory** – einfaches Kategorie-Aggregate
|
||||
- **Supplier** – Lieferanten mit Zertifikaten, Bewertungen, Adressen
|
||||
- **Article** – Artikel mit Verkaufseinheiten (SalesUnits), Lieferantenzuordnung
|
||||
- **Customer** – Kunden (B2B/B2C) mit Lieferadressen, Präferenzen, Rahmenvertrag (nur B2B)
|
||||
|
||||
Die TUI ist das Testwerkzeug für das Backend. Alle Schreiboperationen erfordern `MASTERDATA_WRITE`-Permission.
|
||||
|
||||
---
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Backend läuft (`./mvnw spring-boot:run` oder Docker)
|
||||
- TUI läuft (`pnpm dev` im CLI-Verzeichnis)
|
||||
- Eingeloggt als User **mit** `MASTERDATA_WRITE`-Permission (z.B. `admin`)
|
||||
- Eingeloggt als User **ohne** `MASTERDATA_WRITE`-Permission (z.B. `viewer`) – für Authz-Tests
|
||||
|
||||
---
|
||||
|
||||
## TC-CAT: Produktkategorien
|
||||
|
||||
### TC-CAT-01: Kategorie erstellen (Happy Path)
|
||||
1. Masterdata → Produktkategorien → `[n]` Neu
|
||||
2. Name: `Obst & Gemüse`, Beschreibung: `Frische Produkte` → Enter
|
||||
- [x] **Erwartung:** Kategorie erscheint in der Liste; Name und Beschreibung korrekt
|
||||
|
||||
### TC-CAT-02: Kategorie erstellen – ohne Beschreibung
|
||||
1. Name: `Milchprodukte`, Beschreibung: leer → Enter
|
||||
- [x] **Erwartung:** Kategorie wird angelegt; Beschreibung fehlt ohne Fehler
|
||||
|
||||
### TC-CAT-03: Kategorie bearbeiten
|
||||
1. Kategorie `Obst & Gemüse` auswählen → `[e]`
|
||||
2. Name ändern auf `Obst und Gemüse`, Beschreibung auf `Saisonale Frische`
|
||||
- [x] **Erwartung:** Änderungen werden gespeichert und korrekt angezeigt
|
||||
|
||||
### TC-CAT-04: Doppelter Name wird abgelehnt
|
||||
1. Neue Kategorie mit Name `Milchprodukte` (existiert bereits) anlegen
|
||||
- [x] **Erwartung:** Fehlermeldung – Name bereits vergeben; kein Datensatz angelegt
|
||||
|
||||
### TC-CAT-05: Kategorie löschen
|
||||
1. Kategorie `Milchprodukte` in der Liste auswählen → `[d]`
|
||||
2. Bestätigungsdialog mit `Ja` bestätigen
|
||||
- [c] **Erwartung:** Kategorie verschwindet aus der Liste
|
||||
<!-- TODO: Löschen nicht implementiert -->
|
||||
|
||||
### TC-CAT-06: Leerer Name wird abgelehnt
|
||||
1. Neue Kategorie, Name leer → Enter
|
||||
- [x] **Erwartung:** Fehler / Eingabe nicht möglich; kein API-Aufruf
|
||||
|
||||
---
|
||||
|
||||
## TC-SUP: Lieferanten
|
||||
|
||||
### TC-SUP-01: Lieferant erstellen – Pflichtfelder
|
||||
1. Lieferanten → `[n]` Neu
|
||||
2. Name: `Frisch AG`, Telefon: `+49 30 12345` → restliche Felder leer → Enter
|
||||
- [x] **Erwartung:** Lieferant erscheint in der Liste, Status `AKTIV`
|
||||
|
||||
### TC-SUP-02: Lieferant erstellen – alle Felder
|
||||
1. Name: `Bio GmbH`, Telefon: `+49 89 999`, E-Mail: `bio@example.com`
|
||||
2. Ansprechpartner: `Max Muster`
|
||||
3. Adresse: `Gartenstraße 5`, `12`, `80333`, `München`, `Deutschland`
|
||||
4. Zahlungsziel: `30` Tage
|
||||
- [/] **Erwartung:** Alle Daten in der Detailansicht korrekt angezeigt
|
||||
<!-- TODO: Wenn 'Deutschland' eingegeben wird bekommt man einen Fehler dass nur Länderkennzeichen z.B. DE erlaubt sind -->
|
||||
|
||||
### TC-SUP-03: Lieferant erstellen – ohne Pflichtfelder
|
||||
1. Name leer → versuchen zu speichern
|
||||
- [x] **Erwartung:** Fehler; kein Lieferant angelegt
|
||||
3. Name gefüllt, Telefon leer → versuchen zu speichern
|
||||
- [x] **Erwartung:** Fehler; kein Lieferant angelegt
|
||||
|
||||
### TC-SUP-04: Doppelter Name wird abgelehnt
|
||||
1. Neuen Lieferanten `Frisch AG` anlegen (Name existiert bereits)
|
||||
- [x] **Erwartung:** Fehlermeldung – Name bereits vergeben
|
||||
|
||||
### TC-SUP-05: Lieferant deaktivieren und aktivieren
|
||||
1. `Frisch AG` → Detailansicht → `[Deaktivieren]` → Bestätigen
|
||||
- [x] **Erwartung:** Status wechselt auf `INAKTIV` (roter Punkt in Liste)
|
||||
3. Erneut öffnen → `[Aktivieren]` → Bestätigen
|
||||
- [x] **Erwartung:** Status wechselt auf `AKTIV`
|
||||
|
||||
### TC-SUP-06: Lieferant filtern
|
||||
1. Lieferantenliste: `[A]` – nur Aktive
|
||||
- [x] **Erwartung:** Nur AKTIV-Lieferanten sichtbar
|
||||
3. `[I]` – nur Inaktive
|
||||
- [x] **Erwartung:** Nur INAKTIV-Lieferanten sichtbar
|
||||
5. `[a]` – alle
|
||||
- [x] **Erwartung:** Alle Lieferanten sichtbar
|
||||
|
||||
### TC-SUP-07: Lieferant bewerten
|
||||
1. `Frisch AG` → `[Bewerten]`
|
||||
2. Qualität: 4, Lieferung: 3, Preis: 5 → Enter
|
||||
- [x] **Erwartung:** Bewertung in Detailansicht sichtbar; Durchschnitt = 4.0
|
||||
4. In der Liste: Stern-Anzeige `★ 4.0`
|
||||
|
||||
### TC-SUP-08: Bewertung – Grenzen (1 und 5)
|
||||
1. Alle Scores auf 1 setzen → Speichern
|
||||
- [x] **Erwartung:** Gespeichert; Durchschnitt = 1.0
|
||||
3. Alle Scores auf 5 → Speichern
|
||||
- [x] **Erwartung:** Gespeichert; Durchschnitt = 5.0
|
||||
|
||||
### TC-SUP-09: Zertifikat hinzufügen (gültig)
|
||||
1. `Frisch AG` → `[Zertifikat hinzufügen]`
|
||||
2. Typ: `ISO9001`, Aussteller: `TÜV`, ab: `2024-01-01`, bis: `2027-01-01`
|
||||
- [x] **Erwartung:** Zertifikat erscheint in Detailansicht; Anzahl in Liste = 1
|
||||
|
||||
### TC-SUP-10: Zertifikat hinzufügen (abgelaufen)
|
||||
1. Zertifikat, bis: `2023-12-31` (Datum in der Vergangenheit)
|
||||
- [x] **Erwartung:** Zertifikat wird angelegt (keine Ablauf-Prüfung beim Hinzufügen); in Detail sichtbar
|
||||
*(Edge Case: System soll abgelaufene Zertifikate anzeigen, nicht blocken)*
|
||||
|
||||
### TC-SUP-11: Doppeltes Zertifikat wird abgelehnt
|
||||
1. Erneut `ISO9001`, `TÜV`, `2024-01-01` hinzufügen
|
||||
- [x] **Erwartung:** Fehlermeldung – Duplikat abgelehnt
|
||||
<!-- TODO: Duplikat wird abgelehnt, ABER Fehlermeldung schlecht 'Unexpected error occurred. ...' -->
|
||||
|
||||
### TC-SUP-12: Zertifikat entfernen
|
||||
1. `Frisch AG` → `[Zertifikat entfernen]` → Zertifikat auswählen → Enter
|
||||
- [x] **Erwartung:** Zertifikat aus Detailansicht verschwunden
|
||||
<!-- TODO: Zertifikatsauswahl zeigt kein Datum, daher schwer zu entscheiden wenn mehrere gleiche Zertifikate für untersch. Jahre -->
|
||||
|
||||
---
|
||||
|
||||
## TC-ART: Artikel
|
||||
|
||||
### TC-ART-01: Artikel erstellen – PIECE_FIXED
|
||||
1. Artikel → `[n]` Neu
|
||||
2. Name: `Äpfel Gala`, Nummer: `OG-001`
|
||||
3. Kategorie: `Obst & Gemüse` (mit ← →)
|
||||
4. Einheit: `PIECE_FIXED`, Preis: `1.99`
|
||||
- [ ] **Erwartung:** Artikel in Liste; Preismodell automatisch `FIXED`
|
||||
|
||||
### TC-ART-02: Artikel erstellen – KG (gewichtsbasiert)
|
||||
1. Name: `Bananen`, Nummer: `OG-002`
|
||||
2. Einheit: `KG`, Preis: `2.49`
|
||||
- [ ] **Erwartung:** Preismodell automatisch `WEIGHT_BASED`
|
||||
|
||||
### TC-ART-03: Artikel erstellen – HUNDRED_GRAM und PIECE_VARIABLE
|
||||
1. Einheit `HUNDRED_GRAM` → Preismodell `WEIGHT_BASED`
|
||||
2. Einheit `PIECE_VARIABLE` → Preismodell `WEIGHT_BASED`
|
||||
- [ ] **Erwartung:** Konsistenz in beiden Fällen korrekt
|
||||
|
||||
### TC-ART-04: Doppelte Artikelnummer wird abgelehnt
|
||||
1. Neuen Artikel mit Nummer `OG-001` anlegen
|
||||
- [ ] **Erwartung:** Fehlermeldung – Artikelnummer bereits vergeben
|
||||
|
||||
### TC-ART-05: Artikel deaktivieren und aktivieren
|
||||
1. `Äpfel Gala` → `[Deaktivieren]` → Bestätigen
|
||||
- [ ] **Erwartung:** Status INAKTIV
|
||||
- [ ] **Erwartung:** Status AKTIV
|
||||
|
||||
### TC-ART-06: Artikel filtern
|
||||
1. `[A]` nur Aktive, `[I]` nur Inaktive, `[a]` alle
|
||||
- [ ] **Erwartung:** Filter wirkt korrekt
|
||||
|
||||
### TC-ART-07: Verkaufseinheit hinzufügen
|
||||
1. `Äpfel Gala` → `[Verkaufseinheit hinzufügen]`
|
||||
2. Einheit: `KG`, Preis: `3.50`
|
||||
- [ ] **Erwartung:** Zweite VE in Detailansicht; Anzahl VE in Liste = 2
|
||||
|
||||
### TC-ART-08: Doppelte Einheit wird abgelehnt
|
||||
1. Erneut `PIECE_FIXED` für `Äpfel Gala` hinzufügen
|
||||
- [ ] **Erwartung:** Fehlermeldung – Einheit bereits vorhanden
|
||||
|
||||
### TC-ART-09: Letzte Verkaufseinheit kann nicht entfernt werden
|
||||
1. Artikel mit genau einer VE → `[Verkaufseinheit entfernen]`
|
||||
- [ ] **Erwartung:** Aktion nicht verfügbar / Fehler – mindestens eine VE erforderlich
|
||||
|
||||
### TC-ART-10: Verkaufseinheit entfernen (wenn 2+ vorhanden)
|
||||
1. `Äpfel Gala` hat 2 VE → `[Verkaufseinheit entfernen]` → KG-Einheit wählen
|
||||
- [ ] **Erwartung:** VE entfernt; nur noch PIECE_FIXED vorhanden
|
||||
|
||||
### TC-ART-11: Lieferant dem Artikel zuweisen *(falls TUI-Unterstützung vorhanden)*
|
||||
1. `Äpfel Gala` → Lieferant `Frisch AG` zuweisen
|
||||
- [ ] **Erwartung:** Lieferant in Detailansicht sichtbar
|
||||
|
||||
---
|
||||
|
||||
## TC-CUS: Kunden
|
||||
|
||||
### TC-CUS-01: B2C-Kunde erstellen
|
||||
1. Kunden → `[n]` Neu
|
||||
2. Typ: `B2C`, Name: `Max Mustermann`, Telefon: `+49 176 12345`
|
||||
3. Rechnungsadresse: `Musterstr. 1`, `2`, `10115`, `Berlin`, `Deutschland`
|
||||
- [ ] **Erwartung:** Kunde in Liste, Typ-Badge `B2C`, Status `AKTIV`
|
||||
|
||||
### TC-CUS-02: B2B-Kunde erstellen
|
||||
1. Typ: `B2B`, Name: `Gastro GmbH`, Telefon: `+49 30 9876`
|
||||
2. Rechnungsadresse vollständig ausfüllen
|
||||
- [ ] **Erwartung:** Kunde in Liste, Typ-Badge `B2B`
|
||||
|
||||
### TC-CUS-03: Pflichtfelder Validierung
|
||||
1. Name leer → Fehler
|
||||
2. Telefon leer → Fehler
|
||||
3. Rechnungsadresse unvollständig → Fehler
|
||||
- [ ] **Erwartung:** Jeweils spezifische Fehlermeldung
|
||||
|
||||
### TC-CUS-04: Doppelter Kundenname wird abgelehnt
|
||||
1. Erneut `Gastro GmbH` anlegen
|
||||
- [ ] **Erwartung:** Fehlermeldung – Name bereits vergeben
|
||||
|
||||
### TC-CUS-05: Kunde deaktivieren und aktivieren
|
||||
1. `Max Mustermann` → `[Deaktivieren]` → Bestätigen → `[Aktivieren]`
|
||||
- [ ] **Erwartung:** Status wechselt korrekt
|
||||
|
||||
### TC-CUS-06: Kunden filtern (Status + Typ)
|
||||
1. `[A]` → nur Aktive; `[I]` → nur Inaktive; `[a]` → alle
|
||||
2. `[B]` → nur B2B; `[C]` → nur B2C; `[b]` → alle
|
||||
3. Kombination: Aktive B2B-Kunden
|
||||
- [ ] **Erwartung:** Alle Filter wirken korrekt und kombinierbar
|
||||
|
||||
### TC-CUS-07: Lieferadresse hinzufügen
|
||||
1. `Gastro GmbH` → `[Lieferadresse hinzufügen]`
|
||||
2. Label: `Hauptküche`, Straße: `Kochstr.`, Nr: `12`, PLZ: `10963`, Stadt: `Berlin`, Land: `Deutschland`
|
||||
3. Ansprechpartner: `Koch Müller`, Lieferhinweis: `Bitte kühlen`
|
||||
- [ ] **Erwartung:** Lieferadresse in Detailansicht; Anzahl in Liste = 1
|
||||
|
||||
### TC-CUS-08: Lieferadresse entfernen
|
||||
1. `[Lieferadresse entfernen]` → `Hauptküche` auswählen
|
||||
- [ ] **Erwartung:** Lieferadresse entfernt
|
||||
|
||||
### TC-CUS-09: Mehrere Lieferadressen
|
||||
1. Zwei Lieferadressen `Filiale Nord` und `Filiale Süd` hinzufügen
|
||||
- [ ] **Erwartung:** Beide in Detailansicht; Anzahl in Liste = 2
|
||||
|
||||
### TC-CUS-10: Präferenzen setzen
|
||||
1. `Max Mustermann` → `[Präferenzen setzen]`
|
||||
2. `BIO` und `REGIONAL` aktivieren → Enter
|
||||
- [ ] **Erwartung:** Präferenzen in Detailansicht sichtbar
|
||||
4. Erneut öffnen → nur `HALAL` aktivieren → Enter
|
||||
- [ ] **Erwartung:** Nur `HALAL` gesetzt (Set wird ersetzt, nicht ergänzt)
|
||||
|
||||
### TC-CUS-11: Alle Präferenzen abwählen
|
||||
1. `[Präferenzen setzen]` → alle deaktivieren → Enter
|
||||
- [ ] **Erwartung:** Keine Präferenzen mehr sichtbar
|
||||
|
||||
---
|
||||
|
||||
## TC-B2B: Rahmenverträge (B2B-spezifisch)
|
||||
|
||||
*Vorbedingung: `Gastro GmbH` (B2B) existiert; Artikel `Äpfel Gala` existiert*
|
||||
|
||||
### TC-B2B-01: Rahmenvertrag für B2B-Kunden erstellen *(falls TUI-Unterstützung vorhanden)*
|
||||
1. `Gastro GmbH` → Rahmenvertrag-Bereich
|
||||
2. Gültig ab: `2025-01-01`, bis: `2025-12-31`, Rhythmus: `WEEKLY`
|
||||
3. Position: Artikel `Äpfel Gala`, vereinbarter Preis: `1.50`, Menge: `100`
|
||||
- [ ] **Erwartung:** Rahmenvertrag in Detailansicht sichtbar
|
||||
|
||||
### TC-B2B-02: Rahmenvertrag für B2C-Kunden nicht möglich *(Backend-Test via API)*
|
||||
```
|
||||
POST /api/customers/{b2c-id}/frame-contract
|
||||
```
|
||||
- [ ] **Erwartung:** HTTP 400/422, Fehler `FrameContractNotAllowed`
|
||||
|
||||
---
|
||||
|
||||
## TC-AUTH: Autorisierung
|
||||
|
||||
### TC-AUTH-01: Lesezugriff ohne MASTERDATA_WRITE
|
||||
1. Als `viewer` (ohne MASTERDATA_WRITE) einloggen
|
||||
2. Alle Listen aufrufen (Kategorien, Lieferanten, Artikel, Kunden)
|
||||
- [ ] **Erwartung:** Daten sichtbar, kein Fehler
|
||||
|
||||
### TC-AUTH-02: Schreibzugriff ohne MASTERDATA_WRITE wird abgelehnt
|
||||
1. Als `viewer` versuchen: neue Kategorie erstellen
|
||||
- [ ] **Erwartung:** HTTP 403 / Fehlermeldung in TUI; kein Datensatz angelegt
|
||||
3. Dasselbe für Lieferant, Artikel, Kunde anlegen
|
||||
- [ ] **Erwartung:** Jeweils Ablehnung
|
||||
|
||||
### TC-AUTH-03: Schreibzugriff mit MASTERDATA_WRITE funktioniert
|
||||
1. Als `admin` (mit MASTERDATA_WRITE) → alle CRUD-Operationen möglich
|
||||
- [ ] **Erwartung:** Alle Operationen erfolgreich
|
||||
|
||||
---
|
||||
|
||||
## TC-CROSS: Übergreifende / Integrations-Tests
|
||||
|
||||
### TC-CROSS-01: Artikel-Lieferant-Verknüpfung konsistent
|
||||
1. Lieferant `Frisch AG` einem Artikel zuweisen
|
||||
2. `Frisch AG` deaktivieren
|
||||
3. Artikel aufrufen → Lieferant weiterhin referenziert (keine Zwangstrennung)
|
||||
- [ ] **Erwartung:** Artikel zeigt `Frisch AG` trotz INAKTIV-Status
|
||||
|
||||
### TC-CROSS-02: Kategorie in Artikelauswahl verfügbar
|
||||
1. Neue Kategorie `Getränke` anlegen
|
||||
2. Artikel erstellen → Kategorie-Auswahl enthält `Getränke`
|
||||
- [ ] **Erwartung:** Neue Kategorien sofort im Artikel-Formular verfügbar
|
||||
|
||||
### TC-CROSS-03: Sequenz – Kompletter Lieferant-Workflow
|
||||
1. Lieferant erstellen → bewerten → Zertifikat hinzufügen → deaktivieren → wieder aktivieren → Zertifikat entfernen
|
||||
- [ ] **Erwartung:** Alle Schritte funktionieren in Folge ohne Datenverlust
|
||||
|
||||
### TC-CROSS-04: Sequenz – Kompletter Artikel-Workflow
|
||||
1. Kategorie erstellen → Artikel erstellen (mit Kategorie) → 2. VE hinzufügen → 1. VE entfernen → Artikel deaktivieren → aktivieren
|
||||
- [ ] **Erwartung:** Konsistenz über alle Schritte
|
||||
|
||||
### TC-CROSS-05: Sequenz – B2B-Kunde vollständig
|
||||
1. B2B-Kunde erstellen → 2 Lieferadressen → Präferenzen setzen → 1 Adresse entfernen → Präferenzen ändern → deaktivieren → aktivieren
|
||||
- [ ] **Erwartung:** Konsistenz über alle Schritte
|
||||
|
||||
---
|
||||
|
||||
## Verifikation / Testdurchführung
|
||||
|
||||
```bash
|
||||
# Backend starten
|
||||
./mvnw spring-boot:run
|
||||
|
||||
# TUI starten
|
||||
cd frontend/apps/cli && pnpm dev
|
||||
|
||||
# Backend-Logs beobachten (Fehler sichtbar machen)
|
||||
# Direkte API-Tests (für TC-B2B-02, TC-AUTH)
|
||||
curl -X POST http://localhost:8080/api/... -H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
**Checkliste nach Test-Durchlauf:**
|
||||
- [ ] Alle TC-CAT (1-6) durchgeführt
|
||||
- [ ] Alle TC-SUP (1-12) durchgeführt
|
||||
- [ ] Alle TC-ART (1-11) durchgeführt
|
||||
- [ ] Alle TC-CUS (1-11) durchgeführt
|
||||
- [ ] TC-B2B (1-2) durchgeführt
|
||||
- [ ] TC-AUTH (1-3) durchgeführt
|
||||
- [ ] TC-CROSS (1-5) durchgeführt
|
||||
|
|
@ -0,0 +1,431 @@
|
|||
package de.effigenix.infrastructure.masterdata.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.effigenix.domain.masterdata.PriceModel;
|
||||
import de.effigenix.domain.masterdata.Unit;
|
||||
import de.effigenix.domain.usermanagement.RoleName;
|
||||
import de.effigenix.domain.usermanagement.UserStatus;
|
||||
import de.effigenix.infrastructure.masterdata.web.dto.*;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.repository.RoleJpaRepository;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.repository.UserJpaRepository;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* Integrationstests für ArticleController.
|
||||
*
|
||||
* Abgedeckte Testfälle: TC-ART-01 bis TC-ART-11
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
@DisplayName("Article Controller Integration Tests")
|
||||
class ArticleControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private UserJpaRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private RoleJpaRepository roleRepository;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String jwtSecret;
|
||||
|
||||
@Value("${jwt.expiration}")
|
||||
private long jwtExpiration;
|
||||
|
||||
private String adminToken;
|
||||
private String viewerToken;
|
||||
private String categoryId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
RoleEntity adminRole = new RoleEntity(
|
||||
UUID.randomUUID().toString(), RoleName.ADMIN, Set.of(), "Admin");
|
||||
roleRepository.save(adminRole);
|
||||
|
||||
RoleEntity viewerRole = new RoleEntity(
|
||||
UUID.randomUUID().toString(), RoleName.PRODUCTION_WORKER, Set.of(), "Viewer");
|
||||
roleRepository.save(viewerRole);
|
||||
|
||||
String adminId = UUID.randomUUID().toString();
|
||||
userRepository.save(new UserEntity(
|
||||
adminId, "art.admin", "art.admin@test.com",
|
||||
passwordEncoder.encode("Pass123"), Set.of(adminRole),
|
||||
"BRANCH-01", UserStatus.ACTIVE, LocalDateTime.now(), null));
|
||||
|
||||
String viewerId = UUID.randomUUID().toString();
|
||||
userRepository.save(new UserEntity(
|
||||
viewerId, "art.viewer", "art.viewer@test.com",
|
||||
passwordEncoder.encode("Pass123"), Set.of(viewerRole),
|
||||
"BRANCH-01", UserStatus.ACTIVE, LocalDateTime.now(), null));
|
||||
|
||||
adminToken = generateToken(adminId, "art.admin", "MASTERDATA_WRITE");
|
||||
viewerToken = generateToken(viewerId, "art.viewer", "USER_READ");
|
||||
|
||||
// Vorbedingung: Kategorie erstellen
|
||||
categoryId = createCategory("Obst & Gemüse");
|
||||
}
|
||||
|
||||
// ==================== TC-ART-01: PIECE_FIXED → Preismodell FIXED ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-ART-01: Artikel PIECE_FIXED erstellen → Preismodell FIXED, 201")
|
||||
void createArticle_pieceFix_returns201WithFixedPriceModel() throws Exception {
|
||||
var request = new CreateArticleRequest(
|
||||
"Äpfel Gala", "OG-001", categoryId,
|
||||
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("1.99"));
|
||||
|
||||
mockMvc.perform(post("/api/articles")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").isNotEmpty())
|
||||
.andExpect(jsonPath("$.name").value("Äpfel Gala"))
|
||||
.andExpect(jsonPath("$.articleNumber").value("OG-001"))
|
||||
.andExpect(jsonPath("$.salesUnits[0].unit").value("PIECE_FIXED"))
|
||||
.andExpect(jsonPath("$.salesUnits[0].priceModel").value("FIXED"))
|
||||
.andExpect(jsonPath("$.status").value("ACTIVE"));
|
||||
}
|
||||
|
||||
// ==================== TC-ART-02: KG → WEIGHT_BASED ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-ART-02: Artikel KG erstellen → Preismodell WEIGHT_BASED")
|
||||
void createArticle_kg_returnsWeightBasedPriceModel() throws Exception {
|
||||
var request = new CreateArticleRequest(
|
||||
"Bananen", "OG-002", categoryId,
|
||||
Unit.KG, PriceModel.WEIGHT_BASED, new BigDecimal("2.49"));
|
||||
|
||||
mockMvc.perform(post("/api/articles")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.salesUnits[0].unit").value("KG"))
|
||||
.andExpect(jsonPath("$.salesUnits[0].priceModel").value("WEIGHT_BASED"));
|
||||
}
|
||||
|
||||
// ==================== TC-ART-03: HUNDRED_GRAM und PIECE_VARIABLE ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-ART-03: Artikel HUNDRED_GRAM erstellen → Preismodell WEIGHT_BASED")
|
||||
void createArticle_hundredGram_returnsWeightBased() throws Exception {
|
||||
var request = new CreateArticleRequest(
|
||||
"Käse Scheiben", "MO-001", categoryId,
|
||||
Unit.HUNDRED_GRAM, PriceModel.WEIGHT_BASED, new BigDecimal("1.50"));
|
||||
|
||||
mockMvc.perform(post("/api/articles")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.salesUnits[0].unit").value("HUNDRED_GRAM"))
|
||||
.andExpect(jsonPath("$.salesUnits[0].priceModel").value("WEIGHT_BASED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-ART-03: Artikel PIECE_VARIABLE erstellen → Preismodell WEIGHT_BASED")
|
||||
void createArticle_pieceVariable_returnsWeightBased() throws Exception {
|
||||
var request = new CreateArticleRequest(
|
||||
"Hähnchen ganz", "FL-001", categoryId,
|
||||
Unit.PIECE_VARIABLE, PriceModel.WEIGHT_BASED, new BigDecimal("5.99"));
|
||||
|
||||
mockMvc.perform(post("/api/articles")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.salesUnits[0].unit").value("PIECE_VARIABLE"))
|
||||
.andExpect(jsonPath("$.salesUnits[0].priceModel").value("WEIGHT_BASED"));
|
||||
}
|
||||
|
||||
// ==================== TC-ART-04: Doppelte Artikelnummer ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-ART-04: Doppelte Artikelnummer → 409 Conflict")
|
||||
void createArticle_withDuplicateNumber_returns409() throws Exception {
|
||||
createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
|
||||
|
||||
var duplicate = new CreateArticleRequest(
|
||||
"Andere Äpfel", "OG-001", categoryId,
|
||||
Unit.KG, PriceModel.WEIGHT_BASED, new BigDecimal("2.00"));
|
||||
|
||||
mockMvc.perform(post("/api/articles")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(duplicate)))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
|
||||
// ==================== TC-ART-05: Deaktivieren und Aktivieren ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-ART-05: Artikel deaktivieren → Status INACTIVE")
|
||||
void deactivateArticle_activeArticle_returnsInactive() throws Exception {
|
||||
String articleId = createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
|
||||
|
||||
mockMvc.perform(post("/api/articles/{id}/deactivate", articleId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("INACTIVE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-ART-05: Artikel aktivieren → Status ACTIVE")
|
||||
void activateArticle_inactiveArticle_returnsActive() throws Exception {
|
||||
String articleId = createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
|
||||
|
||||
mockMvc.perform(post("/api/articles/{id}/deactivate", articleId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mockMvc.perform(post("/api/articles/{id}/activate", articleId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("ACTIVE"));
|
||||
}
|
||||
|
||||
// ==================== TC-ART-06: Filter ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-ART-06: Nur aktive Artikel filtern → nur ACTIVE")
|
||||
void listArticles_filterByActive_returnsOnlyActive() throws Exception {
|
||||
String id = createArticle("Inaktiv Produkt", "IN-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.00");
|
||||
mockMvc.perform(post("/api/articles/{id}/deactivate", id)
|
||||
.header("Authorization", "Bearer " + adminToken)).andReturn();
|
||||
|
||||
createArticle("Aktiv Produkt", "AC-001", Unit.KG, PriceModel.WEIGHT_BASED, "2.00");
|
||||
|
||||
mockMvc.perform(get("/api/articles")
|
||||
.param("status", "ACTIVE")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[*].status", everyItem(is("ACTIVE"))));
|
||||
}
|
||||
|
||||
// ==================== TC-ART-07: Verkaufseinheit hinzufügen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-ART-07: Zweite Verkaufseinheit hinzufügen → 2 VEs vorhanden")
|
||||
void addSalesUnit_toExistingArticle_returnsTwoUnits() throws Exception {
|
||||
String articleId = createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
|
||||
|
||||
var addUnit = new AddSalesUnitRequest(Unit.KG, PriceModel.WEIGHT_BASED, new BigDecimal("3.50"));
|
||||
|
||||
mockMvc.perform(post("/api/articles/{id}/sales-units", articleId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(addUnit)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.salesUnits", hasSize(2)));
|
||||
}
|
||||
|
||||
// ==================== TC-ART-08: Doppelte Einheit ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-ART-08: Doppelte Einheit hinzufügen → 409 Conflict")
|
||||
void addSalesUnit_duplicateUnit_returns409() throws Exception {
|
||||
String articleId = createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
|
||||
|
||||
var duplicate = new AddSalesUnitRequest(Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("2.50"));
|
||||
|
||||
mockMvc.perform(post("/api/articles/{id}/sales-units", articleId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(duplicate)))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
|
||||
// ==================== TC-ART-09: Letzte VE kann nicht entfernt werden ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-ART-09: Letzte Verkaufseinheit entfernen → 400 (Mindestanforderung)")
|
||||
void removeSalesUnit_lastUnit_returns400() throws Exception {
|
||||
String articleId = createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
|
||||
|
||||
// Hole die salesUnitId
|
||||
MvcResult getResult = mockMvc.perform(get("/api/articles/{id}", articleId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andReturn();
|
||||
String salesUnitId = objectMapper.readTree(getResult.getResponse().getContentAsString())
|
||||
.get("salesUnits").get(0).get("id").asText();
|
||||
|
||||
mockMvc.perform(delete("/api/articles/{id}/sales-units/{suId}", articleId, salesUnitId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
// ==================== TC-ART-10: VE entfernen (2+ vorhanden) ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-ART-10: Zweite Verkaufseinheit entfernen → nur erste bleibt übrig")
|
||||
void removeSalesUnit_whenTwoUnitsExist_removesCorrectUnit() throws Exception {
|
||||
String articleId = createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
|
||||
|
||||
// Zweite VE hinzufügen
|
||||
var addUnit = new AddSalesUnitRequest(Unit.KG, PriceModel.WEIGHT_BASED, new BigDecimal("3.50"));
|
||||
MvcResult addResult = mockMvc.perform(post("/api/articles/{id}/sales-units", articleId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(addUnit)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
|
||||
// KG-VE-ID ermitteln
|
||||
String kgSalesUnitId = null;
|
||||
var salesUnits = objectMapper.readTree(addResult.getResponse().getContentAsString()).get("salesUnits");
|
||||
for (var su : salesUnits) {
|
||||
if ("KG".equals(su.get("unit").asText())) {
|
||||
kgSalesUnitId = su.get("id").asText();
|
||||
}
|
||||
}
|
||||
|
||||
mockMvc.perform(delete("/api/articles/{id}/sales-units/{suId}", articleId, kgSalesUnitId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
mockMvc.perform(get("/api/articles/{id}", articleId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.salesUnits", hasSize(1)))
|
||||
.andExpect(jsonPath("$.salesUnits[0].unit").value("PIECE_FIXED"));
|
||||
}
|
||||
|
||||
// ==================== TC-ART-11: Lieferant zuweisen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-ART-11: Lieferant dem Artikel zuweisen → Lieferant in Detailansicht")
|
||||
void assignSupplier_toArticle_appearsInDetail() throws Exception {
|
||||
String articleId = createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
|
||||
String supplierId = createSupplier("Frisch AG");
|
||||
|
||||
var assignRequest = new AssignSupplierRequest(supplierId);
|
||||
|
||||
mockMvc.perform(post("/api/articles/{id}/suppliers", articleId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(assignRequest)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.supplierIds", hasItem(supplierId)));
|
||||
}
|
||||
|
||||
// ==================== TC-AUTH ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-AUTH-02: Artikel erstellen ohne MASTERDATA_WRITE → 403")
|
||||
void createArticle_withViewerToken_returns403() throws Exception {
|
||||
var request = new CreateArticleRequest(
|
||||
"Verboten", "VB-001", categoryId,
|
||||
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("1.00"));
|
||||
|
||||
mockMvc.perform(post("/api/articles")
|
||||
.header("Authorization", "Bearer " + viewerToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-AUTH-01: Artikelliste ohne MASTERDATA_WRITE → 200")
|
||||
void listArticles_withViewerToken_returns200() throws Exception {
|
||||
mockMvc.perform(get("/api/articles")
|
||||
.header("Authorization", "Bearer " + viewerToken))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Nicht existierenden Artikel abfragen → 404")
|
||||
void getArticle_nonExistent_returns404() throws Exception {
|
||||
mockMvc.perform(get("/api/articles/{id}", UUID.randomUUID())
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
// ==================== Hilfsmethoden ====================
|
||||
|
||||
private String createCategory(String name) throws Exception {
|
||||
var request = new CreateProductCategoryRequest(name, null);
|
||||
MvcResult result = mockMvc.perform(post("/api/categories")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
}
|
||||
|
||||
private String createArticle(String name, String number, Unit unit, PriceModel priceModel, String price) throws Exception {
|
||||
var request = new CreateArticleRequest(
|
||||
name, number, categoryId, unit, priceModel, new BigDecimal(price));
|
||||
MvcResult result = mockMvc.perform(post("/api/articles")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
}
|
||||
|
||||
private String createSupplier(String name) throws Exception {
|
||||
var request = new CreateSupplierRequest(
|
||||
name, "+49 30 12345",
|
||||
null, null, null, null, null, null, null, null, null);
|
||||
MvcResult result = mockMvc.perform(post("/api/suppliers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
}
|
||||
|
||||
private String generateToken(String userId, String username, String permissions) {
|
||||
long now = System.currentTimeMillis();
|
||||
javax.crypto.SecretKey key = io.jsonwebtoken.security.Keys.hmacShaKeyFor(
|
||||
jwtSecret.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
return Jwts.builder()
|
||||
.subject(userId)
|
||||
.claim("username", username)
|
||||
.claim("permissions", permissions)
|
||||
.issuedAt(new Date(now))
|
||||
.expiration(new Date(now + jwtExpiration))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,555 @@
|
|||
package de.effigenix.infrastructure.masterdata.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.effigenix.domain.masterdata.*;
|
||||
import de.effigenix.domain.usermanagement.RoleName;
|
||||
import de.effigenix.domain.usermanagement.UserStatus;
|
||||
import de.effigenix.infrastructure.masterdata.web.dto.*;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.repository.RoleJpaRepository;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.repository.UserJpaRepository;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* Integrationstests für CustomerController.
|
||||
*
|
||||
* Abgedeckte Testfälle: TC-CUS-01 bis TC-CUS-11, TC-B2B-01/02, TC-AUTH
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
@DisplayName("Customer Controller Integration Tests")
|
||||
class CustomerControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private UserJpaRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private RoleJpaRepository roleRepository;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String jwtSecret;
|
||||
|
||||
@Value("${jwt.expiration}")
|
||||
private long jwtExpiration;
|
||||
|
||||
private String adminToken;
|
||||
private String viewerToken;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RoleEntity adminRole = new RoleEntity(
|
||||
UUID.randomUUID().toString(), RoleName.ADMIN, Set.of(), "Admin");
|
||||
roleRepository.save(adminRole);
|
||||
|
||||
RoleEntity viewerRole = new RoleEntity(
|
||||
UUID.randomUUID().toString(), RoleName.PRODUCTION_WORKER, Set.of(), "Viewer");
|
||||
roleRepository.save(viewerRole);
|
||||
|
||||
String adminId = UUID.randomUUID().toString();
|
||||
userRepository.save(new UserEntity(
|
||||
adminId, "cus.admin", "cus.admin@test.com",
|
||||
passwordEncoder.encode("Pass123"), Set.of(adminRole),
|
||||
"BRANCH-01", UserStatus.ACTIVE, LocalDateTime.now(), null));
|
||||
|
||||
String viewerId = UUID.randomUUID().toString();
|
||||
userRepository.save(new UserEntity(
|
||||
viewerId, "cus.viewer", "cus.viewer@test.com",
|
||||
passwordEncoder.encode("Pass123"), Set.of(viewerRole),
|
||||
"BRANCH-01", UserStatus.ACTIVE, LocalDateTime.now(), null));
|
||||
|
||||
adminToken = generateToken(adminId, "cus.admin", "MASTERDATA_WRITE");
|
||||
viewerToken = generateToken(viewerId, "cus.viewer", "USER_READ");
|
||||
}
|
||||
|
||||
// ==================== TC-CUS-01: B2C-Kunde erstellen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-01: B2C-Kunde erstellen → 201, Typ B2C, Status ACTIVE")
|
||||
void createCustomer_b2c_returns201() throws Exception {
|
||||
var request = new CreateCustomerRequest(
|
||||
"Max Mustermann", CustomerType.B2C,
|
||||
"Musterstr. 1", "2", "10115", "Berlin", "DE",
|
||||
"+49 176 12345", null, null, null, null);
|
||||
|
||||
mockMvc.perform(post("/api/customers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").isNotEmpty())
|
||||
.andExpect(jsonPath("$.name").value("Max Mustermann"))
|
||||
.andExpect(jsonPath("$.type").value("B2C"))
|
||||
.andExpect(jsonPath("$.status").value("ACTIVE"))
|
||||
.andExpect(jsonPath("$.billingAddress.city").value("Berlin"));
|
||||
}
|
||||
|
||||
// ==================== TC-CUS-02: B2B-Kunde erstellen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-02: B2B-Kunde erstellen → 201, Typ B2B")
|
||||
void createCustomer_b2b_returns201() throws Exception {
|
||||
var request = new CreateCustomerRequest(
|
||||
"Gastro GmbH", CustomerType.B2B,
|
||||
"Gastrostr. 10", "1", "10178", "Berlin", "DE",
|
||||
"+49 30 9876", "gastro@example.com", "Frau Koch",
|
||||
30, "30 Tage netto");
|
||||
|
||||
mockMvc.perform(post("/api/customers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.name").value("Gastro GmbH"))
|
||||
.andExpect(jsonPath("$.type").value("B2B"));
|
||||
}
|
||||
|
||||
// ==================== TC-CUS-03: Pflichtfelder ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-03: Kunde ohne Namen → 400")
|
||||
void createCustomer_withBlankName_returns400() throws Exception {
|
||||
var request = new CreateCustomerRequest(
|
||||
"", CustomerType.B2C,
|
||||
"Musterstr. 1", "2", "10115", "Berlin", "DE",
|
||||
"+49 176 12345", null, null, null, null);
|
||||
|
||||
mockMvc.perform(post("/api/customers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-03: Kunde ohne Telefon → 400")
|
||||
void createCustomer_withBlankPhone_returns400() throws Exception {
|
||||
var request = new CreateCustomerRequest(
|
||||
"Kein Telefon", CustomerType.B2C,
|
||||
"Musterstr. 1", "2", "10115", "Berlin", "DE",
|
||||
"", null, null, null, null);
|
||||
|
||||
mockMvc.perform(post("/api/customers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-03: Kunde ohne Rechnungsadresse → 400")
|
||||
void createCustomer_withMissingBillingAddress_returns400() throws Exception {
|
||||
mockMvc.perform(post("/api/customers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
// ==================== TC-CUS-04: Doppelter Name ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-04: Doppelter Kundenname → 409 Conflict")
|
||||
void createCustomer_withDuplicateName_returns409() throws Exception {
|
||||
createB2bCustomer("Gastro GmbH");
|
||||
|
||||
var duplicate = new CreateCustomerRequest(
|
||||
"Gastro GmbH", CustomerType.B2B,
|
||||
"Andere Str. 5", null, "10179", "Berlin", "DE",
|
||||
"+49 30 1111", null, null, null, null);
|
||||
|
||||
mockMvc.perform(post("/api/customers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(duplicate)))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
|
||||
// ==================== TC-CUS-05: Deaktivieren und Aktivieren ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-05: Kunde deaktivieren → Status INACTIVE")
|
||||
void deactivateCustomer_activeCustomer_returnsInactive() throws Exception {
|
||||
String customerId = createB2cCustomer("Max Mustermann");
|
||||
|
||||
mockMvc.perform(post("/api/customers/{id}/deactivate", customerId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("INACTIVE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-05: Kunde aktivieren → Status ACTIVE")
|
||||
void activateCustomer_inactiveCustomer_returnsActive() throws Exception {
|
||||
String customerId = createB2cCustomer("Max Mustermann");
|
||||
|
||||
mockMvc.perform(post("/api/customers/{id}/deactivate", customerId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mockMvc.perform(post("/api/customers/{id}/activate", customerId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("ACTIVE"));
|
||||
}
|
||||
|
||||
// ==================== TC-CUS-06: Filter ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-06: Nur B2B-Kunden filtern → nur B2B")
|
||||
void listCustomers_filterByB2B_returnsOnlyB2B() throws Exception {
|
||||
createB2bCustomer("Gastro GmbH");
|
||||
createB2cCustomer("Max Mustermann");
|
||||
|
||||
mockMvc.perform(get("/api/customers")
|
||||
.param("type", "B2B")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[*].type", everyItem(is("B2B"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-06: Nur B2C-Kunden filtern → nur B2C")
|
||||
void listCustomers_filterByB2C_returnsOnlyB2C() throws Exception {
|
||||
createB2bCustomer("Gastro GmbH");
|
||||
createB2cCustomer("Max Mustermann");
|
||||
|
||||
mockMvc.perform(get("/api/customers")
|
||||
.param("type", "B2C")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[*].type", everyItem(is("B2C"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-06: Nur aktive Kunden filtern → nur ACTIVE")
|
||||
void listCustomers_filterByActive_returnsOnlyActive() throws Exception {
|
||||
String id = createB2cCustomer("Inaktiver Kunde");
|
||||
mockMvc.perform(post("/api/customers/{id}/deactivate", id)
|
||||
.header("Authorization", "Bearer " + adminToken)).andReturn();
|
||||
|
||||
createB2cCustomer("Aktiver Kunde");
|
||||
|
||||
mockMvc.perform(get("/api/customers")
|
||||
.param("status", "ACTIVE")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[*].status", everyItem(is("ACTIVE"))));
|
||||
}
|
||||
|
||||
// ==================== TC-CUS-07: Lieferadresse hinzufügen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-07: Lieferadresse hinzufügen → erscheint in Detailansicht")
|
||||
void addDeliveryAddress_validAddress_appearsInDetail() throws Exception {
|
||||
String customerId = createB2bCustomer("Gastro GmbH");
|
||||
|
||||
var addr = new AddDeliveryAddressRequest(
|
||||
"Hauptküche", "Kochstr.", "12", "10963", "Berlin", "DE",
|
||||
"Koch Müller", "Bitte kühlen");
|
||||
|
||||
mockMvc.perform(post("/api/customers/{id}/delivery-addresses", customerId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(addr)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.deliveryAddresses", hasSize(1)))
|
||||
.andExpect(jsonPath("$.deliveryAddresses[0].label").value("Hauptküche"))
|
||||
.andExpect(jsonPath("$.deliveryAddresses[0].address.city").value("Berlin"));
|
||||
}
|
||||
|
||||
// ==================== TC-CUS-08: Lieferadresse entfernen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-08: Lieferadresse entfernen → nicht mehr vorhanden")
|
||||
void removeDeliveryAddress_existingAddress_removedFromDetail() throws Exception {
|
||||
String customerId = createB2bCustomer("Gastro GmbH");
|
||||
|
||||
var addr = new AddDeliveryAddressRequest(
|
||||
"Hauptküche", "Kochstr.", "12", "10963", "Berlin", "DE",
|
||||
null, null);
|
||||
|
||||
mockMvc.perform(post("/api/customers/{id}/delivery-addresses", customerId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(addr)))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
mockMvc.perform(delete("/api/customers/{id}/delivery-addresses/{label}", customerId, "Hauptküche")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
mockMvc.perform(get("/api/customers/{id}", customerId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.deliveryAddresses", hasSize(0)));
|
||||
}
|
||||
|
||||
// ==================== TC-CUS-09: Mehrere Lieferadressen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-09: Zwei Lieferadressen → beide in Detailansicht")
|
||||
void addTwoDeliveryAddresses_bothAppearInDetail() throws Exception {
|
||||
String customerId = createB2bCustomer("Gastro GmbH");
|
||||
|
||||
var nord = new AddDeliveryAddressRequest(
|
||||
"Filiale Nord", "Nordstr.", "1", "20095", "Hamburg", "DE",
|
||||
null, null);
|
||||
var sued = new AddDeliveryAddressRequest(
|
||||
"Filiale Süd", "Südstr.", "2", "80331", "München", "DE",
|
||||
null, null);
|
||||
|
||||
mockMvc.perform(post("/api/customers/{id}/delivery-addresses", customerId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(nord)))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
mockMvc.perform(post("/api/customers/{id}/delivery-addresses", customerId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(sued)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.deliveryAddresses", hasSize(2)));
|
||||
}
|
||||
|
||||
// ==================== TC-CUS-10: Präferenzen setzen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-10: Präferenzen BIO und REGIONAL setzen → beide sichtbar")
|
||||
void setPreferences_bioAndRegional_appearsInDetail() throws Exception {
|
||||
String customerId = createB2cCustomer("Max Mustermann");
|
||||
|
||||
var prefs = new SetPreferencesRequest(Set.of(CustomerPreference.BIO, CustomerPreference.REGIONAL));
|
||||
|
||||
mockMvc.perform(put("/api/customers/{id}/preferences", customerId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(prefs)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.preferences", hasSize(2)))
|
||||
.andExpect(jsonPath("$.preferences", containsInAnyOrder("BIO", "REGIONAL")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-10: Präferenzen überschreiben → nur neue Präferenz gesetzt")
|
||||
void setPreferences_replacesPreviousPreferences() throws Exception {
|
||||
String customerId = createB2cCustomer("Max Mustermann");
|
||||
|
||||
var firstPrefs = new SetPreferencesRequest(Set.of(CustomerPreference.BIO, CustomerPreference.REGIONAL));
|
||||
mockMvc.perform(put("/api/customers/{id}/preferences", customerId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(firstPrefs))).andReturn();
|
||||
|
||||
var secondPrefs = new SetPreferencesRequest(Set.of(CustomerPreference.HALAL));
|
||||
mockMvc.perform(put("/api/customers/{id}/preferences", customerId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(secondPrefs)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.preferences", hasSize(1)))
|
||||
.andExpect(jsonPath("$.preferences[0]").value("HALAL"));
|
||||
}
|
||||
|
||||
// ==================== TC-CUS-11: Alle Präferenzen abwählen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CUS-11: Alle Präferenzen abwählen → leere Liste")
|
||||
void setPreferences_empty_clearsPreferences() throws Exception {
|
||||
String customerId = createB2cCustomer("Max Mustermann");
|
||||
|
||||
var prefs = new SetPreferencesRequest(Set.of(CustomerPreference.BIO));
|
||||
mockMvc.perform(put("/api/customers/{id}/preferences", customerId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(prefs))).andReturn();
|
||||
|
||||
var emptyPrefs = new SetPreferencesRequest(Set.of());
|
||||
mockMvc.perform(put("/api/customers/{id}/preferences", customerId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(emptyPrefs)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.preferences", hasSize(0)));
|
||||
}
|
||||
|
||||
// ==================== TC-B2B-02: Rahmenvertrag für B2C-Kunden ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-B2B-02: Rahmenvertrag für B2C-Kunden → 400/422 FrameContractNotAllowed")
|
||||
void setFrameContract_forB2cCustomer_returns4xx() throws Exception {
|
||||
String b2cId = createB2cCustomer("Privatkunde");
|
||||
|
||||
String articleId = createArticle();
|
||||
|
||||
var contract = new SetFrameContractRequest(
|
||||
LocalDate.of(2025, 1, 1), LocalDate.of(2025, 12, 31),
|
||||
DeliveryRhythm.WEEKLY,
|
||||
List.of(new SetFrameContractRequest.LineItem(
|
||||
articleId, new BigDecimal("1.50"), new BigDecimal("100"), Unit.KG)));
|
||||
|
||||
mockMvc.perform(put("/api/customers/{id}/frame-contract", b2cId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(contract)))
|
||||
.andExpect(status().is4xxClientError());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-B2B-01: Rahmenvertrag für B2B-Kunden → 200")
|
||||
void setFrameContract_forB2bCustomer_returns200() throws Exception {
|
||||
String b2bId = createB2bCustomer("Gastro GmbH");
|
||||
String articleId = createArticle();
|
||||
|
||||
var contract = new SetFrameContractRequest(
|
||||
LocalDate.of(2025, 1, 1), LocalDate.of(2025, 12, 31),
|
||||
DeliveryRhythm.WEEKLY,
|
||||
List.of(new SetFrameContractRequest.LineItem(
|
||||
articleId, new BigDecimal("1.50"), new BigDecimal("100"), Unit.KG)));
|
||||
|
||||
mockMvc.perform(put("/api/customers/{id}/frame-contract", b2bId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(contract)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.frameContract").isNotEmpty())
|
||||
.andExpect(jsonPath("$.frameContract.deliveryRhythm").value("WEEKLY"));
|
||||
}
|
||||
|
||||
// ==================== TC-AUTH ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-AUTH-02: Kunde erstellen ohne MASTERDATA_WRITE → 403")
|
||||
void createCustomer_withViewerToken_returns403() throws Exception {
|
||||
var request = new CreateCustomerRequest(
|
||||
"Kein Zugriff", CustomerType.B2C,
|
||||
"Str. 1", null, "10115", "Berlin", "DE",
|
||||
"+49 30 0", null, null, null, null);
|
||||
|
||||
mockMvc.perform(post("/api/customers")
|
||||
.header("Authorization", "Bearer " + viewerToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-AUTH-01: Kundenliste ohne MASTERDATA_WRITE → 200")
|
||||
void listCustomers_withViewerToken_returns200() throws Exception {
|
||||
mockMvc.perform(get("/api/customers")
|
||||
.header("Authorization", "Bearer " + viewerToken))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Nicht existierenden Kunden abfragen → 404")
|
||||
void getCustomer_nonExistent_returns404() throws Exception {
|
||||
mockMvc.perform(get("/api/customers/{id}", UUID.randomUUID())
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
// ==================== Hilfsmethoden ====================
|
||||
|
||||
private String createB2cCustomer(String name) throws Exception {
|
||||
var request = new CreateCustomerRequest(
|
||||
name, CustomerType.B2C,
|
||||
"Musterstr. 1", "2", "10115", "Berlin", "DE",
|
||||
"+49 176 12345", null, null, null, null);
|
||||
MvcResult result = mockMvc.perform(post("/api/customers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
}
|
||||
|
||||
private String createB2bCustomer(String name) throws Exception {
|
||||
var request = new CreateCustomerRequest(
|
||||
name, CustomerType.B2B,
|
||||
"Geschäftsstr. 1", "1", "10178", "Berlin", "DE",
|
||||
"+49 30 9876", null, null, null, null);
|
||||
MvcResult result = mockMvc.perform(post("/api/customers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
}
|
||||
|
||||
/** Erstellt Hilfskategorie und Artikel für Rahmenvertrags-Tests. */
|
||||
private String createArticle() throws Exception {
|
||||
var catRequest = new CreateProductCategoryRequest("Testkat", null);
|
||||
MvcResult catResult = mockMvc.perform(post("/api/categories")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(catRequest)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String catId = objectMapper.readTree(catResult.getResponse().getContentAsString()).get("id").asText();
|
||||
|
||||
var artRequest = new CreateArticleRequest(
|
||||
"Testartikel", "TA-001", catId,
|
||||
Unit.KG, PriceModel.WEIGHT_BASED, new BigDecimal("2.00"));
|
||||
MvcResult artResult = mockMvc.perform(post("/api/articles")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(artRequest)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
return objectMapper.readTree(artResult.getResponse().getContentAsString()).get("id").asText();
|
||||
}
|
||||
|
||||
private String generateToken(String userId, String username, String permissions) {
|
||||
long now = System.currentTimeMillis();
|
||||
javax.crypto.SecretKey key = io.jsonwebtoken.security.Keys.hmacShaKeyFor(
|
||||
jwtSecret.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
return Jwts.builder()
|
||||
.subject(userId)
|
||||
.claim("username", username)
|
||||
.claim("permissions", permissions)
|
||||
.issuedAt(new Date(now))
|
||||
.expiration(new Date(now + jwtExpiration))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,291 @@
|
|||
package de.effigenix.infrastructure.masterdata.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.effigenix.domain.usermanagement.UserStatus;
|
||||
import de.effigenix.infrastructure.masterdata.web.dto.CreateProductCategoryRequest;
|
||||
import de.effigenix.infrastructure.masterdata.web.dto.UpdateProductCategoryRequest;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.repository.RoleJpaRepository;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.repository.UserJpaRepository;
|
||||
import de.effigenix.domain.usermanagement.RoleName;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* Integrationstests für ProductCategoryController.
|
||||
*
|
||||
* Abgedeckte Testfälle: TC-CAT-01 bis TC-CAT-06
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
@DisplayName("ProductCategory Controller Integration Tests")
|
||||
class ProductCategoryControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private UserJpaRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private RoleJpaRepository roleRepository;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String jwtSecret;
|
||||
|
||||
@Value("${jwt.expiration}")
|
||||
private long jwtExpiration;
|
||||
|
||||
private String adminToken;
|
||||
private String viewerToken;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RoleEntity adminRole = new RoleEntity(
|
||||
UUID.randomUUID().toString(), RoleName.ADMIN, Set.of(), "Admin");
|
||||
roleRepository.save(adminRole);
|
||||
|
||||
RoleEntity viewerRole = new RoleEntity(
|
||||
UUID.randomUUID().toString(), RoleName.PRODUCTION_WORKER, Set.of(), "Viewer");
|
||||
roleRepository.save(viewerRole);
|
||||
|
||||
String adminId = UUID.randomUUID().toString();
|
||||
userRepository.save(new UserEntity(
|
||||
adminId, "cat.admin", "cat.admin@test.com",
|
||||
passwordEncoder.encode("Pass123"), Set.of(adminRole),
|
||||
"BRANCH-01", UserStatus.ACTIVE, LocalDateTime.now(), null));
|
||||
|
||||
String viewerId = UUID.randomUUID().toString();
|
||||
userRepository.save(new UserEntity(
|
||||
viewerId, "cat.viewer", "cat.viewer@test.com",
|
||||
passwordEncoder.encode("Pass123"), Set.of(viewerRole),
|
||||
"BRANCH-01", UserStatus.ACTIVE, LocalDateTime.now(), null));
|
||||
|
||||
adminToken = generateToken(adminId, "cat.admin", "MASTERDATA_WRITE");
|
||||
viewerToken = generateToken(viewerId, "cat.viewer", "USER_READ");
|
||||
}
|
||||
|
||||
// ==================== TC-CAT-01: Kategorie erstellen (Happy Path) ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CAT-01: Kategorie mit Name und Beschreibung erstellen → 201")
|
||||
void createCategory_withNameAndDescription_returns201() throws Exception {
|
||||
var request = new CreateProductCategoryRequest("Obst & Gemüse", "Frische Produkte");
|
||||
|
||||
mockMvc.perform(post("/api/categories")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").isNotEmpty())
|
||||
.andExpect(jsonPath("$.name").value("Obst & Gemüse"))
|
||||
.andExpect(jsonPath("$.description").value("Frische Produkte"));
|
||||
}
|
||||
|
||||
// ==================== TC-CAT-02: Kategorie ohne Beschreibung ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CAT-02: Kategorie ohne Beschreibung erstellen → 201")
|
||||
void createCategory_withoutDescription_returns201() throws Exception {
|
||||
var request = new CreateProductCategoryRequest("Milchprodukte", null);
|
||||
|
||||
mockMvc.perform(post("/api/categories")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.name").value("Milchprodukte"))
|
||||
.andExpect(jsonPath("$.description").doesNotExist());
|
||||
}
|
||||
|
||||
// ==================== TC-CAT-03: Kategorie bearbeiten ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CAT-03: Kategorie bearbeiten → 200 mit aktualisierten Daten")
|
||||
void updateCategory_withValidData_returns200() throws Exception {
|
||||
String categoryId = createCategory("Obst & Gemüse", "Alt");
|
||||
|
||||
var update = new UpdateProductCategoryRequest("Obst und Gemüse", "Saisonale Frische");
|
||||
|
||||
mockMvc.perform(put("/api/categories/{id}", categoryId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(update)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.name").value("Obst und Gemüse"))
|
||||
.andExpect(jsonPath("$.description").value("Saisonale Frische"));
|
||||
}
|
||||
|
||||
// ==================== TC-CAT-04: Doppelter Name wird abgelehnt ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CAT-04: Doppelter Kategoriename → 409 Conflict")
|
||||
void createCategory_withDuplicateName_returns409() throws Exception {
|
||||
createCategory("Milchprodukte", null);
|
||||
|
||||
var request = new CreateProductCategoryRequest("Milchprodukte", null);
|
||||
|
||||
mockMvc.perform(post("/api/categories")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
|
||||
// ==================== TC-CAT-05: Kategorie löschen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CAT-05: Kategorie löschen → 204 und nicht mehr in Gesamtliste")
|
||||
void deleteCategory_existingCategory_returns204() throws Exception {
|
||||
String categoryId = createCategory("Löschtest", null);
|
||||
|
||||
mockMvc.perform(delete("/api/categories/{id}", categoryId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
// Verifizierung über Liste – kein GET-by-ID-Endpoint vorhanden
|
||||
mockMvc.perform(get("/api/categories")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[?(@.id == '" + categoryId + "')]").isEmpty());
|
||||
}
|
||||
|
||||
// ==================== TC-CAT-06: Leerer Name wird abgelehnt ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CAT-06: Leerer Name → 400 Bad Request")
|
||||
void createCategory_withBlankName_returns400() throws Exception {
|
||||
var request = new CreateProductCategoryRequest("", "Beschreibung");
|
||||
|
||||
mockMvc.perform(post("/api/categories")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-CAT-06: Null-Name → 400 Bad Request")
|
||||
void createCategory_withNullName_returns400() throws Exception {
|
||||
mockMvc.perform(post("/api/categories")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
// ==================== Liste ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Kategorienliste abfragen → 200 mit Array")
|
||||
void listCategories_returns200() throws Exception {
|
||||
createCategory("Fleisch", null);
|
||||
createCategory("Wurst", null);
|
||||
|
||||
mockMvc.perform(get("/api/categories")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", hasSize(greaterThanOrEqualTo(2))));
|
||||
}
|
||||
|
||||
// ==================== TC-AUTH: Autorisierung ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-AUTH-01: Kategorienliste ohne MASTERDATA_WRITE → 200 (Lesezugriff)")
|
||||
void listCategories_withViewerToken_returns200() throws Exception {
|
||||
mockMvc.perform(get("/api/categories")
|
||||
.header("Authorization", "Bearer " + viewerToken))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-AUTH-02: Kategorie erstellen ohne MASTERDATA_WRITE → 403")
|
||||
void createCategory_withViewerToken_returns403() throws Exception {
|
||||
var request = new CreateProductCategoryRequest("Getränke", null);
|
||||
|
||||
mockMvc.perform(post("/api/categories")
|
||||
.header("Authorization", "Bearer " + viewerToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Kategorie erstellen ohne Token → 401")
|
||||
void createCategory_withoutToken_returns401() throws Exception {
|
||||
var request = new CreateProductCategoryRequest("Getränke", null);
|
||||
|
||||
mockMvc.perform(post("/api/categories")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Nicht existierende Kategorie aktualisieren → 404")
|
||||
void updateCategory_nonExistent_returns404() throws Exception {
|
||||
var update = new UpdateProductCategoryRequest("Neuer Name", null);
|
||||
mockMvc.perform(put("/api/categories/{id}", UUID.randomUUID())
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(update)))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
// ==================== Hilfsmethoden ====================
|
||||
|
||||
private String createCategory(String name, String description) throws Exception {
|
||||
var request = new CreateProductCategoryRequest(name, description);
|
||||
MvcResult result = mockMvc.perform(post("/api/categories")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
}
|
||||
|
||||
private String generateToken(String userId, String username, String permissions) {
|
||||
long now = System.currentTimeMillis();
|
||||
javax.crypto.SecretKey key = io.jsonwebtoken.security.Keys.hmacShaKeyFor(
|
||||
jwtSecret.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
return Jwts.builder()
|
||||
.subject(userId)
|
||||
.claim("username", username)
|
||||
.claim("permissions", permissions)
|
||||
.issuedAt(new Date(now))
|
||||
.expiration(new Date(now + jwtExpiration))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,466 @@
|
|||
package de.effigenix.infrastructure.masterdata.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.effigenix.domain.usermanagement.RoleName;
|
||||
import de.effigenix.domain.usermanagement.UserStatus;
|
||||
import de.effigenix.infrastructure.masterdata.web.dto.*;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.repository.RoleJpaRepository;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.repository.UserJpaRepository;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* Integrationstests für SupplierController.
|
||||
*
|
||||
* Abgedeckte Testfälle: TC-SUP-01 bis TC-SUP-12, TC-AUTH-01/02
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
@DisplayName("Supplier Controller Integration Tests")
|
||||
class SupplierControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private UserJpaRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private RoleJpaRepository roleRepository;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String jwtSecret;
|
||||
|
||||
@Value("${jwt.expiration}")
|
||||
private long jwtExpiration;
|
||||
|
||||
private String adminToken;
|
||||
private String viewerToken;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RoleEntity adminRole = new RoleEntity(
|
||||
UUID.randomUUID().toString(), RoleName.ADMIN, Set.of(), "Admin");
|
||||
roleRepository.save(adminRole);
|
||||
|
||||
RoleEntity viewerRole = new RoleEntity(
|
||||
UUID.randomUUID().toString(), RoleName.PRODUCTION_WORKER, Set.of(), "Viewer");
|
||||
roleRepository.save(viewerRole);
|
||||
|
||||
String adminId = UUID.randomUUID().toString();
|
||||
userRepository.save(new UserEntity(
|
||||
adminId, "sup.admin", "sup.admin@test.com",
|
||||
passwordEncoder.encode("Pass123"), Set.of(adminRole),
|
||||
"BRANCH-01", UserStatus.ACTIVE, LocalDateTime.now(), null));
|
||||
|
||||
String viewerId = UUID.randomUUID().toString();
|
||||
userRepository.save(new UserEntity(
|
||||
viewerId, "sup.viewer", "sup.viewer@test.com",
|
||||
passwordEncoder.encode("Pass123"), Set.of(viewerRole),
|
||||
"BRANCH-01", UserStatus.ACTIVE, LocalDateTime.now(), null));
|
||||
|
||||
adminToken = generateToken(adminId, "sup.admin", "MASTERDATA_WRITE");
|
||||
viewerToken = generateToken(viewerId, "sup.viewer", "USER_READ");
|
||||
}
|
||||
|
||||
// ==================== TC-SUP-01: Lieferant erstellen – Pflichtfelder ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-01: Lieferant mit Pflichtfeldern erstellen → 201, Status ACTIVE")
|
||||
void createSupplier_withRequiredFields_returns201() throws Exception {
|
||||
var request = new CreateSupplierRequest(
|
||||
"Frisch AG", "+49 30 12345",
|
||||
null, null, null, null, null, null, null, null, null);
|
||||
|
||||
mockMvc.perform(post("/api/suppliers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").isNotEmpty())
|
||||
.andExpect(jsonPath("$.name").value("Frisch AG"))
|
||||
.andExpect(jsonPath("$.status").value("ACTIVE"));
|
||||
}
|
||||
|
||||
// ==================== TC-SUP-02: Lieferant erstellen – alle Felder ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-02: Lieferant mit allen Feldern erstellen → alle Daten korrekt")
|
||||
void createSupplier_withAllFields_returnsCompleteData() throws Exception {
|
||||
var request = new CreateSupplierRequest(
|
||||
"Bio GmbH", "+49 89 999",
|
||||
"bio@example.com", "Max Muster",
|
||||
"Gartenstraße 5", "12", "80333", "München", "DE",
|
||||
30, "30 Tage netto");
|
||||
|
||||
mockMvc.perform(post("/api/suppliers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.name").value("Bio GmbH"))
|
||||
.andExpect(jsonPath("$.contactInfo.email").value("bio@example.com"))
|
||||
.andExpect(jsonPath("$.contactInfo.contactPerson").value("Max Muster"))
|
||||
.andExpect(jsonPath("$.address.city").value("München"))
|
||||
.andExpect(jsonPath("$.paymentTerms.paymentDueDays").value(30));
|
||||
}
|
||||
|
||||
// ==================== TC-SUP-03: Pflichtfelder fehlen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-03: Lieferant ohne Namen → 400")
|
||||
void createSupplier_withBlankName_returns400() throws Exception {
|
||||
var request = new CreateSupplierRequest(
|
||||
"", "+49 30 12345",
|
||||
null, null, null, null, null, null, null, null, null);
|
||||
|
||||
mockMvc.perform(post("/api/suppliers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-03: Lieferant ohne Telefon → 400")
|
||||
void createSupplier_withBlankPhone_returns400() throws Exception {
|
||||
var request = new CreateSupplierRequest(
|
||||
"Kein Telefon AG", "",
|
||||
null, null, null, null, null, null, null, null, null);
|
||||
|
||||
mockMvc.perform(post("/api/suppliers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
// ==================== TC-SUP-04: Doppelter Name ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-04: Lieferant mit doppeltem Namen → 409 Conflict")
|
||||
void createSupplier_withDuplicateName_returns409() throws Exception {
|
||||
createSupplier("Frisch AG");
|
||||
|
||||
var duplicate = new CreateSupplierRequest(
|
||||
"Frisch AG", "+49 30 99999",
|
||||
null, null, null, null, null, null, null, null, null);
|
||||
|
||||
mockMvc.perform(post("/api/suppliers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(duplicate)))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
|
||||
// ==================== TC-SUP-05: Deaktivieren und Aktivieren ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-05: Lieferant deaktivieren → Status INACTIVE")
|
||||
void deactivateSupplier_activeSupplier_returnsInactive() throws Exception {
|
||||
String supplierId = createSupplier("Frisch AG");
|
||||
|
||||
mockMvc.perform(post("/api/suppliers/{id}/deactivate", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("INACTIVE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-05: Lieferant aktivieren → Status ACTIVE")
|
||||
void activateSupplier_inactiveSupplier_returnsActive() throws Exception {
|
||||
String supplierId = createSupplier("Frisch AG");
|
||||
|
||||
mockMvc.perform(post("/api/suppliers/{id}/deactivate", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mockMvc.perform(post("/api/suppliers/{id}/activate", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("ACTIVE"));
|
||||
}
|
||||
|
||||
// ==================== TC-SUP-06: Filtern ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-06: Nur aktive Lieferanten filtern → nur ACTIVE in Ergebnis")
|
||||
void listSuppliers_filterByActive_returnsOnlyActive() throws Exception {
|
||||
String supplierId = createSupplier("Wird inaktiv AG");
|
||||
mockMvc.perform(post("/api/suppliers/{id}/deactivate", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken)).andReturn();
|
||||
|
||||
createSupplier("Bleibt aktiv GmbH");
|
||||
|
||||
mockMvc.perform(get("/api/suppliers")
|
||||
.param("status", "ACTIVE")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[*].status", everyItem(is("ACTIVE"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-06: Nur inaktive Lieferanten filtern → nur INACTIVE in Ergebnis")
|
||||
void listSuppliers_filterByInactive_returnsOnlyInactive() throws Exception {
|
||||
String supplierId = createSupplier("Inaktiv AG");
|
||||
mockMvc.perform(post("/api/suppliers/{id}/deactivate", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken)).andReturn();
|
||||
|
||||
mockMvc.perform(get("/api/suppliers")
|
||||
.param("status", "INACTIVE")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[*].status", everyItem(is("INACTIVE"))));
|
||||
}
|
||||
|
||||
// ==================== TC-SUP-07: Lieferant bewerten ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-07: Lieferant bewerten → Bewertung und Durchschnitt korrekt")
|
||||
void rateSupplier_validScores_returnsBewertungWithAverage() throws Exception {
|
||||
String supplierId = createSupplier("Frisch AG");
|
||||
|
||||
var rating = new RateSupplierRequest(4, 3, 5);
|
||||
|
||||
mockMvc.perform(post("/api/suppliers/{id}/rating", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(rating)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.rating.qualityScore").value(4))
|
||||
.andExpect(jsonPath("$.rating.deliveryScore").value(3))
|
||||
.andExpect(jsonPath("$.rating.priceScore").value(5));
|
||||
}
|
||||
|
||||
// ==================== TC-SUP-08: Bewertungsgrenzen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-08: Bewertung mit Score 0 → 400 (unter Minimum)")
|
||||
void rateSupplier_scoreBelowMin_returns400() throws Exception {
|
||||
String supplierId = createSupplier("Frisch AG");
|
||||
|
||||
var rating = new RateSupplierRequest(0, 3, 3);
|
||||
|
||||
mockMvc.perform(post("/api/suppliers/{id}/rating", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(rating)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-08: Bewertung mit Score 6 → 400 (über Maximum)")
|
||||
void rateSupplier_scoreAboveMax_returns400() throws Exception {
|
||||
String supplierId = createSupplier("Frisch AG");
|
||||
|
||||
var rating = new RateSupplierRequest(6, 3, 3);
|
||||
|
||||
mockMvc.perform(post("/api/suppliers/{id}/rating", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(rating)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-08: Bewertung mit Grenzwerten 1 und 5 → erfolgreich")
|
||||
void rateSupplier_boundaryScores_succeeds() throws Exception {
|
||||
String supplierId = createSupplier("Frisch AG");
|
||||
|
||||
var rating = new RateSupplierRequest(1, 5, 1);
|
||||
|
||||
mockMvc.perform(post("/api/suppliers/{id}/rating", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(rating)))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
// ==================== TC-SUP-09: Zertifikat hinzufügen (gültig) ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-09: Gültiges Zertifikat hinzufügen → erscheint in Detailansicht")
|
||||
void addCertificate_validCert_appearsInDetail() throws Exception {
|
||||
String supplierId = createSupplier("Frisch AG");
|
||||
|
||||
var cert = new AddCertificateRequest(
|
||||
"ISO9001", "TÜV",
|
||||
LocalDate.of(2024, 1, 1), LocalDate.of(2027, 1, 1));
|
||||
|
||||
mockMvc.perform(post("/api/suppliers/{id}/certificates", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(cert)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.certificates", hasSize(1)))
|
||||
.andExpect(jsonPath("$.certificates[0].certificateType").value("ISO9001"))
|
||||
.andExpect(jsonPath("$.certificates[0].issuer").value("TÜV"));
|
||||
}
|
||||
|
||||
// ==================== TC-SUP-10: Abgelaufenes Zertifikat ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-10: Abgelaufenes Zertifikat hinzufügen → wird angelegt (kein Blockieren)")
|
||||
void addCertificate_expiredCert_isAccepted() throws Exception {
|
||||
String supplierId = createSupplier("Frisch AG");
|
||||
|
||||
var cert = new AddCertificateRequest(
|
||||
"ISO14001", "TÜV",
|
||||
LocalDate.of(2020, 1, 1), LocalDate.of(2023, 12, 31));
|
||||
|
||||
mockMvc.perform(post("/api/suppliers/{id}/certificates", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(cert)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.certificates", hasSize(1)));
|
||||
}
|
||||
|
||||
// ==================== TC-SUP-11: Doppeltes Zertifikat ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-11: Doppeltes Zertifikat wird idempotent ignoriert (kein Duplikat in Liste)")
|
||||
void addCertificate_duplicate_isIdempotent() throws Exception {
|
||||
String supplierId = createSupplier("Frisch AG");
|
||||
|
||||
var cert = new AddCertificateRequest(
|
||||
"ISO9001", "TÜV",
|
||||
LocalDate.of(2024, 1, 1), LocalDate.of(2027, 1, 1));
|
||||
|
||||
mockMvc.perform(post("/api/suppliers/{id}/certificates", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(cert)))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
// Zweiter Aufruf mit identischen Daten: Domain ignoriert Duplikate still
|
||||
mockMvc.perform(post("/api/suppliers/{id}/certificates", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(cert)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.certificates", hasSize(1)));
|
||||
}
|
||||
|
||||
// ==================== TC-SUP-12: Zertifikat entfernen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-SUP-12: Zertifikat entfernen → verschwindet aus Detailansicht")
|
||||
void removeCertificate_existingCert_removedFromDetail() throws Exception {
|
||||
String supplierId = createSupplier("Frisch AG");
|
||||
|
||||
var cert = new AddCertificateRequest(
|
||||
"ISO9001", "TÜV",
|
||||
LocalDate.of(2024, 1, 1), LocalDate.of(2027, 1, 1));
|
||||
mockMvc.perform(post("/api/suppliers/{id}/certificates", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(cert))).andReturn();
|
||||
|
||||
var removeRequest = new RemoveCertificateRequest("ISO9001", "TÜV", LocalDate.of(2024, 1, 1));
|
||||
|
||||
mockMvc.perform(delete("/api/suppliers/{id}/certificates", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(removeRequest)))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
mockMvc.perform(get("/api/suppliers/{id}", supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.certificates", hasSize(0)));
|
||||
}
|
||||
|
||||
// ==================== Allgemein ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Nicht existierenden Lieferanten abfragen → 404")
|
||||
void getSupplier_nonExistent_returns404() throws Exception {
|
||||
mockMvc.perform(get("/api/suppliers/{id}", UUID.randomUUID())
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-AUTH-02: Lieferant erstellen ohne MASTERDATA_WRITE → 403")
|
||||
void createSupplier_withViewerToken_returns403() throws Exception {
|
||||
var request = new CreateSupplierRequest(
|
||||
"Kein Zugriff AG", "+49 30 0",
|
||||
null, null, null, null, null, null, null, null, null);
|
||||
|
||||
mockMvc.perform(post("/api/suppliers")
|
||||
.header("Authorization", "Bearer " + viewerToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-AUTH-01: Lieferantenliste ohne MASTERDATA_WRITE → 200")
|
||||
void listSuppliers_withViewerToken_returns200() throws Exception {
|
||||
mockMvc.perform(get("/api/suppliers")
|
||||
.header("Authorization", "Bearer " + viewerToken))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
// ==================== Hilfsmethoden ====================
|
||||
|
||||
private String createSupplier(String name) throws Exception {
|
||||
var request = new CreateSupplierRequest(
|
||||
name, "+49 30 12345",
|
||||
null, null, null, null, null, null, null, null, null);
|
||||
MvcResult result = mockMvc.perform(post("/api/suppliers")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
}
|
||||
|
||||
private String generateToken(String userId, String username, String permissions) {
|
||||
long now = System.currentTimeMillis();
|
||||
javax.crypto.SecretKey key = io.jsonwebtoken.security.Keys.hmacShaKeyFor(
|
||||
jwtSecret.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
return Jwts.builder()
|
||||
.subject(userId)
|
||||
.claim("username", username)
|
||||
.claim("permissions", permissions)
|
||||
.issuedAt(new Date(now))
|
||||
.expiration(new Date(now + jwtExpiration))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue