1
0
Fork 0
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:
Sebastian Frick 2026-02-19 21:46:56 +01:00
parent 408813a5b5
commit 1e12353b9b
3 changed files with 327 additions and 0 deletions

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -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 {