mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 17:39:57 +01:00
feat(production): Charge abschließen (CompleteBatch)
Batch.complete() mit Ist-Menge, Ausschuss und Bemerkungen.
Invarianten: nur IN_PRODUCTION→COMPLETED, mind. eine Consumption,
ActualQuantity > 0, Waste >= 0. Full Vertical Slice mit Domain Event
Stub (BatchCompleted), REST POST /api/batches/{id}/complete und
Liquibase-Migration für die neuen Spalten.
This commit is contained in:
parent
f63790c058
commit
a08e4194ab
23 changed files with 1138 additions and 9 deletions
|
|
@ -0,0 +1,231 @@
|
|||
package de.effigenix.application.production;
|
||||
|
||||
import de.effigenix.application.production.command.CompleteBatchCommand;
|
||||
import de.effigenix.domain.production.*;
|
||||
import de.effigenix.shared.common.Quantity;
|
||||
import de.effigenix.shared.common.RepositoryError;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import de.effigenix.shared.common.UnitOfMeasure;
|
||||
import de.effigenix.shared.security.ActorId;
|
||||
import de.effigenix.shared.security.AuthorizationPort;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("CompleteBatch Use Case")
|
||||
class CompleteBatchTest {
|
||||
|
||||
@Mock private BatchRepository batchRepository;
|
||||
@Mock private AuthorizationPort authPort;
|
||||
|
||||
private CompleteBatch completeBatch;
|
||||
private ActorId performedBy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
completeBatch = new CompleteBatch(batchRepository, authPort);
|
||||
performedBy = ActorId.of("admin-user");
|
||||
}
|
||||
|
||||
private Batch inProductionBatchWithConsumption(String id) {
|
||||
var batch = Batch.reconstitute(
|
||||
BatchId.of(id),
|
||||
BatchNumber.generate(LocalDate.of(2026, 3, 1), 1),
|
||||
RecipeId.of("recipe-1"),
|
||||
BatchStatus.IN_PRODUCTION,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
LocalDate.of(2026, 3, 1),
|
||||
LocalDate.of(2026, 6, 1),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
batch.recordConsumption(new ConsumptionDraft("input-1", "article-1", "5.0", "KILOGRAM"));
|
||||
return batch;
|
||||
}
|
||||
|
||||
private Batch inProductionBatchWithoutConsumption(String id) {
|
||||
return Batch.reconstitute(
|
||||
BatchId.of(id),
|
||||
BatchNumber.generate(LocalDate.of(2026, 3, 1), 1),
|
||||
RecipeId.of("recipe-1"),
|
||||
BatchStatus.IN_PRODUCTION,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
LocalDate.of(2026, 3, 1),
|
||||
LocalDate.of(2026, 6, 1),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
}
|
||||
|
||||
private CompleteBatchCommand validCommand(String batchId) {
|
||||
return new CompleteBatchCommand(batchId, "95", "KILOGRAM", "5", "KILOGRAM", "Test remarks");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should complete batch with actual quantity and waste")
|
||||
void should_CompleteBatch_When_Valid() {
|
||||
var batchId = BatchId.of("batch-1");
|
||||
var batch = inProductionBatchWithConsumption("batch-1");
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
|
||||
when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));
|
||||
when(batchRepository.save(any())).thenReturn(Result.success(null));
|
||||
|
||||
var result = completeBatch.execute(validCommand("batch-1"), performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().status()).isEqualTo(BatchStatus.COMPLETED);
|
||||
assertThat(result.unsafeGetValue().actualQuantity().amount()).isEqualByComparingTo(new BigDecimal("95"));
|
||||
assertThat(result.unsafeGetValue().waste().amount()).isEqualByComparingTo(new BigDecimal("5"));
|
||||
assertThat(result.unsafeGetValue().remarks()).isEqualTo("Test remarks");
|
||||
verify(batchRepository).save(batch);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when batch not found")
|
||||
void should_Fail_When_BatchNotFound() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
|
||||
when(batchRepository.findById(any())).thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var result = completeBatch.execute(validCommand("nonexistent"), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.BatchNotFound.class);
|
||||
verify(batchRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when no consumptions recorded")
|
||||
void should_Fail_When_NoConsumptions() {
|
||||
var batchId = BatchId.of("batch-1");
|
||||
var batch = inProductionBatchWithoutConsumption("batch-1");
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
|
||||
when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));
|
||||
|
||||
var result = completeBatch.execute(validCommand("batch-1"), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.MissingConsumptions.class);
|
||||
verify(batchRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when actualQuantity is zero")
|
||||
void should_Fail_When_ActualQuantityZero() {
|
||||
var batchId = BatchId.of("batch-1");
|
||||
var batch = inProductionBatchWithConsumption("batch-1");
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
|
||||
when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));
|
||||
|
||||
var cmd = new CompleteBatchCommand("batch-1", "0", "KILOGRAM", "5", "KILOGRAM", null);
|
||||
var result = completeBatch.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.InvalidActualQuantity.class);
|
||||
verify(batchRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when batch is PLANNED (not in production)")
|
||||
void should_Fail_When_BatchIsPlanned() {
|
||||
var batchId = BatchId.of("batch-1");
|
||||
var batch = Batch.reconstitute(
|
||||
batchId,
|
||||
BatchNumber.generate(LocalDate.of(2026, 3, 1), 1),
|
||||
RecipeId.of("recipe-1"),
|
||||
BatchStatus.PLANNED,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
LocalDate.of(2026, 3, 1),
|
||||
LocalDate.of(2026, 6, 1),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
|
||||
when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));
|
||||
|
||||
var result = completeBatch.execute(validCommand("batch-1"), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.InvalidStatusTransition.class);
|
||||
verify(batchRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when waste is negative")
|
||||
void should_Fail_When_NegativeWaste() {
|
||||
var batchId = BatchId.of("batch-1");
|
||||
var batch = inProductionBatchWithConsumption("batch-1");
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
|
||||
when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));
|
||||
|
||||
var cmd = new CompleteBatchCommand("batch-1", "95", "KILOGRAM", "-1", "KILOGRAM", null);
|
||||
var result = completeBatch.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.InvalidWaste.class);
|
||||
verify(batchRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with Unauthorized when actor lacks permission")
|
||||
void should_FailWithUnauthorized_When_ActorLacksPermission() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(false);
|
||||
|
||||
var result = completeBatch.execute(validCommand("batch-1"), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.Unauthorized.class);
|
||||
verify(batchRepository, never()).findById(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure on repository error during find")
|
||||
void should_FailWithRepositoryFailure_When_FindFails() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
|
||||
when(batchRepository.findById(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = completeBatch.execute(validCommand("batch-1"), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure on repository error during save")
|
||||
void should_FailWithRepositoryFailure_When_SaveFails() {
|
||||
var batchId = BatchId.of("batch-1");
|
||||
var batch = inProductionBatchWithConsumption("batch-1");
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_COMPLETE)).thenReturn(true);
|
||||
when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));
|
||||
when(batchRepository.save(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("write error")));
|
||||
|
||||
var result = completeBatch.execute(validCommand("batch-1"), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
|
|
@ -49,10 +49,12 @@ class FindBatchByNumberTest {
|
|||
RecipeId.of("recipe-1"),
|
||||
BatchStatus.PLANNED,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
LocalDate.of(2026, 3, 1),
|
||||
LocalDate.of(2026, 6, 1),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,10 +47,12 @@ class GetBatchTest {
|
|||
RecipeId.of("recipe-1"),
|
||||
BatchStatus.PLANNED,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
LocalDate.of(2026, 3, 1),
|
||||
LocalDate.of(2026, 6, 1),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,10 +51,12 @@ class ListBatchesTest {
|
|||
RecipeId.of("recipe-1"),
|
||||
status,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
PRODUCTION_DATE,
|
||||
LocalDate.of(2026, 6, 1),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,10 +48,12 @@ class RecordConsumptionTest {
|
|||
RecipeId.of("recipe-1"),
|
||||
BatchStatus.IN_PRODUCTION,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
LocalDate.of(2026, 3, 1),
|
||||
LocalDate.of(2026, 6, 1),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
}
|
||||
|
|
@ -63,10 +65,12 @@ class RecordConsumptionTest {
|
|||
RecipeId.of("recipe-1"),
|
||||
BatchStatus.PLANNED,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
LocalDate.of(2026, 3, 1),
|
||||
LocalDate.of(2026, 6, 1),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,10 +48,12 @@ class StartBatchTest {
|
|||
RecipeId.of("recipe-1"),
|
||||
BatchStatus.PLANNED,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
LocalDate.of(2026, 3, 1),
|
||||
LocalDate.of(2026, 6, 1),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
}
|
||||
|
|
@ -63,10 +65,12 @@ class StartBatchTest {
|
|||
RecipeId.of("recipe-1"),
|
||||
BatchStatus.IN_PRODUCTION,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
LocalDate.of(2026, 3, 1),
|
||||
LocalDate.of(2026, 6, 1),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,8 +208,10 @@ class BatchTest {
|
|||
BatchId.of("b-1"), BATCH_NUMBER, RecipeId.of("r-1"),
|
||||
BatchStatus.IN_PRODUCTION,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
PRODUCTION_DATE, BEST_BEFORE_DATE,
|
||||
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
|
||||
|
|
@ -229,8 +231,10 @@ class BatchTest {
|
|||
BatchId.of("b-1"), BATCH_NUMBER, RecipeId.of("r-1"),
|
||||
BatchStatus.COMPLETED,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
PRODUCTION_DATE, BEST_BEFORE_DATE,
|
||||
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
|
||||
|
|
@ -247,8 +251,10 @@ class BatchTest {
|
|||
BatchId.of("b-1"), BATCH_NUMBER, RecipeId.of("r-1"),
|
||||
BatchStatus.CANCELLED,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
PRODUCTION_DATE, BEST_BEFORE_DATE,
|
||||
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
|
||||
|
|
@ -311,8 +317,10 @@ class BatchTest {
|
|||
BatchId.of("b-1"), BATCH_NUMBER, RecipeId.of("r-1"),
|
||||
BatchStatus.COMPLETED,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
PRODUCTION_DATE, BEST_BEFORE_DATE,
|
||||
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
var draft = new ConsumptionDraft("input-1", "article-1", "5.0", "KILOGRAM");
|
||||
|
|
@ -362,6 +370,257 @@ class BatchTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("complete()")
|
||||
class Complete {
|
||||
|
||||
private Batch inProductionBatchWithConsumption() {
|
||||
var batch = Batch.plan(validDraft(), BATCH_NUMBER).unsafeGetValue();
|
||||
batch.startProduction();
|
||||
batch.recordConsumption(new ConsumptionDraft("input-1", "article-1", "5.0", "KILOGRAM"));
|
||||
return batch;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should complete batch with actual quantity, waste, and remarks")
|
||||
void should_Complete_When_ValidDraft() {
|
||||
var batch = inProductionBatchWithConsumption();
|
||||
var draft = new CompleteBatchDraft("95", "KILOGRAM", "5", "KILOGRAM", "Alles ok");
|
||||
|
||||
var result = batch.complete(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(batch.status()).isEqualTo(BatchStatus.COMPLETED);
|
||||
assertThat(batch.actualQuantity().amount()).isEqualByComparingTo(new BigDecimal("95"));
|
||||
assertThat(batch.actualQuantity().uom()).isEqualTo(UnitOfMeasure.KILOGRAM);
|
||||
assertThat(batch.waste().amount()).isEqualByComparingTo(new BigDecimal("5"));
|
||||
assertThat(batch.waste().uom()).isEqualTo(UnitOfMeasure.KILOGRAM);
|
||||
assertThat(batch.remarks()).isEqualTo("Alles ok");
|
||||
assertThat(batch.completedAt()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should complete batch with zero waste")
|
||||
void should_Complete_When_ZeroWaste() {
|
||||
var batch = inProductionBatchWithConsumption();
|
||||
var draft = new CompleteBatchDraft("100", "KILOGRAM", "0", "KILOGRAM", null);
|
||||
|
||||
var result = batch.complete(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(batch.status()).isEqualTo(BatchStatus.COMPLETED);
|
||||
assertThat(batch.waste().amount()).isEqualByComparingTo(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should complete batch without remarks")
|
||||
void should_Complete_When_NoRemarks() {
|
||||
var batch = inProductionBatchWithConsumption();
|
||||
var draft = new CompleteBatchDraft("95", "KILOGRAM", "5", "KILOGRAM", null);
|
||||
|
||||
var result = batch.complete(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(batch.remarks()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when not IN_PRODUCTION (PLANNED)")
|
||||
void should_Fail_When_Planned() {
|
||||
var batch = Batch.plan(validDraft(), BATCH_NUMBER).unsafeGetValue();
|
||||
var draft = new CompleteBatchDraft("95", "KILOGRAM", "5", "KILOGRAM", null);
|
||||
|
||||
var result = batch.complete(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.InvalidStatusTransition.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when already COMPLETED")
|
||||
void should_Fail_When_AlreadyCompleted() {
|
||||
var batch = Batch.reconstitute(
|
||||
BatchId.of("b-1"), BATCH_NUMBER, RecipeId.of("r-1"),
|
||||
BatchStatus.COMPLETED,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
PRODUCTION_DATE, BEST_BEFORE_DATE,
|
||||
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
var draft = new CompleteBatchDraft("95", "KILOGRAM", "5", "KILOGRAM", null);
|
||||
|
||||
var result = batch.complete(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.InvalidStatusTransition.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when no consumptions recorded")
|
||||
void should_Fail_When_NoConsumptions() {
|
||||
var batch = Batch.plan(validDraft(), BATCH_NUMBER).unsafeGetValue();
|
||||
batch.startProduction();
|
||||
var draft = new CompleteBatchDraft("95", "KILOGRAM", "5", "KILOGRAM", null);
|
||||
|
||||
var result = batch.complete(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.MissingConsumptions.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when actualQuantity is zero")
|
||||
void should_Fail_When_ActualQuantityZero() {
|
||||
var batch = inProductionBatchWithConsumption();
|
||||
var draft = new CompleteBatchDraft("0", "KILOGRAM", "5", "KILOGRAM", null);
|
||||
|
||||
var result = batch.complete(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.InvalidActualQuantity.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when actualQuantity is negative")
|
||||
void should_Fail_When_ActualQuantityNegative() {
|
||||
var batch = inProductionBatchWithConsumption();
|
||||
var draft = new CompleteBatchDraft("-10", "KILOGRAM", "5", "KILOGRAM", null);
|
||||
|
||||
var result = batch.complete(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.InvalidActualQuantity.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when waste is negative")
|
||||
void should_Fail_When_WasteNegative() {
|
||||
var batch = inProductionBatchWithConsumption();
|
||||
var draft = new CompleteBatchDraft("95", "KILOGRAM", "-1", "KILOGRAM", null);
|
||||
|
||||
var result = batch.complete(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.InvalidWaste.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when CANCELLED")
|
||||
void should_Fail_When_Cancelled() {
|
||||
var batch = Batch.reconstitute(
|
||||
BatchId.of("b-1"), BATCH_NUMBER, RecipeId.of("r-1"),
|
||||
BatchStatus.CANCELLED,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
PRODUCTION_DATE, BEST_BEFORE_DATE,
|
||||
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
var draft = new CompleteBatchDraft("95", "KILOGRAM", "5", "KILOGRAM", null);
|
||||
|
||||
var result = batch.complete(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.InvalidStatusTransition.class);
|
||||
var err = (BatchError.InvalidStatusTransition) result.unsafeGetError();
|
||||
assertThat(err.current()).isEqualTo(BatchStatus.CANCELLED);
|
||||
assertThat(err.target()).isEqualTo(BatchStatus.COMPLETED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when waste format is invalid")
|
||||
void should_Fail_When_InvalidWasteFormat() {
|
||||
var batch = inProductionBatchWithConsumption();
|
||||
var draft = new CompleteBatchDraft("95", "KILOGRAM", "abc", "KILOGRAM", null);
|
||||
|
||||
var result = batch.complete(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.InvalidWaste.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when waste unit is invalid")
|
||||
void should_Fail_When_InvalidWasteUnit() {
|
||||
var batch = inProductionBatchWithConsumption();
|
||||
var draft = new CompleteBatchDraft("95", "KILOGRAM", "5", "INVALID", null);
|
||||
|
||||
var result = batch.complete(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.InvalidWaste.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not mutate state on validation failure")
|
||||
void should_NotMutateState_When_ValidationFails() {
|
||||
var batch = inProductionBatchWithConsumption();
|
||||
var draft = new CompleteBatchDraft("0", "KILOGRAM", "5", "KILOGRAM", null);
|
||||
|
||||
batch.complete(draft);
|
||||
|
||||
assertThat(batch.status()).isEqualTo(BatchStatus.IN_PRODUCTION);
|
||||
assertThat(batch.actualQuantity()).isNull();
|
||||
assertThat(batch.waste()).isNull();
|
||||
assertThat(batch.remarks()).isNull();
|
||||
assertThat(batch.completedAt()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not mutate state when missing consumptions")
|
||||
void should_NotMutateState_When_MissingConsumptions() {
|
||||
var batch = Batch.plan(validDraft(), BATCH_NUMBER).unsafeGetValue();
|
||||
batch.startProduction();
|
||||
var beforeUpdate = batch.updatedAt();
|
||||
|
||||
batch.complete(new CompleteBatchDraft("95", "KILOGRAM", "5", "KILOGRAM", null));
|
||||
|
||||
assertThat(batch.status()).isEqualTo(BatchStatus.IN_PRODUCTION);
|
||||
assertThat(batch.actualQuantity()).isNull();
|
||||
assertThat(batch.updatedAt()).isEqualTo(beforeUpdate);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when actualQuantity format is invalid")
|
||||
void should_Fail_When_InvalidActualQuantityFormat() {
|
||||
var batch = inProductionBatchWithConsumption();
|
||||
var draft = new CompleteBatchDraft("abc", "KILOGRAM", "5", "KILOGRAM", null);
|
||||
|
||||
var result = batch.complete(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.InvalidActualQuantity.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when actualQuantity unit is invalid")
|
||||
void should_Fail_When_InvalidActualQuantityUnit() {
|
||||
var batch = inProductionBatchWithConsumption();
|
||||
var draft = new CompleteBatchDraft("95", "INVALID", "5", "KILOGRAM", null);
|
||||
|
||||
var result = batch.complete(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.InvalidActualQuantity.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt and set completedAt on completion")
|
||||
void should_UpdateTimestamps_When_Completing() {
|
||||
var batch = inProductionBatchWithConsumption();
|
||||
var beforeComplete = batch.updatedAt();
|
||||
|
||||
batch.complete(new CompleteBatchDraft("95", "KILOGRAM", "5", "KILOGRAM", null));
|
||||
|
||||
assertThat(batch.updatedAt()).isAfterOrEqualTo(beforeComplete);
|
||||
assertThat(batch.completedAt()).isNotNull();
|
||||
assertThat(batch.completedAt()).isEqualTo(batch.updatedAt());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("reconstitute()")
|
||||
class Reconstitute {
|
||||
|
|
@ -375,10 +634,12 @@ class BatchTest {
|
|||
RecipeId.of("recipe-123"),
|
||||
BatchStatus.IN_PRODUCTION,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
null, null, null,
|
||||
PRODUCTION_DATE,
|
||||
BEST_BEFORE_DATE,
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
null,
|
||||
0L, List.of()
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -278,10 +278,256 @@ class BatchControllerIntegrationTest extends AbstractIntegrationTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("POST /api/production/batches/{id}/complete – Charge abschließen")
|
||||
class CompleteBatchEndpoint {
|
||||
|
||||
private String completeToken;
|
||||
|
||||
@BeforeEach
|
||||
void setUpCompleteToken() {
|
||||
completeToken = generateToken(UUID.randomUUID().toString(), "complete.admin",
|
||||
"BATCH_WRITE,BATCH_READ,BATCH_COMPLETE,RECIPE_WRITE,RECIPE_READ");
|
||||
}
|
||||
|
||||
private String createInProductionBatchWithConsumption() throws Exception {
|
||||
String recipeId = createActiveRecipeWith(completeToken);
|
||||
|
||||
// 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 " + completeToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(planRequest)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String batchId = objectMapper.readTree(planResult.getResponse().getContentAsString()).get("id").asText();
|
||||
|
||||
// Start production
|
||||
mockMvc.perform(post("/api/production/batches/{id}/start", batchId)
|
||||
.header("Authorization", "Bearer " + completeToken))
|
||||
.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 " + completeToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(consumptionJson))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
return batchId;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Charge abschließen mit Ist-Menge und Ausschuss → 200, Status COMPLETED")
|
||||
void completeBatch_withValidData_returns200() throws Exception {
|
||||
String batchId = createInProductionBatchWithConsumption();
|
||||
|
||||
String completeJson = """
|
||||
{"actualQuantity": "95", "actualQuantityUnit": "KILOGRAM", "waste": "5", "wasteUnit": "KILOGRAM", "remarks": "Alles ok"}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/api/production/batches/{id}/complete", batchId)
|
||||
.header("Authorization", "Bearer " + completeToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(completeJson))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("COMPLETED"))
|
||||
.andExpect(jsonPath("$.actualQuantity").value("95.000000"))
|
||||
.andExpect(jsonPath("$.actualQuantityUnit").value("KILOGRAM"))
|
||||
.andExpect(jsonPath("$.waste").value("5.000000"))
|
||||
.andExpect(jsonPath("$.wasteUnit").value("KILOGRAM"))
|
||||
.andExpect(jsonPath("$.remarks").value("Alles ok"))
|
||||
.andExpect(jsonPath("$.completedAt").isNotEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Ohne Consumptions → 409 (MissingConsumptions)")
|
||||
void completeBatch_withoutConsumptions_returns400() throws Exception {
|
||||
String recipeId = createActiveRecipeWith(completeToken);
|
||||
|
||||
// Plan + Start, aber keine Consumption
|
||||
var planRequest = new PlanBatchRequest(
|
||||
recipeId, "100", "KILOGRAM", PRODUCTION_DATE, BEST_BEFORE_DATE);
|
||||
var planResult = mockMvc.perform(post("/api/production/batches")
|
||||
.header("Authorization", "Bearer " + completeToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(planRequest)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String batchId = objectMapper.readTree(planResult.getResponse().getContentAsString()).get("id").asText();
|
||||
|
||||
mockMvc.perform(post("/api/production/batches/{id}/start", batchId)
|
||||
.header("Authorization", "Bearer " + completeToken))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
String completeJson = """
|
||||
{"actualQuantity": "95", "actualQuantityUnit": "KILOGRAM", "waste": "5", "wasteUnit": "KILOGRAM"}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/api/production/batches/{id}/complete", batchId)
|
||||
.header("Authorization", "Bearer " + completeToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(completeJson))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_MISSING_CONSUMPTIONS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ActualQuantity 0 → 400")
|
||||
void completeBatch_zeroActualQuantity_returns400() throws Exception {
|
||||
String batchId = createInProductionBatchWithConsumption();
|
||||
|
||||
String completeJson = """
|
||||
{"actualQuantity": "0", "actualQuantityUnit": "KILOGRAM", "waste": "5", "wasteUnit": "KILOGRAM"}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/api/production/batches/{id}/complete", batchId)
|
||||
.header("Authorization", "Bearer " + completeToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(completeJson))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_INVALID_ACTUAL_QUANTITY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Negativer Waste → 400")
|
||||
void completeBatch_negativeWaste_returns400() throws Exception {
|
||||
String batchId = createInProductionBatchWithConsumption();
|
||||
|
||||
String completeJson = """
|
||||
{"actualQuantity": "95", "actualQuantityUnit": "KILOGRAM", "waste": "-1", "wasteUnit": "KILOGRAM"}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/api/production/batches/{id}/complete", batchId)
|
||||
.header("Authorization", "Bearer " + completeToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(completeJson))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_INVALID_WASTE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PLANNED Batch (nicht gestartet) → 409")
|
||||
void completeBatch_plannedBatch_returns409() throws Exception {
|
||||
String recipeId = createActiveRecipeWith(completeToken);
|
||||
|
||||
var planRequest = new PlanBatchRequest(
|
||||
recipeId, "100", "KILOGRAM", PRODUCTION_DATE, BEST_BEFORE_DATE);
|
||||
var planResult = mockMvc.perform(post("/api/production/batches")
|
||||
.header("Authorization", "Bearer " + completeToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(planRequest)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String batchId = objectMapper.readTree(planResult.getResponse().getContentAsString()).get("id").asText();
|
||||
|
||||
String completeJson = """
|
||||
{"actualQuantity": "95", "actualQuantityUnit": "KILOGRAM", "waste": "5", "wasteUnit": "KILOGRAM"}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/api/production/batches/{id}/complete", batchId)
|
||||
.header("Authorization", "Bearer " + completeToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(completeJson))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_INVALID_STATUS_TRANSITION"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Bereits COMPLETED → 409")
|
||||
void completeBatch_alreadyCompleted_returns409() throws Exception {
|
||||
String batchId = createInProductionBatchWithConsumption();
|
||||
|
||||
String completeJson = """
|
||||
{"actualQuantity": "95", "actualQuantityUnit": "KILOGRAM", "waste": "5", "wasteUnit": "KILOGRAM"}
|
||||
""";
|
||||
|
||||
// First complete → success
|
||||
mockMvc.perform(post("/api/production/batches/{id}/complete", batchId)
|
||||
.header("Authorization", "Bearer " + completeToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(completeJson))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// Second complete → conflict
|
||||
mockMvc.perform(post("/api/production/batches/{id}/complete", batchId)
|
||||
.header("Authorization", "Bearer " + completeToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(completeJson))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_INVALID_STATUS_TRANSITION"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Batch nicht gefunden → 404")
|
||||
void completeBatch_notFound_returns404() throws Exception {
|
||||
String completeJson = """
|
||||
{"actualQuantity": "95", "actualQuantityUnit": "KILOGRAM", "waste": "5", "wasteUnit": "KILOGRAM"}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/api/production/batches/{id}/complete", UUID.randomUUID().toString())
|
||||
.header("Authorization", "Bearer " + completeToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(completeJson))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_NOT_FOUND"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Bean Validation: actualQuantity leer → 400")
|
||||
void completeBatch_blankActualQuantity_returns400() throws Exception {
|
||||
String completeJson = """
|
||||
{"actualQuantity": "", "actualQuantityUnit": "KILOGRAM", "waste": "5", "wasteUnit": "KILOGRAM"}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/api/production/batches/{id}/complete", UUID.randomUUID().toString())
|
||||
.header("Authorization", "Bearer " + completeToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(completeJson))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Ohne Token → 401")
|
||||
void completeBatch_withoutToken_returns401() throws Exception {
|
||||
String completeJson = """
|
||||
{"actualQuantity": "95", "actualQuantityUnit": "KILOGRAM", "waste": "5", "wasteUnit": "KILOGRAM"}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/api/production/batches/{id}/complete", UUID.randomUUID().toString())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(completeJson))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Ohne BATCH_COMPLETE → 403")
|
||||
void completeBatch_withoutPermission_returns403() throws Exception {
|
||||
String completeJson = """
|
||||
{"actualQuantity": "95", "actualQuantityUnit": "KILOGRAM", "waste": "5", "wasteUnit": "KILOGRAM"}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post("/api/production/batches/{id}/complete", UUID.randomUUID().toString())
|
||||
.header("Authorization", "Bearer " + viewerToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(completeJson))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Hilfsmethoden ====================
|
||||
|
||||
private String createActiveRecipe() throws Exception {
|
||||
String recipeId = createDraftRecipe();
|
||||
return createActiveRecipeWith(adminToken);
|
||||
}
|
||||
|
||||
private String createActiveRecipeWith(String token) throws Exception {
|
||||
String recipeId = createDraftRecipeWith(token);
|
||||
|
||||
// Add ingredient (required for activation)
|
||||
String ingredientJson = """
|
||||
|
|
@ -289,20 +535,24 @@ class BatchControllerIntegrationTest extends AbstractIntegrationTest {
|
|||
""".formatted(UUID.randomUUID().toString());
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/ingredients", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(ingredientJson))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
// Activate
|
||||
mockMvc.perform(post("/api/recipes/{id}/activate", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.header("Authorization", "Bearer " + token))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
return recipeId;
|
||||
}
|
||||
|
||||
private String createDraftRecipe() throws Exception {
|
||||
return createDraftRecipeWith(adminToken);
|
||||
}
|
||||
|
||||
private String createDraftRecipeWith(String token) throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"name": "Test-Rezept-%s",
|
||||
|
|
@ -318,7 +568,7 @@ class BatchControllerIntegrationTest extends AbstractIntegrationTest {
|
|||
""".formatted(UUID.randomUUID().toString().substring(0, 8));
|
||||
|
||||
var result = mockMvc.perform(post("/api/recipes")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(json))
|
||||
.andExpect(status().isCreated())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue