mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 13:49:36 +01:00
test: Blackbox-Integrationstests für dokumentierte Domain-Invarianten
Neue Testdateien: - RecipeLifecycleIntegrationTest (7 Tests): Aktivierung ohne Zutaten, Modifikation aktiver Rezepte, Archivierung, Duplikat-Prüfungen - RoleControllerIntegrationTest (3 Tests): GET /api/roles mit/ohne Berechtigung und ohne Token Erweiterte Testdateien: - BatchControllerIntegrationTest (+2): start/cancel von COMPLETED - StockControllerIntegrationTest (+3): removeBatch und FEFO-Allokation - StorageLocationControllerIntegrationTest (+2): active=false Filter - ArticleControllerIntegrationTest (+4): updatePrice, removeSupplier, filterByCategory, updateArticle - CustomerControllerIntegrationTest (+3): removeFrameContract, updateCustomer, duplicateLineItems
This commit is contained in:
parent
11bda32ffc
commit
d7fcc946e7
7 changed files with 694 additions and 4 deletions
|
|
@ -727,6 +727,94 @@ class StockControllerIntegrationTest extends AbstractIntegrationTest {
|
|||
}
|
||||
}
|
||||
|
||||
// ==================== Charge entnehmen (removeBatch) ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("POST /{stockId}/batches/{batchId}/remove – Charge entnehmen")
|
||||
class RemoveBatch {
|
||||
|
||||
@Test
|
||||
@DisplayName("Teilentnahme → 200, Restmenge korrekt")
|
||||
void removeBatch_partialQuantity_returns200() throws Exception {
|
||||
String stockId = createStock();
|
||||
String batchId = addBatchToStock(stockId); // 10 KILOGRAM
|
||||
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/remove", stockId, batchId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"quantityAmount": "3", "quantityUnit": "KILOGRAM"}
|
||||
"""))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// Restmenge prüfen
|
||||
mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.totalQuantity").value(7))
|
||||
.andExpect(jsonPath("$.availableQuantity").value(7));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Entnahme über verfügbare Menge → 409")
|
||||
void removeBatch_moreThanAvailable_returns409() throws Exception {
|
||||
String stockId = createStock();
|
||||
String batchId = addBatchToStock(stockId); // 10 KILOGRAM
|
||||
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/remove", stockId, batchId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"quantityAmount": "999", "quantityUnit": "KILOGRAM"}
|
||||
"""))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== FEFO-Reservierung ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("FEFO-Allokation bei Reservierung")
|
||||
class FefoReservation {
|
||||
|
||||
@Test
|
||||
@DisplayName("Reservierung allokiert Charge mit frühestem Ablaufdatum zuerst (FEFO)")
|
||||
void reserve_fefoOrder_allocatesEarliestFirst() throws Exception {
|
||||
String stockId = createStock();
|
||||
|
||||
// Charge mit spätem Ablaufdatum zuerst einbuchen
|
||||
var lateBatch = new AddStockBatchRequest(
|
||||
"BATCH-LATE", "PRODUCED", "10", "KILOGRAM", "2027-12-31");
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", stockId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(lateBatch)))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
// Charge mit frühem Ablaufdatum danach einbuchen
|
||||
var earlyBatch = new AddStockBatchRequest(
|
||||
"BATCH-EARLY", "PRODUCED", "10", "KILOGRAM", "2026-06-30");
|
||||
var earlyResult = mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", stockId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(earlyBatch)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String earlyBatchId = objectMapper.readTree(earlyResult.getResponse().getContentAsString()).get("id").asText();
|
||||
|
||||
// Reservierung: 5 kg – soll aus BATCH-EARLY (früheres Ablaufdatum) allokiert werden
|
||||
var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-FEFO", "5", "KILOGRAM", "NORMAL");
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.allocations.length()").value(1))
|
||||
.andExpect(jsonPath("$.allocations[0].stockBatchId").value(earlyBatchId))
|
||||
.andExpect(jsonPath("$.allocations[0].allocatedQuantityAmount").value(5));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Bestandsposition abfragen (getStock) ====================
|
||||
|
||||
@Nested
|
||||
|
|
|
|||
|
|
@ -525,8 +525,8 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Lagerorte nach active filtern → 200")
|
||||
void listStorageLocations_filterByActive() throws Exception {
|
||||
@DisplayName("Lagerorte nach active=true filtern → 200 nur aktive")
|
||||
void listStorageLocations_filterByActiveTrue() throws Exception {
|
||||
String activeId = createAndReturnId("Aktiv", "DRY_STORAGE", null, null);
|
||||
String inactiveId = createAndReturnId("Inaktiv", "COLD_ROOM", null, null);
|
||||
|
||||
|
|
@ -544,8 +544,26 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Lagerorte mit storageType und active filtern → 200")
|
||||
void listStorageLocations_filterByStorageTypeAndActive() throws Exception {
|
||||
@DisplayName("Lagerorte nach active=false filtern → 200 nur inaktive")
|
||||
void listStorageLocations_filterByActiveFalse() throws Exception {
|
||||
createAndReturnId("Aktiv", "DRY_STORAGE", null, null);
|
||||
String inactiveId = createAndReturnId("Inaktiv", "COLD_ROOM", null, null);
|
||||
|
||||
mockMvc.perform(patch("/api/inventory/storage-locations/" + inactiveId + "/deactivate")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mockMvc.perform(get("/api/inventory/storage-locations")
|
||||
.param("active", "false")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", hasSize(1)))
|
||||
.andExpect(jsonPath("$[0].name").value("Inaktiv"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Lagerorte mit storageType und active=true filtern → 200")
|
||||
void listStorageLocations_filterByStorageTypeAndActiveTrue() throws Exception {
|
||||
createAndReturnId("Kühl Aktiv", "COLD_ROOM", null, null);
|
||||
String inactiveId = createAndReturnId("Kühl Inaktiv", "COLD_ROOM", null, null);
|
||||
createAndReturnId("Trocken Aktiv", "DRY_STORAGE", null, null);
|
||||
|
|
@ -563,6 +581,26 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest {
|
|||
.andExpect(jsonPath("$[0].name").value("Kühl Aktiv"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Lagerorte mit storageType und active=false filtern → 200 nur inaktive des Typs")
|
||||
void listStorageLocations_filterByStorageTypeAndActiveFalse() throws Exception {
|
||||
createAndReturnId("Kühl Aktiv", "COLD_ROOM", null, null);
|
||||
String inactiveId = createAndReturnId("Kühl Inaktiv", "COLD_ROOM", null, null);
|
||||
createAndReturnId("Trocken Aktiv", "DRY_STORAGE", null, null);
|
||||
|
||||
mockMvc.perform(patch("/api/inventory/storage-locations/" + inactiveId + "/deactivate")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mockMvc.perform(get("/api/inventory/storage-locations")
|
||||
.param("storageType", "COLD_ROOM")
|
||||
.param("active", "false")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", hasSize(1)))
|
||||
.andExpect(jsonPath("$[0].name").value("Kühl Inaktiv"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Lagerorte mit ungültigem StorageType filtern → 400")
|
||||
void listStorageLocations_invalidStorageType_returns400() throws Exception {
|
||||
|
|
|
|||
|
|
@ -324,6 +324,91 @@ class ArticleControllerIntegrationTest extends AbstractIntegrationTest {
|
|||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
// ==================== Verkaufseinheit-Preis aktualisieren ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Verkaufseinheit-Preis aktualisieren → 200")
|
||||
void updateSalesUnitPrice_returns200() throws Exception {
|
||||
String articleId = createArticle("Preis-Test", "PT-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
|
||||
|
||||
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();
|
||||
|
||||
var priceRequest = new UpdateSalesUnitPriceRequest(new BigDecimal("3.49"));
|
||||
|
||||
mockMvc.perform(put("/api/articles/{id}/sales-units/{suId}/price", articleId, salesUnitId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(priceRequest)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.salesUnits[0].price").value(3.49));
|
||||
}
|
||||
|
||||
// ==================== Lieferant entfernen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Lieferant vom Artikel entfernen → 204")
|
||||
void removeSupplier_returns204() throws Exception {
|
||||
String articleId = createArticle("Supplier-Test", "ST-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
|
||||
String supplierId = createSupplier("Entfern AG");
|
||||
|
||||
// Zuweisen
|
||||
mockMvc.perform(post("/api/articles/{id}/suppliers", articleId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(new AssignSupplierRequest(supplierId))))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// Entfernen
|
||||
mockMvc.perform(delete("/api/articles/{id}/suppliers/{supplierId}", articleId, supplierId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
// Prüfen
|
||||
mockMvc.perform(get("/api/articles/{id}", articleId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.supplierIds", hasSize(0)));
|
||||
}
|
||||
|
||||
// ==================== Artikel nach Kategorie filtern ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Artikel nach categoryId filtern → nur passende zurückgegeben")
|
||||
void filterByCategory_returnsOnlyMatching() throws Exception {
|
||||
String otherCategoryId = createCategory("Milchprodukte");
|
||||
createArticle("Äpfel", "FI-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
|
||||
createArticleInCategory("Milch", "MI-001", Unit.KG, PriceModel.WEIGHT_BASED, "1.29", otherCategoryId);
|
||||
|
||||
mockMvc.perform(get("/api/articles")
|
||||
.param("categoryId", otherCategoryId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", hasSize(1)))
|
||||
.andExpect(jsonPath("$[0].name").value("Milch"));
|
||||
}
|
||||
|
||||
// ==================== Artikel aktualisieren ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Artikel Name und Kategorie ändern → 200")
|
||||
void updateArticle_returns200() throws Exception {
|
||||
String articleId = createArticle("Alt Name", "UP-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
|
||||
String newCategoryId = createCategory("Neue Kategorie");
|
||||
|
||||
var request = new UpdateArticleRequest("Neuer Name", newCategoryId);
|
||||
|
||||
mockMvc.perform(put("/api/articles/{id}", articleId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.name").value("Neuer Name"));
|
||||
}
|
||||
|
||||
// ==================== Hilfsmethoden ====================
|
||||
|
||||
private String createCategory(String name) throws Exception {
|
||||
|
|
@ -349,6 +434,18 @@ class ArticleControllerIntegrationTest extends AbstractIntegrationTest {
|
|||
return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
}
|
||||
|
||||
private String createArticleInCategory(String name, String number, Unit unit, PriceModel priceModel, String price, String catId) throws Exception {
|
||||
var request = new CreateArticleRequest(
|
||||
name, number, catId, 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",
|
||||
|
|
|
|||
|
|
@ -400,6 +400,78 @@ class CustomerControllerIntegrationTest extends AbstractIntegrationTest {
|
|||
.andExpect(jsonPath("$.frameContract.deliveryRhythm").value("WEEKLY"));
|
||||
}
|
||||
|
||||
// ==================== Rahmenvertrag entfernen ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Rahmenvertrag entfernen → 204, kein FrameContract mehr vorhanden")
|
||||
void removeFrameContract_returns204() throws Exception {
|
||||
String b2bId = createB2bCustomer("Vertrags 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());
|
||||
|
||||
mockMvc.perform(delete("/api/customers/{id}/frame-contract", b2bId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
mockMvc.perform(get("/api/customers/{id}", b2bId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.frameContract").isEmpty());
|
||||
}
|
||||
|
||||
// ==================== Kunden aktualisieren ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Kundenstammdaten aktualisieren → 200")
|
||||
void updateCustomer_returns200() throws Exception {
|
||||
String customerId = createB2cCustomer("Alt Name");
|
||||
|
||||
var request = new UpdateCustomerRequest(
|
||||
"Neuer Name", "Neue Str.", "5", "20095", "Hamburg", "DE",
|
||||
"+49 40 1234", null, null, null, null);
|
||||
|
||||
mockMvc.perform(put("/api/customers/{id}", customerId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.name").value("Neuer Name"))
|
||||
.andExpect(jsonPath("$.billingAddress.city").value("Hamburg"));
|
||||
}
|
||||
|
||||
// ==================== Doppelte LineItems im Rahmenvertrag ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Rahmenvertrag mit doppelten ArticleId-LineItems → 400")
|
||||
void setFrameContract_duplicateLineItems_returns400() throws Exception {
|
||||
String b2bId = createB2bCustomer("Duplikat 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),
|
||||
new SetFrameContractRequest.LineItem(articleId, new BigDecimal("2.00"), new BigDecimal("200"), 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().isBadRequest());
|
||||
}
|
||||
|
||||
// ==================== TC-AUTH ====================
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -670,6 +670,122 @@ class BatchControllerIntegrationTest extends AbstractIntegrationTest {
|
|||
}
|
||||
}
|
||||
|
||||
// ==================== POST /api/production/batches/{id}/start – ungültige Übergänge ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("POST /api/production/batches/{id}/start – Charge starten")
|
||||
class StartBatchEndpoint {
|
||||
|
||||
private String startToken;
|
||||
|
||||
@BeforeEach
|
||||
void setUpStartToken() {
|
||||
startToken = generateToken(java.util.UUID.randomUUID().toString(), "start.admin",
|
||||
"BATCH_WRITE,BATCH_READ,BATCH_COMPLETE,BATCH_CANCEL,RECIPE_WRITE,RECIPE_READ");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("COMPLETED Charge starten → 409")
|
||||
void startBatch_fromCompleted_returns409() throws Exception {
|
||||
String recipeId = createActiveRecipeWith(startToken);
|
||||
|
||||
// Plan batch
|
||||
var planRequest = new PlanBatchRequest(
|
||||
recipeId, "100", "KILOGRAM", PRODUCTION_DATE, BEST_BEFORE_DATE);
|
||||
var planResult = mockMvc.perform(post("/api/production/batches")
|
||||
.header("Authorization", "Bearer " + startToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(planRequest)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String batchId = objectMapper.readTree(planResult.getResponse().getContentAsString()).get("id").asText();
|
||||
|
||||
// Start
|
||||
mockMvc.perform(post("/api/production/batches/{id}/start", batchId)
|
||||
.header("Authorization", "Bearer " + startToken))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// Record consumption
|
||||
String consumptionJson = """
|
||||
{"inputBatchId": "%s", "articleId": "%s", "quantityUsed": "5.0", "quantityUnit": "KILOGRAM"}
|
||||
""".formatted(UUID.randomUUID().toString(), UUID.randomUUID().toString());
|
||||
mockMvc.perform(post("/api/production/batches/{id}/consumptions", batchId)
|
||||
.header("Authorization", "Bearer " + startToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(consumptionJson))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
// Complete
|
||||
String completeJson = """
|
||||
{"actualQuantity": "95", "actualQuantityUnit": "KILOGRAM", "waste": "5", "wasteUnit": "KILOGRAM"}
|
||||
""";
|
||||
mockMvc.perform(post("/api/production/batches/{id}/complete", batchId)
|
||||
.header("Authorization", "Bearer " + startToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(completeJson))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// Try to start again → 409
|
||||
mockMvc.perform(post("/api/production/batches/{id}/start", batchId)
|
||||
.header("Authorization", "Bearer " + startToken))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_INVALID_STATUS_TRANSITION"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("COMPLETED Charge stornieren → 409")
|
||||
void cancelBatch_fromCompleted_returns409() throws Exception {
|
||||
String recipeId = createActiveRecipeWith(startToken);
|
||||
|
||||
// Plan batch
|
||||
var planRequest = new PlanBatchRequest(
|
||||
recipeId, "100", "KILOGRAM", PRODUCTION_DATE, BEST_BEFORE_DATE);
|
||||
var planResult = mockMvc.perform(post("/api/production/batches")
|
||||
.header("Authorization", "Bearer " + startToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(planRequest)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String batchId = objectMapper.readTree(planResult.getResponse().getContentAsString()).get("id").asText();
|
||||
|
||||
// Start
|
||||
mockMvc.perform(post("/api/production/batches/{id}/start", batchId)
|
||||
.header("Authorization", "Bearer " + startToken))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// Record consumption
|
||||
String consumptionJson = """
|
||||
{"inputBatchId": "%s", "articleId": "%s", "quantityUsed": "5.0", "quantityUnit": "KILOGRAM"}
|
||||
""".formatted(UUID.randomUUID().toString(), UUID.randomUUID().toString());
|
||||
mockMvc.perform(post("/api/production/batches/{id}/consumptions", batchId)
|
||||
.header("Authorization", "Bearer " + startToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(consumptionJson))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
// Complete
|
||||
String completeJson = """
|
||||
{"actualQuantity": "95", "actualQuantityUnit": "KILOGRAM", "waste": "5", "wasteUnit": "KILOGRAM"}
|
||||
""";
|
||||
mockMvc.perform(post("/api/production/batches/{id}/complete", batchId)
|
||||
.header("Authorization", "Bearer " + startToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(completeJson))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// Try to cancel → 409
|
||||
String cancelJson = """
|
||||
{"reason": "Nachträgliche Stornierung"}
|
||||
""";
|
||||
mockMvc.perform(post("/api/production/batches/{id}/cancel", batchId)
|
||||
.header("Authorization", "Bearer " + startToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(cancelJson))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_INVALID_STATUS_TRANSITION"));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Hilfsmethoden ====================
|
||||
|
||||
private String createActiveRecipe() throws Exception {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,220 @@
|
|||
package de.effigenix.infrastructure.production.web;
|
||||
|
||||
import de.effigenix.domain.usermanagement.RoleName;
|
||||
import de.effigenix.infrastructure.AbstractIntegrationTest;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@DisplayName("Recipe Lifecycle Integration Tests")
|
||||
class RecipeLifecycleIntegrationTest extends AbstractIntegrationTest {
|
||||
|
||||
private String adminToken;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RoleEntity adminRole = createRole(RoleName.ADMIN, "Admin");
|
||||
UserEntity admin = createUser("recipe.admin", "recipe.admin@test.com", Set.of(adminRole), "BRANCH-01");
|
||||
adminToken = generateToken(admin.getId(), "recipe.admin", "RECIPE_WRITE,RECIPE_READ");
|
||||
}
|
||||
|
||||
// ==================== Aktivierung ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("POST /api/recipes/{id}/activate – Rezept aktivieren")
|
||||
class ActivateRecipe {
|
||||
|
||||
@Test
|
||||
@DisplayName("Rezept ohne Zutaten aktivieren → 400")
|
||||
void activateRecipe_withoutIngredients_returns400() throws Exception {
|
||||
String recipeId = createDraftRecipe();
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/activate", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Modifikation aktiver Rezepte ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Modifikation aktiver Rezepte")
|
||||
class ModifyActiveRecipe {
|
||||
|
||||
@Test
|
||||
@DisplayName("Zutat zu aktivem Rezept hinzufügen → 409")
|
||||
void addIngredient_toActiveRecipe_returns409() throws Exception {
|
||||
String recipeId = createActiveRecipe();
|
||||
|
||||
String ingredientJson = """
|
||||
{"position": 2, "articleId": "%s", "quantity": "3.0", "uom": "KILOGRAM", "substitutable": false}
|
||||
""".formatted(UUID.randomUUID().toString());
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/ingredients", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(ingredientJson))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Schritt zu aktivem Rezept hinzufügen → 409")
|
||||
void addStep_toActiveRecipe_returns409() throws Exception {
|
||||
String recipeId = createActiveRecipe();
|
||||
|
||||
String stepJson = """
|
||||
{"stepNumber": 1, "description": "Mischen", "durationMinutes": 15}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/steps", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(stepJson))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Archivierung ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("POST /api/recipes/{id}/archive – Rezept archivieren")
|
||||
class ArchiveRecipe {
|
||||
|
||||
@Test
|
||||
@DisplayName("Aktives Rezept archivieren → 200, Status ARCHIVED")
|
||||
void archiveRecipe_fromActive_returns200() throws Exception {
|
||||
String recipeId = createActiveRecipe();
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/archive", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("ARCHIVED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Draft-Rezept archivieren → 409 (ungültiger Übergang)")
|
||||
void archiveRecipe_fromDraft_returns409() throws Exception {
|
||||
String recipeId = createDraftRecipe();
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/archive", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Duplikate ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Duplikat-Prüfungen")
|
||||
class DuplicateChecks {
|
||||
|
||||
@Test
|
||||
@DisplayName("Zutat mit doppelter Position → 409")
|
||||
void addIngredient_duplicatePosition_returns409() throws Exception {
|
||||
String recipeId = createDraftRecipe();
|
||||
|
||||
String ingredientJson = """
|
||||
{"position": 1, "articleId": "%s", "quantity": "5.5", "uom": "KILOGRAM", "substitutable": false}
|
||||
""".formatted(UUID.randomUUID().toString());
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/ingredients", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(ingredientJson))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
String duplicateIngredientJson = """
|
||||
{"position": 1, "articleId": "%s", "quantity": "3.0", "uom": "KILOGRAM", "substitutable": false}
|
||||
""".formatted(UUID.randomUUID().toString());
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/ingredients", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(duplicateIngredientJson))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Schritt mit doppelter Schrittnummer → 409")
|
||||
void addStep_duplicateStepNumber_returns409() throws Exception {
|
||||
String recipeId = createDraftRecipe();
|
||||
|
||||
String stepJson = """
|
||||
{"stepNumber": 1, "description": "Mischen", "durationMinutes": 15}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/steps", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(stepJson))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
String duplicateStepJson = """
|
||||
{"stepNumber": 1, "description": "Anderer Schritt", "durationMinutes": 10}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/steps", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(duplicateStepJson))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Hilfsmethoden ====================
|
||||
|
||||
private String createDraftRecipe() throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"name": "Test-Rezept-%s",
|
||||
"version": 1,
|
||||
"type": "FINISHED_PRODUCT",
|
||||
"description": "Testrezept",
|
||||
"yieldPercentage": 85,
|
||||
"shelfLifeDays": 14,
|
||||
"outputQuantity": "100",
|
||||
"outputUom": "KILOGRAM",
|
||||
"articleId": "article-123"
|
||||
}
|
||||
""".formatted(UUID.randomUUID().toString().substring(0, 8));
|
||||
|
||||
var result = mockMvc.perform(post("/api/recipes")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(json))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
}
|
||||
|
||||
private String createActiveRecipe() throws Exception {
|
||||
String recipeId = createDraftRecipe();
|
||||
|
||||
String ingredientJson = """
|
||||
{"position": 1, "articleId": "%s", "quantity": "5.5", "uom": "KILOGRAM", "substitutable": false}
|
||||
""".formatted(UUID.randomUUID().toString());
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/ingredients", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(ingredientJson))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/activate", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
return recipeId;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package de.effigenix.infrastructure.usermanagement.web;
|
||||
|
||||
import de.effigenix.domain.usermanagement.RoleName;
|
||||
import de.effigenix.infrastructure.AbstractIntegrationTest;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@DisplayName("Role Controller Integration Tests")
|
||||
class RoleControllerIntegrationTest extends AbstractIntegrationTest {
|
||||
|
||||
private String adminToken;
|
||||
private String viewerToken;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RoleEntity adminRole = createRole(RoleName.ADMIN, "Admin");
|
||||
RoleEntity viewerRole = createRole(RoleName.PRODUCTION_WORKER, "Viewer");
|
||||
|
||||
UserEntity admin = createUser("role.admin", "role.admin@test.com", Set.of(adminRole), "BRANCH-01");
|
||||
UserEntity viewer = createUser("role.viewer", "role.viewer@test.com", Set.of(viewerRole), "BRANCH-01");
|
||||
|
||||
adminToken = generateToken(admin.getId(), "role.admin", "ROLE_READ");
|
||||
viewerToken = generateToken(viewer.getId(), "role.viewer", "USER_READ");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Rollen auflisten mit ROLE_READ → 200")
|
||||
void listRoles_withPermission_returns200() throws Exception {
|
||||
mockMvc.perform(get("/api/roles")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", hasSize(greaterThanOrEqualTo(2))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Rollen auflisten ohne ROLE_READ → 403")
|
||||
void listRoles_withoutPermission_returns403() throws Exception {
|
||||
mockMvc.perform(get("/api/roles")
|
||||
.header("Authorization", "Bearer " + viewerToken))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Rollen auflisten ohne Token → 401")
|
||||
void listRoles_withoutToken_returns401() throws Exception {
|
||||
mockMvc.perform(get("/api/roles"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue