mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 12:09:35 +01:00
test(production): Tests für ActivateRecipe und ArchiveRecipe Use Cases
Domain-Tests für archive() in RecipeTest, Application-Layer-Tests für beide Use Cases mit Mockito (Auth, NotFound, Statusübergänge).
This commit is contained in:
parent
408813a5b5
commit
1e12353b9b
3 changed files with 327 additions and 0 deletions
|
|
@ -0,0 +1,135 @@
|
||||||
|
package de.effigenix.application.production;
|
||||||
|
|
||||||
|
import de.effigenix.application.production.command.ActivateRecipeCommand;
|
||||||
|
import de.effigenix.domain.production.*;
|
||||||
|
import de.effigenix.shared.common.Quantity;
|
||||||
|
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.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("ActivateRecipe Use Case")
|
||||||
|
class ActivateRecipeTest {
|
||||||
|
|
||||||
|
@Mock private RecipeRepository recipeRepository;
|
||||||
|
@Mock private AuthorizationPort authPort;
|
||||||
|
|
||||||
|
private ActivateRecipe activateRecipe;
|
||||||
|
private ActorId performedBy;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
activateRecipe = new ActivateRecipe(recipeRepository, authPort);
|
||||||
|
performedBy = ActorId.of("admin-user");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Recipe draftRecipeWithIngredient() {
|
||||||
|
var recipe = Recipe.create(new RecipeDraft(
|
||||||
|
"Bratwurst", 1, RecipeType.FINISHED_PRODUCT,
|
||||||
|
null, 85, 14, "100", "KILOGRAM"
|
||||||
|
)).unsafeGetValue();
|
||||||
|
recipe.addIngredient(new IngredientDraft(1, "article-123", "5.5", "KILOGRAM", null, false));
|
||||||
|
return recipe;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Recipe draftRecipeWithoutIngredient() {
|
||||||
|
return Recipe.create(new RecipeDraft(
|
||||||
|
"Bratwurst", 1, RecipeType.FINISHED_PRODUCT,
|
||||||
|
null, 85, 14, "100", "KILOGRAM"
|
||||||
|
)).unsafeGetValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Recipe activeRecipe() {
|
||||||
|
return Recipe.reconstitute(
|
||||||
|
RecipeId.of("recipe-1"), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
|
||||||
|
null, new YieldPercentage(85), 14,
|
||||||
|
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||||
|
RecipeStatus.ACTIVE, List.of(), List.of(),
|
||||||
|
LocalDateTime.now(), LocalDateTime.now()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should_ActivateRecipe_When_DraftWithIngredients")
|
||||||
|
void should_ActivateRecipe_When_DraftWithIngredients() {
|
||||||
|
var recipe = draftRecipeWithIngredient();
|
||||||
|
when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true);
|
||||||
|
when(recipeRepository.findById(recipe.id())).thenReturn(Result.success(Optional.of(recipe)));
|
||||||
|
when(recipeRepository.save(any())).thenReturn(Result.success(null));
|
||||||
|
|
||||||
|
var result = activateRecipe.execute(new ActivateRecipeCommand(recipe.id().value()), performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isSuccess()).isTrue();
|
||||||
|
assertThat(result.unsafeGetValue().status()).isEqualTo(RecipeStatus.ACTIVE);
|
||||||
|
verify(recipeRepository).save(argThat(r -> r.status() == RecipeStatus.ACTIVE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should_FailWithUnauthorized_When_ActorLacksPermission")
|
||||||
|
void should_FailWithUnauthorized_When_ActorLacksPermission() {
|
||||||
|
when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(false);
|
||||||
|
|
||||||
|
var result = activateRecipe.execute(new ActivateRecipeCommand("recipe-1"), performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.Unauthorized.class);
|
||||||
|
verify(recipeRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should_FailWithRecipeNotFound_When_RecipeDoesNotExist")
|
||||||
|
void should_FailWithRecipeNotFound_When_RecipeDoesNotExist() {
|
||||||
|
when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true);
|
||||||
|
when(recipeRepository.findById(RecipeId.of("nonexistent"))).thenReturn(Result.success(Optional.empty()));
|
||||||
|
|
||||||
|
var result = activateRecipe.execute(new ActivateRecipeCommand("nonexistent"), performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.RecipeNotFound.class);
|
||||||
|
verify(recipeRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should_FailWithNoIngredients_When_DraftHasNoIngredients")
|
||||||
|
void should_FailWithNoIngredients_When_DraftHasNoIngredients() {
|
||||||
|
var recipe = draftRecipeWithoutIngredient();
|
||||||
|
when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true);
|
||||||
|
when(recipeRepository.findById(recipe.id())).thenReturn(Result.success(Optional.of(recipe)));
|
||||||
|
|
||||||
|
var result = activateRecipe.execute(new ActivateRecipeCommand(recipe.id().value()), performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.NoIngredients.class);
|
||||||
|
verify(recipeRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should_FailWithInvalidStatusTransition_When_AlreadyActive")
|
||||||
|
void should_FailWithInvalidStatusTransition_When_AlreadyActive() {
|
||||||
|
var recipe = activeRecipe();
|
||||||
|
when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true);
|
||||||
|
when(recipeRepository.findById(RecipeId.of("recipe-1"))).thenReturn(Result.success(Optional.of(recipe)));
|
||||||
|
|
||||||
|
var result = activateRecipe.execute(new ActivateRecipeCommand("recipe-1"), performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.InvalidStatusTransition.class);
|
||||||
|
verify(recipeRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
package de.effigenix.application.production;
|
||||||
|
|
||||||
|
import de.effigenix.application.production.command.ArchiveRecipeCommand;
|
||||||
|
import de.effigenix.domain.production.*;
|
||||||
|
import de.effigenix.shared.common.Quantity;
|
||||||
|
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.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@DisplayName("ArchiveRecipe Use Case")
|
||||||
|
class ArchiveRecipeTest {
|
||||||
|
|
||||||
|
@Mock private RecipeRepository recipeRepository;
|
||||||
|
@Mock private AuthorizationPort authPort;
|
||||||
|
|
||||||
|
private ArchiveRecipe archiveRecipe;
|
||||||
|
private ActorId performedBy;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
archiveRecipe = new ArchiveRecipe(recipeRepository, authPort);
|
||||||
|
performedBy = ActorId.of("admin-user");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Recipe activeRecipe() {
|
||||||
|
return Recipe.reconstitute(
|
||||||
|
RecipeId.of("recipe-1"), new RecipeName("Bratwurst"), 1, RecipeType.FINISHED_PRODUCT,
|
||||||
|
null, new YieldPercentage(85), 14,
|
||||||
|
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||||
|
RecipeStatus.ACTIVE, List.of(), List.of(),
|
||||||
|
LocalDateTime.now(), LocalDateTime.now()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Recipe draftRecipe() {
|
||||||
|
return Recipe.create(new RecipeDraft(
|
||||||
|
"Bratwurst", 1, RecipeType.FINISHED_PRODUCT,
|
||||||
|
null, 85, 14, "100", "KILOGRAM"
|
||||||
|
)).unsafeGetValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should_ArchiveRecipe_When_RecipeIsActive")
|
||||||
|
void should_ArchiveRecipe_When_RecipeIsActive() {
|
||||||
|
var recipe = activeRecipe();
|
||||||
|
when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true);
|
||||||
|
when(recipeRepository.findById(RecipeId.of("recipe-1"))).thenReturn(Result.success(Optional.of(recipe)));
|
||||||
|
when(recipeRepository.save(any())).thenReturn(Result.success(null));
|
||||||
|
|
||||||
|
var result = archiveRecipe.execute(new ArchiveRecipeCommand("recipe-1"), performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isSuccess()).isTrue();
|
||||||
|
assertThat(result.unsafeGetValue().status()).isEqualTo(RecipeStatus.ARCHIVED);
|
||||||
|
verify(recipeRepository).save(argThat(r -> r.status() == RecipeStatus.ARCHIVED));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should_FailWithUnauthorized_When_ActorLacksPermission")
|
||||||
|
void should_FailWithUnauthorized_When_ActorLacksPermission() {
|
||||||
|
when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(false);
|
||||||
|
|
||||||
|
var result = archiveRecipe.execute(new ArchiveRecipeCommand("recipe-1"), performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.Unauthorized.class);
|
||||||
|
verify(recipeRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should_FailWithRecipeNotFound_When_RecipeDoesNotExist")
|
||||||
|
void should_FailWithRecipeNotFound_When_RecipeDoesNotExist() {
|
||||||
|
when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true);
|
||||||
|
when(recipeRepository.findById(RecipeId.of("nonexistent"))).thenReturn(Result.success(Optional.empty()));
|
||||||
|
|
||||||
|
var result = archiveRecipe.execute(new ArchiveRecipeCommand("nonexistent"), performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.RecipeNotFound.class);
|
||||||
|
verify(recipeRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should_FailWithInvalidStatusTransition_When_RecipeIsDraft")
|
||||||
|
void should_FailWithInvalidStatusTransition_When_RecipeIsDraft() {
|
||||||
|
var recipe = draftRecipe();
|
||||||
|
when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true);
|
||||||
|
when(recipeRepository.findById(recipe.id())).thenReturn(Result.success(Optional.of(recipe)));
|
||||||
|
|
||||||
|
var result = archiveRecipe.execute(new ArchiveRecipeCommand(recipe.id().value()), performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.InvalidStatusTransition.class);
|
||||||
|
verify(recipeRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should_FailWithInvalidStatusTransition_When_AlreadyArchived")
|
||||||
|
void should_FailWithInvalidStatusTransition_When_AlreadyArchived() {
|
||||||
|
var recipe = Recipe.reconstitute(
|
||||||
|
RecipeId.of("recipe-2"), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
|
||||||
|
null, new YieldPercentage(85), 14,
|
||||||
|
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||||
|
RecipeStatus.ARCHIVED, List.of(), List.of(),
|
||||||
|
LocalDateTime.now(), LocalDateTime.now()
|
||||||
|
);
|
||||||
|
when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true);
|
||||||
|
when(recipeRepository.findById(RecipeId.of("recipe-2"))).thenReturn(Result.success(Optional.of(recipe)));
|
||||||
|
|
||||||
|
var result = archiveRecipe.execute(new ArchiveRecipeCommand("recipe-2"), performedBy);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.InvalidStatusTransition.class);
|
||||||
|
verify(recipeRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -542,6 +542,66 @@ class RecipeTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("archive()")
|
||||||
|
class Archive {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should archive ACTIVE recipe")
|
||||||
|
void should_Archive_When_ActiveStatus() {
|
||||||
|
var recipe = Recipe.reconstitute(
|
||||||
|
RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
|
||||||
|
null, new YieldPercentage(85), 14,
|
||||||
|
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||||
|
RecipeStatus.ACTIVE, List.of(), List.of(),
|
||||||
|
java.time.LocalDateTime.now(), java.time.LocalDateTime.now()
|
||||||
|
);
|
||||||
|
var updatedBefore = recipe.updatedAt();
|
||||||
|
|
||||||
|
var result = recipe.archive();
|
||||||
|
|
||||||
|
assertThat(result.isSuccess()).isTrue();
|
||||||
|
assertThat(recipe.status()).isEqualTo(RecipeStatus.ARCHIVED);
|
||||||
|
assertThat(recipe.updatedAt()).isAfter(updatedBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail when recipe is in DRAFT status")
|
||||||
|
void should_Fail_When_DraftStatus() {
|
||||||
|
var recipe = Recipe.create(validDraft()).unsafeGetValue();
|
||||||
|
|
||||||
|
var result = recipe.archive();
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
var error = result.unsafeGetError();
|
||||||
|
assertThat(error).isInstanceOf(RecipeError.InvalidStatusTransition.class);
|
||||||
|
var transition = (RecipeError.InvalidStatusTransition) error;
|
||||||
|
assertThat(transition.current()).isEqualTo(RecipeStatus.DRAFT);
|
||||||
|
assertThat(transition.target()).isEqualTo(RecipeStatus.ARCHIVED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail when recipe is already ARCHIVED")
|
||||||
|
void should_Fail_When_AlreadyArchived() {
|
||||||
|
var recipe = Recipe.reconstitute(
|
||||||
|
RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
|
||||||
|
null, new YieldPercentage(85), 14,
|
||||||
|
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||||
|
RecipeStatus.ARCHIVED, List.of(), List.of(),
|
||||||
|
java.time.LocalDateTime.now(), java.time.LocalDateTime.now()
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = recipe.archive();
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
var error = result.unsafeGetError();
|
||||||
|
assertThat(error).isInstanceOf(RecipeError.InvalidStatusTransition.class);
|
||||||
|
var transition = (RecipeError.InvalidStatusTransition) error;
|
||||||
|
assertThat(transition.current()).isEqualTo(RecipeStatus.ARCHIVED);
|
||||||
|
assertThat(transition.target()).isEqualTo(RecipeStatus.ARCHIVED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@DisplayName("Equality")
|
@DisplayName("Equality")
|
||||||
class Equality {
|
class Equality {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue