diff --git a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/command/CommandRepository.java b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/command/CommandRepository.java index a9fa9e550582efd27f8840b3cae3d33f86ee3395..a9f57bac1d65ca9aba3af9671ab6d2efb2948558 100644 --- a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/command/CommandRepository.java +++ b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/command/CommandRepository.java @@ -35,6 +35,7 @@ import java.util.Optional; import java.util.stream.Stream; import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.ArrayUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.FindAndModifyOptions; import org.springframework.data.mongodb.core.MongoOperations; @@ -49,8 +50,6 @@ import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.stereotype.Repository; -import com.google.common.collect.ObjectArrays; - import de.ozgcloud.command.Command; import de.ozgcloud.command.CommandStatus; @@ -58,7 +57,8 @@ import de.ozgcloud.command.CommandStatus; class CommandRepository { private static final String MONGODB_ID = "id"; - private static final String MONGODB_STATUS = "status"; + static final String MONGODB_STATUS = "status"; + private static final String MONGODB_REFERENCE_STATUS = "$status"; private static final String MONGODB_FINISHED_AT = "finishedAt"; private static final String MONGODB_RELATION_VERSION = "relationVersion"; private static final String MONGODB_ERROR_MSG = "errorMessage"; @@ -108,6 +108,10 @@ class CommandRepository { Command.class); } + public Command updateCommandStatusAndReturnPrevious(String commandId, CommandStatus status) { + return mongoOperations.findAndModify(queryById(commandId), new Update().set(MONGODB_STATUS, status), Command.class); + } + void setErrorMessage(String commandId, String errorMsg) { mongoOperations.updateFirst(queryById(commandId), new Update() @@ -202,12 +206,9 @@ class CommandRepository { mongoOperations.updateFirst(queryById(commandId), update, Command.class); } - public Optional<String> getNotFailedParentId(String commandId) { - return getParentId(commandId) - .map(this::queryNotFailedCommandById) - .map(this::includeIdAndClass) - .flatMap(this::findOne) - .map(Command::getId); + public boolean isCommandFailed(String commandId) { + return mongoOperations.exists(query(new Criteria().andOperator(criteriaById(commandId), where(MONGODB_STATUS).is(CommandStatus.ERROR))), + Command.class); } public Optional<String> getParentId(String commandId) { @@ -224,12 +225,6 @@ class CommandRepository { return MapUtils.getString(body, PersistedCommand.PROPERTY_PARENT_ID); } - Query queryNotFailedCommandById(String commandId) { - return query(new Criteria().andOperator( - where(MONGODB_ID).is(commandId), - where(MONGODB_STATUS).ne(CommandStatus.ERROR))); - } - Optional<Command> findOne(Query query) { return Optional.ofNullable(mongoOperations.findOne(query, Command.class)); } @@ -246,17 +241,21 @@ class CommandRepository { return mongoOperations.exists(buildQueryNotInStatus(parentId, CommandStatus.FINISHED, CommandStatus.REVOKED), Command.class); } - public Stream<String> findNotFailedSubCommandIds(String parentId) { - return mongoOperations.stream(includeIdAndClass(buildQueryNotInStatus(parentId, CommandStatus.ERROR)), Command.class).map(Command::getId); - } - - private Query buildQueryNotInStatus(String parentId, CommandStatus status, CommandStatus... statuses) { - Object[] excludeStatuses = ObjectArrays.concat(status, statuses); + Query buildQueryNotInStatus(String parentId, CommandStatus status, CommandStatus... statuses) { + Object[] excludeStatuses = ArrayUtils.add(statuses, status); return query(new Criteria().andOperator( - where(MONGODB_PARENT_ID).is(parentId), + buildIsParentIdCriteria(parentId), where(MONGODB_STATUS).nin(excludeStatuses))); } + public Stream<String> findSubCommandIds(String parentId) { + return mongoOperations.stream(includeIdAndClass(query(buildIsParentIdCriteria(parentId))), Command.class).map(Command::getId); + } + + private Criteria buildIsParentIdCriteria(String parentId) { + return where(MONGODB_PARENT_ID).is(parentId); + } + Query includeIdAndClass(Query query) { query.fields().include(MONGODB_CLASS, MONGODB_ID); return query; @@ -266,16 +265,30 @@ class CommandRepository { return mongoOperations.findAndModify(queryById(id), buildUpdateStatusRevoke(), FindAndModifyOptions.options().returnNew(true), Command.class); } + private UpdateDefinition buildUpdateStatusRevoke() { + var switchOperation = Switch.switchCases( + CaseOperator.when(Eq.valueOf(MONGODB_STATUS).equalToValue(CommandStatus.NEW)).then(CommandStatus.CANCELED), + CaseOperator.when(Eq.valueOf(MONGODB_STATUS).equalToValue(CommandStatus.PENDING)).then(CommandStatus.REVOKE_PENDING), + CaseOperator.when(Eq.valueOf(MONGODB_STATUS).equalToValue(CommandStatus.FINISHED)).then(CommandStatus.REVOKE_PENDING)) + .defaultTo(MONGODB_REFERENCE_STATUS); + return Aggregation.newUpdate(SetOperation.set(MONGODB_STATUS).toValue(switchOperation)); + } + + public Optional<Command> setRevokeStatusIfNotPending(String id) { + var updateCommand = mongoOperations.findAndModify(queryById(id), buildUpdateStatusRevokeIfNotPending(), + FindAndModifyOptions.options().returnNew(true), Command.class); + return Optional.ofNullable(updateCommand); + } + private Query queryById(String commandId) { return query(criteriaById(commandId)); } - private UpdateDefinition buildUpdateStatusRevoke() { + private UpdateDefinition buildUpdateStatusRevokeIfNotPending() { var switchOperation = Switch.switchCases( CaseOperator.when(Eq.valueOf(MONGODB_STATUS).equalToValue(CommandStatus.NEW)).then(CommandStatus.CANCELED), - CaseOperator.when(Eq.valueOf(MONGODB_STATUS).equalToValue(CommandStatus.PENDING)).then(CommandStatus.REVOKE_PENDING), - CaseOperator.when(Eq.valueOf(MONGODB_STATUS).equalToValue(CommandStatus.FINISHED)).then(CommandStatus.REVOKE_PENDING)) - .defaultTo("$status"); + CaseOperator.when(Eq.valueOf(MONGODB_STATUS).equalToValue(CommandStatus.FINISHED)).then(CommandStatus.REVOKE_PENDING) + ).defaultTo(MONGODB_REFERENCE_STATUS); return Aggregation.newUpdate(SetOperation.set(MONGODB_STATUS).toValue(switchOperation)); } diff --git a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/command/CommandService.java b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/command/CommandService.java index d8db39506c8338dbebe21a79985a5d3f7cfccfb1..2f661a37b47f0da8143fdd5332e8385da4ecb1e5 100644 --- a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/command/CommandService.java +++ b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/command/CommandService.java @@ -24,7 +24,6 @@ package de.ozgcloud.vorgang.command; import static de.ozgcloud.vorgang.command.PersistedCommand.*; -import static java.util.Objects.*; import java.time.ZonedDateTime; import java.util.Collection; @@ -34,10 +33,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.function.Consumer; import java.util.stream.Stream; -import org.apache.commons.lang3.StringUtils; +import org.apache.commons.collections4.MapUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -55,6 +53,7 @@ import de.ozgcloud.vorgang.callcontext.CallContextUser; import de.ozgcloud.vorgang.callcontext.CurrentUserService; import de.ozgcloud.vorgang.callcontext.User; import de.ozgcloud.vorgang.common.errorhandling.NotFoundException; +import de.ozgcloud.vorgang.common.errorhandling.RevokeFailedException; import lombok.NonNull; @Service @@ -117,52 +116,85 @@ public class CommandService { public void setCommandFinished(String commandId, String createdResource) { var command = getById(commandId); - if (command.getStatus() == CommandStatus.REVOKE_PENDING) { + if (shouldRevoke(command)) { + repository.setRevokeStatus(commandId); publishRevokeCommandEvent(command); return; } - Optional.ofNullable(createdResource).ifPresentOrElse( - resource -> repository.finishCommand(commandId, resource), + Optional.ofNullable(createdResource).ifPresentOrElse(resource -> repository.finishCommand(commandId, resource), () -> repository.finishCommand(commandId)); - getCompletableParentId(commandId).filter(parentId -> !repository.existsNotFinishedSubCommands(parentId)) - .ifPresent(this::publishCommandExecutedEvent); + getCompletableParentId(command).ifPresent(this::publishCommandExecutedEvent); } - Optional<String> getCompletableParentId(String commandId) { - return repository.getParentId(commandId).filter(repository::isCompleteIfSubsCompleted); + boolean shouldRevoke(Command command) { + return command.getStatus() == CommandStatus.REVOKE_PENDING || isParentCommandFailed(command); + } + + boolean isParentCommandFailed(Command command) { + return getParentId(command).filter(repository::isCommandFailed).isPresent(); + } + + Optional<String> getCompletableParentId(Command command) { + return getParentId(command).filter(repository::isCompleteIfSubsCompleted) + .filter(parentId -> !repository.existsNotFinishedSubCommands(parentId)); + } + + Optional<String> getParentId(Command command) { + return Optional.ofNullable(command.getBodyObject()).map(body -> MapUtils.getString(body, PROPERTY_PARENT_ID)); } public void publishCommandExecutedEvent(String commandId) { findById(commandId).ifPresent(command -> publisher.publishEvent(new CommandExecutedEvent(command))); } - public void setCommandError(String id, String errorMessage) { - repository.setErrorMessage(id, errorMessage); - var publishCommandFailedEvent = createCommandFailedEventPublisher("Command %s failed: %s".formatted(id, errorMessage)); - repository.findNotFailedSubCommandIds(id).filter(notFailedCommandId -> !StringUtils.equals(notFailedCommandId, id)) - .forEach(publishCommandFailedEvent); - repository.getNotFailedParentId(id).ifPresent(publishCommandFailedEvent); + public void setCommandError(String commandId, String errorMessage) { + repository.setErrorMessage(commandId, errorMessage); + repository.getParentId(commandId).map(this::setErrorStatus).filter(this::notErrorStatus) + .map(Command::getId).ifPresent(parentId -> handleCommandError(commandId, parentId)); + } + + Command setErrorStatus(String commandId) { + return Optional.ofNullable(repository.updateCommandStatusAndReturnPrevious(commandId, CommandStatus.ERROR)) + .orElseThrow(() -> new NotFoundException(Command.class, commandId)); } - Consumer<String> createCommandFailedEventPublisher(String errorMessage) { - return commandId -> publisher.publishEvent(new CommandFailedEvent(commandId, errorMessage)); + boolean notErrorStatus(Command command) { + return command.getStatus() != CommandStatus.ERROR; + } + + void handleCommandError(String commandId, String parentId) { + publishCommandFailedEvent(parentId, "Command %s failed because subcommand %s failed".formatted(parentId, commandId)); + revokeSubCommands(parentId); + } + + void publishCommandFailedEvent(String commandId, String errorMessage) { + publisher.publishEvent(new CommandFailedEvent(commandId, errorMessage)); + } + + void revokeSubCommands(String parentId) { + repository.findSubCommandIds(parentId).forEach(this::revokeCommandSilent); } public void setCommandRevoked(String commandId) { repository.updateCommandStatus(commandId, CommandStatus.REVOKED); } - public Command revokeCommand(final String commandId) { - var updatedCommand = setRevokeStatus(commandId); - if (isNull(updatedCommand)) { - throw new NotFoundException(Command.class, commandId); + public Command revokeCommand(String commandId) { + var updatedCommand = revokeCommandSilent(commandId).orElseThrow(() -> new NotFoundException(Command.class, commandId)); + if (!isRevokePending(updatedCommand)) { + throw new RevokeFailedException(commandId, "Unexpected status '%s'. (expected: REVOKE_PENDING)".formatted(updatedCommand.getStatus())); } - publishRevokeCommandEvent(updatedCommand); return updatedCommand; } - public Command setRevokeStatus(String commandId) { - return repository.setRevokeStatus(commandId); + Optional<Command> revokeCommandSilent(String commandId) { + var updatedCommand = repository.setRevokeStatusIfNotPending(commandId); + updatedCommand.filter(this::isRevokePending).ifPresent(this::publishRevokeCommandEvent); + return updatedCommand; + } + + boolean isRevokePending(Command command) { + return command.getStatus() == CommandStatus.REVOKE_PENDING; } void publishRevokeCommandEvent(Command command) { @@ -194,16 +226,16 @@ public class CommandService { } public Stream<Command> createSubCommands(CreateSubCommandsRequest request) { - var parentCommand = getNotFailedCommand(request.getParentId()); + var parentCommand = getPendingCommand(request.getParentId()); repository.addSubCommands(parentCommand.getId(), buildSubCommandValues(request)); return request.getSubCommands().stream().map(subCommand -> buildCreateCommandRequest(subCommand, parentCommand)) .map(this::createCommand); } - Command getNotFailedCommand(String commandId) { + Command getPendingCommand(String commandId) { var command = getById(commandId); - if (command.getStatus() == CommandStatus.ERROR) { - throw new TechnicalException("Cannot add sub commands to failed command ('%s')".formatted(commandId)); + if (command.getStatus() != CommandStatus.PENDING) { + throw new TechnicalException("Cannot add sub commands to command '%s'. Parent command must have status PENDING".formatted(commandId)); } return command; } diff --git a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/common/errorhandling/RevokeFailedException.java b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/common/errorhandling/RevokeFailedException.java new file mode 100644 index 0000000000000000000000000000000000000000..133b8970b815aaf02ad3284a24babd821ea13b0a --- /dev/null +++ b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/common/errorhandling/RevokeFailedException.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.vorgang.common.errorhandling; + +import java.util.Map; + +import de.ozgcloud.common.errorhandling.FunctionalErrorCode; + +public class RevokeFailedException extends FunctionalException { + + static final String KEY_COMMAND_ID = "Id"; + + private static final String MESSAGE_TEMPLATE = "Can not revoke command with id '%s'. %s"; + private static final FunctionalErrorCode ERROR_CODE = () -> "error.revoke_failed"; + + public RevokeFailedException(String commandId, String message) { + super(MESSAGE_TEMPLATE.formatted(commandId, message), ERROR_CODE, Map.of(KEY_COMMAND_ID, commandId)); + } +} diff --git a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandITCase.java b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandITCase.java index 90a0fda17c0b5223d9e2745acfb09dc8cc8ece93..cbb89a295b4dde09f94322dc54cad4cf97f7c172 100644 --- a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandITCase.java +++ b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandITCase.java @@ -309,7 +309,7 @@ class CommandITCase { mongoOperations.dropCollection(Vorgang.class); mongoOperations.save(VorgangTestFactory.createBuilder().status(Status.ANGENOMMEN).build()); - mongoOperations.save(CommandTestFactory.createBuilder() + mongoOperations.save(CommandTestFactory.createBuilder().status(CommandStatus.FINISHED) .relationId(VorgangTestFactory.ID).relationVersion(VorgangTestFactory.VERSION - 1) .previousState(Map.of(Vorgang.MONGODB_FIELDNAME_STATUS, Status.NEU)).build()); } @@ -369,7 +369,7 @@ class CommandITCase { void persistVorgangWithVorgangVerwerfenCommand() { mongoOperations.save(VorgangTestFactory.createBuilder().status(Status.VERWORFEN).build()); - mongoOperations.save(CommandTestFactory.createBuilder().relationId(VorgangTestFactory.ID) + mongoOperations.save(CommandTestFactory.createBuilder().status(CommandStatus.FINISHED).relationId(VorgangTestFactory.ID) .relationVersion(VorgangTestFactory.VERSION - 1).order(Order.VORGANG_VERWERFEN.toString()) .previousState(Map.of(Vorgang.MONGODB_FIELDNAME_STATUS, Status.IN_BEARBEITUNG)) .build()); diff --git a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandRepositoryITCase.java b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandRepositoryITCase.java index a5ad4922d9ac97cd7dff429a2a0634f280f4313d..6f99e9b91bef7f1664e80194b2755173a0c1da37 100644 --- a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandRepositoryITCase.java +++ b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandRepositoryITCase.java @@ -246,6 +246,31 @@ class CommandRepositoryITCase { } } + @Nested + class TestUpdateCommandStatusAndReturnPrevious { + + @BeforeEach + void persistVorgangWithCommand() { + repository.save(CommandTestFactory.create()); + } + + @Test + void shouldUpdateStatus() { + repository.updateCommandStatusAndReturnPrevious(CommandTestFactory.ID, CommandStatus.ERROR); + + var command = mongoOperations.findById(CommandTestFactory.ID, Command.class); + assertThat(command.getStatus()).isEqualTo(CommandStatus.ERROR); + } + + @Test + void shouldReturnPreviousCommand() { + Command previousCommand = repository.updateCommandStatusAndReturnPrevious(CommandTestFactory.ID, CommandStatus.ERROR); + + assertThat(previousCommand.getStatus()).isEqualTo(CommandStatus.PENDING); + } + + } + @Nested class TestGetPendingCommands { @@ -417,7 +442,7 @@ class CommandRepositoryITCase { } @Nested - class TestGetNotFailedParentId { + class TestIsCommandFailed { @BeforeEach void init() { @@ -426,28 +451,22 @@ class CommandRepositoryITCase { @Test void shouldReturnParentId() { - var parentId = mongoOperations.save(CommandTestFactory.createBuilder().bodyObject(Map.of( + var commandId = mongoOperations.save(CommandTestFactory.createBuilder().bodyObject(Map.of( PersistedCommand.PROPERTY_COMPLETE_IF_SUBS_COMPLETED, true)).build()).getId(); - var commandId = mongoOperations.save( - CommandTestFactory.createBuilder().id(null).bodyObject(Map.of(PersistedCommand.PROPERTY_PARENT_ID, parentId)).build()) - .getId(); - var result = repository.getNotFailedParentId(commandId); + var result = repository.isCommandFailed(commandId); - assertThat(result).contains(parentId); + assertThat(result).isFalse(); } @Test void shouldReturnEmpty() { - var parentId = mongoOperations.save(CommandTestFactory.createBuilder().status(CommandStatus.ERROR).bodyObject(Map.of( + var commandId = mongoOperations.save(CommandTestFactory.createBuilder().status(CommandStatus.ERROR).bodyObject(Map.of( PersistedCommand.PROPERTY_COMPLETE_IF_SUBS_COMPLETED, true)).build()).getId(); - var commandId = mongoOperations.save( - CommandTestFactory.createBuilder().id(null).bodyObject(Map.of(PersistedCommand.PROPERTY_PARENT_ID, parentId)).build()) - .getId(); - var result = repository.getNotFailedParentId(commandId); + var result = repository.isCommandFailed(commandId); - assertThat(result).isEmpty(); + assertThat(result).isTrue(); } } @@ -562,8 +581,8 @@ class CommandRepositoryITCase { private static final String PARENT_ID = "parent-id"; - private final PersistedCommandBuilder commandWithParentIdBuilder = CommandTestFactory.createBuilder().id(null).bodyObject(Map.of( - PersistedCommand.PROPERTY_PARENT_ID, PARENT_ID)); + private static final Command COMMAND_WITH_PARENT_ID = CommandTestFactory.createBuilder().id(null).bodyObject(Map.of( + PersistedCommand.PROPERTY_PARENT_ID, PARENT_ID)).build(); @BeforeEach void init() { @@ -571,30 +590,19 @@ class CommandRepositoryITCase { } @Test - void shouldIgnoreErrorStatus() { - mongoOperations.save(commandWithParentIdBuilder.status(CommandStatus.ERROR).build()); + void shouldFindSubCommandIds() { + var commandId = mongoOperations.save(COMMAND_WITH_PARENT_ID).getId(); - var result = repository.findNotFailedSubCommandIds(PARENT_ID); + var result = repository.findSubCommandIds(PARENT_ID); - assertThat(result).isEmpty(); - } - - @DisplayName("should find commands") - @ParameterizedTest(name = "with status {0}") - @EnumSource(value = CommandStatus.class, names = { "ERROR" }, mode = EnumSource.Mode.EXCLUDE) - void shouldFindCommands(CommandStatus status) { - mongoOperations.save(commandWithParentIdBuilder.status(status).build()); - - var result = repository.findNotFailedSubCommandIds(PARENT_ID); - - assertThat(result).hasSize(1); + assertThat(result).hasSize(1).first().isEqualTo(commandId); } @Test void shouldReturnEmpty() { - mongoOperations.save(commandWithParentIdBuilder.build()); + mongoOperations.save(COMMAND_WITH_PARENT_ID); - var result = repository.findNotFailedSubCommandIds("other-parent-id"); + var result = repository.findSubCommandIds("other-parent-id"); assertThat(result).isEmpty(); } @@ -645,4 +653,45 @@ class CommandRepositoryITCase { assertThat(result).isPresent().get().extracting(Command::getStatus).isEqualTo(status); } } + + @Nested + class TestSetRevokeStatusIfNotPending { + + @Test + void shouldSetCanceled() { + var command = createCommand(CommandStatus.NEW); + + repository.setRevokeStatusIfNotPending(command.getId()); + + assertThat(getCommandStatus(command)).isEqualTo(CommandStatus.CANCELED); + } + + @Test + void shouldSetRevokePendingWhenFinished() { + var command = createCommand(CommandStatus.FINISHED); + + repository.setRevokeStatusIfNotPending(command.getId()); + + assertThat(getCommandStatus(command)).isEqualTo(CommandStatus.REVOKE_PENDING); + } + + @DisplayName("should not update when") + @ParameterizedTest(name = "status is {0}") + @EnumSource(value = CommandStatus.class, names = { "NEW", "FINISHED" }, mode = EnumSource.Mode.EXCLUDE) + void shouldNotUpdate(CommandStatus status) { + var command = createCommand(status); + + repository.setRevokeStatusIfNotPending(command.getId()); + + assertThat(getCommandStatus(command)).isEqualTo(status); + } + + private Command createCommand(CommandStatus status) { + return mongoOperations.save(CommandTestFactory.createBuilder().id(null).status(status).build()); + } + + private CommandStatus getCommandStatus(Command command) { + return mongoOperations.findById(command.getId(), Command.class).getStatus(); + } + } } \ No newline at end of file diff --git a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandServiceITCase.java b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandServiceITCase.java index 4e0eb1bf86891204eae2eaeec75a9dc8689e2649..36429601be30bae6182023940b10249ba1bd6a6f 100644 --- a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandServiceITCase.java +++ b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandServiceITCase.java @@ -28,6 +28,7 @@ import static org.awaitility.Awaitility.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -115,12 +116,14 @@ class CommandServiceITCase { @Nested class TestVorgangCommandFinished { + private Vorgang vorgang; + @BeforeEach void persistVorgang() { mongoOperations.dropCollection(Vorgang.class); mongoOperations.dropCollection(Command.class); - mongoOperations.save(VorgangTestFactory.create()); + vorgang = mongoOperations.save(VorgangTestFactory.create()); mongoOperations.save(CommandTestFactory.createBuilder().relationId(VorgangTestFactory.ID).build()); } @@ -136,6 +139,37 @@ class CommandServiceITCase { assertThat(command.getStatus()).isEqualTo(CommandStatus.FINISHED); assertThat(command.getRelationId()).isEqualTo(VorgangTestFactory.ID); } + + @Test + void shouldRevokeFinishedCommand() { + var commandId = saveSubCommandWithFailedParent(); + + commandService.setCommandFinished(commandId); + + await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> { + var commandStatus = mongoOperations.findById(commandId, Command.class).getStatus(); + assertThat(commandStatus).isEqualTo(CommandStatus.REVOKED); + }); + } + + private String saveSubCommandWithFailedParent() { + var parentId = mongoOperations.save(createFailedParentCommand()).getId(); + return mongoOperations.save(createSubCommand(parentId)).getId(); + } + + private Command createFailedParentCommand() { + return CommandTestFactory.createBuilder().id(null).status(CommandStatus.ERROR) + .bodyObject(Map.of( + PersistedCommand.PROPERTY_EXECUTION_MODE, SubCommandExecutionMode.PARALLEL.name(), + PersistedCommand.PROPERTY_COMPLETE_IF_SUBS_COMPLETED, true + )).build(); + } + + private Command createSubCommand(String parentId) { + return CommandTestFactory.createBuilder().id(null).vorgangId(vorgang.getId()) + .previousState(Map.of(CommandRepository.MONGODB_STATUS, Vorgang.Status.NEU.name())) + .bodyObject(Map.of(PersistedCommand.PROPERTY_PARENT_ID, parentId)).build(); + } } @Nested diff --git a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandServiceTest.java b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandServiceTest.java index 04b5e6f9dfb000b85d9685a792608d81625583b5..85714d30f81e92ba77ce0c7f575e89dcc04343fa 100644 --- a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandServiceTest.java +++ b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/CommandServiceTest.java @@ -39,6 +39,8 @@ 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.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; @@ -48,7 +50,6 @@ import org.springframework.context.ApplicationEventPublisher; import de.ozgcloud.command.Command; import de.ozgcloud.command.CommandExecutedEvent; -import de.ozgcloud.command.CommandFailedEvent; import de.ozgcloud.command.CommandStatus; import de.ozgcloud.command.RevokeCommandEvent; import de.ozgcloud.common.errorhandling.TechnicalException; @@ -57,6 +58,7 @@ import de.ozgcloud.vorgang.callcontext.CallContextUserTestFactory; import de.ozgcloud.vorgang.callcontext.CurrentUserService; import de.ozgcloud.vorgang.callcontext.UserTestFactory; import de.ozgcloud.vorgang.common.errorhandling.NotFoundException; +import de.ozgcloud.vorgang.common.errorhandling.RevokeFailedException; import de.ozgcloud.vorgang.vorgang.VorgangTestFactory; class CommandServiceTest { @@ -210,53 +212,73 @@ class CommandServiceTest { } @Nested - class TestRevokeCommand { + class TestRevokePendingCommand { - private Command command; + private static final Command REVOKE_PENDING_COMMAND = CommandTestFactory.createBuilder().status(CommandStatus.REVOKE_PENDING).build(); @BeforeEach void init() { - command = CommandTestFactory.createBuilder().status(CommandStatus.REVOKE_PENDING).build(); - doReturn(command).when(service).getById(anyString()); + doReturn(true).when(service).shouldRevoke(any()); + doReturn(REVOKE_PENDING_COMMAND).when(service).getById(anyString()); + } + + @Test + void shouldCallIsRevokeCommand() { + setCommandFinished(); + + verify(service).shouldRevoke(REVOKE_PENDING_COMMAND); + } + + @Test + void shouldCallRepository() { + setCommandFinished(); + + verify(repository).setRevokeStatus(CommandTestFactory.ID); } @Test void shouldCallPublishRevokeCommandEvent() { - service.setCommandFinished(CommandTestFactory.ID); + setCommandFinished(); - verify(service).publishRevokeCommandEvent(command); + verify(service).publishRevokeCommandEvent(REVOKE_PENDING_COMMAND); } @Test void shouldNotCallFinishCommandIfRevokePending() { - service.setCommandFinished(CommandTestFactory.ID); + setCommandFinished(); verify(repository, never()).finishCommand(anyString()); verify(repository, never()).finishCommand(anyString(), anyString()); } + void setCommandFinished() { + service.setCommandFinished(CommandTestFactory.ID, CommandTestFactory.CREATED_RESOURCE); + } } @Nested class TestFinishCommand { + private static final Command FINISHED_COMMAND = CommandTestFactory.createBuilder().status(CommandStatus.FINISHED).build(); + @BeforeEach void init() { - doReturn(CommandTestFactory.create()).when(service).getById(anyString()); + doReturn(false).when(service).isParentCommandFailed(any()); + doReturn(FINISHED_COMMAND).when(service).getById(anyString()); } @Test - void shouldCallGetCommand() { + void shouldCallSetCommandFinish() { service.setCommandFinished(CommandTestFactory.ID); - verify(service).getById(CommandTestFactory.ID); + verify(service).setCommandFinished(CommandTestFactory.ID, null); } @Test - void shouldCallFinishCommand() { - service.setCommandFinished(CommandTestFactory.ID); + void shouldCallGetCommand() { + setCommandFinished(); - verify(service).setCommandFinished(CommandTestFactory.ID, null); + verify(service).getById(CommandTestFactory.ID); } @Test @@ -268,105 +290,219 @@ class CommandServiceTest { @Test void shouldCallRepositoryFinishCommandWithCreatedResource() { - service.setCommandFinished(CommandTestFactory.ID, CommandTestFactory.CREATED_RESOURCE); + setCommandFinished(); verify(repository).finishCommand(CommandTestFactory.ID, CommandTestFactory.CREATED_RESOURCE); } @Test void shouldCallGetCompletableParentId() { - service.setCommandFinished(CommandTestFactory.ID); + setCommandFinished(); - verify(service).getCompletableParentId(CommandTestFactory.ID); + verify(service).getCompletableParentId(FINISHED_COMMAND); } @Test - void shouldCallExistsNotFinishedSubCommands() { + void shouldCallPublishCommandExecutedEvent() { var parentId = "parent-id"; doReturn(Optional.of(parentId)).when(service).getCompletableParentId(any()); - service.setCommandFinished(CommandTestFactory.ID, CommandTestFactory.CREATED_RESOURCE); + setCommandFinished(); - verify(repository).existsNotFinishedSubCommands(parentId); + verify(service).publishCommandExecutedEvent(parentId); } + @DisplayName("should not call publishCommandExecutedEvent when no completable parent id") @Test - void shouldCallPublishCommandExecutedEvent() { - var parentId = "parent-id"; - doReturn(Optional.of(parentId)).when(service).getCompletableParentId(any()); + void shouldNotPublishWhenNoParentId() { + doReturn(Optional.empty()).when(service).getCompletableParentId(any()); + + setCommandFinished(); + verify(service, never()).publishCommandExecutedEvent(anyString()); + } + + void setCommandFinished() { service.setCommandFinished(CommandTestFactory.ID, CommandTestFactory.CREATED_RESOURCE); + } + } + } - verify(service).publishCommandExecutedEvent(parentId); + @Nested + class TestShouldRevoke { + + @Test + void shouldReturnTrueWhenRevokePending() { + var command = CommandTestFactory.createBuilder().status(CommandStatus.REVOKE_PENDING).build(); + + var result = service.shouldRevoke(command); + + assertThat(result).isTrue(); + } + + @Test + void shouldRevokeTrueWhenParentFailed() { + doReturn(true).when(service).isParentCommandFailed(any()); + + var result = service.shouldRevoke(CommandTestFactory.create()); + + assertThat(result).isTrue(); + } + } + + @Nested + class TestIsParentCommandFailed { + + @Test + void shouldCallGetParentId() { + var command = CommandTestFactory.create(); + + service.isParentCommandFailed(command); + + verify(service).getParentId(command); + } + + @Test + void shouldReturnFalseWhenParentMissing() { + doReturn(Optional.empty()).when(service).getParentId(any()); + + var result = isParentCommandFailed(); + + assertThat(result).isFalse(); + } + + @Nested + class TestWithParent { + + private static final String PARENT_ID = "parent-id"; + + @BeforeEach + void init() { + doReturn(Optional.of(PARENT_ID)).when(service).getParentId(any()); } - @DisplayName("should not call publishCommandExecutedEvent when no parent id") @Test - void shouldNotCallPublishCommandExecutedEvent() { - doReturn(Optional.empty()).when(service).getCompletableParentId(any()); + void shouldCallRepository() { + isParentCommandFailed(); - service.setCommandFinished(CommandTestFactory.ID, CommandTestFactory.CREATED_RESOURCE); - - verify(service, never()).publishCommandExecutedEvent(anyString()); + verify(repository).isCommandFailed(PARENT_ID); } - @DisplayName("should not call publishCommandExecutedEvent when not all subcommands are finished") @Test - void shouldNotCallPublishCommandExecutedEvent1() { - var parentId = "parent-id"; - doReturn(Optional.of(parentId)).when(service).getCompletableParentId(any()); - when(repository.existsNotFinishedSubCommands(any())).thenReturn(true); + void shouldReturnTrueWhenParentNotFailed() { + when(repository.isCommandFailed(any())).thenReturn(true); - service.setCommandFinished(CommandTestFactory.ID, CommandTestFactory.CREATED_RESOURCE); + var result = isParentCommandFailed(); - verify(service, never()).publishCommandExecutedEvent(anyString()); + assertThat(result).isTrue(); + } + + @Test + void shouldReturnFalseWhenParentFailed() { + var result = isParentCommandFailed(); + + assertThat(result).isFalse(); } } + + boolean isParentCommandFailed() { + return service.isParentCommandFailed(CommandTestFactory.create()); + } } @Nested class TestGetCompletableParentId { + private static final Command COMMAND = CommandTestFactory.create(); + @Test - void shouldGetParentId() { - service.getCompletableParentId(CommandTestFactory.ID); + void shouldCallGetParentId() { + service.getCompletableParentId(COMMAND); - verify(repository).getParentId(CommandTestFactory.ID); + verify(service).getParentId(COMMAND); } @Test - void shouldFilterIfParentCompletableBySubCommands() { - var expectedParentId = "parent-id"; - when(repository.getParentId(anyString())).thenReturn(Optional.of(expectedParentId)); + void shouldReturnEmptyWhenNoParentId() { + doReturn(Optional.empty()).when(service).getParentId(any()); - service.getCompletableParentId(CommandTestFactory.ID); + var result = service.getCompletableParentId(COMMAND); - verify(repository).isCompleteIfSubsCompleted(expectedParentId); + assertThat(result).isEmpty(); } + @Nested + class TestCompleteIfSubsCompleted { + + private static final String PARENT_ID = "parent-id"; + + @BeforeEach + void init() { + doReturn(Optional.of(PARENT_ID)).when(service).getParentId(any()); + } + + @Test + void shouldCallIsCompleteIfSubsCompleted() { + service.getCompletableParentId(COMMAND); + + verify(repository).isCompleteIfSubsCompleted(PARENT_ID); + } + + @Test + void shouldCallExistsNotFinishedSubCommands() { + when(repository.isCompleteIfSubsCompleted(anyString())).thenReturn(true); + + service.getCompletableParentId(COMMAND); + + verify(repository).existsNotFinishedSubCommands(PARENT_ID); + } + + @Test + void shouldReturnParentId() { + when(repository.isCompleteIfSubsCompleted(anyString())).thenReturn(true); + + var result = service.getCompletableParentId(COMMAND); + + assertThat(result).contains(PARENT_ID); + } + + @Test + void shouldReturnEmptyWhenNotCompletableBySubCommands() { + var result = service.getCompletableParentId(COMMAND); + + assertThat(result).isEmpty(); + } + } + + } + + @Nested + class TestGetParentId { + @Test - void shouldReturnParentId() { - var expectedParentId = "parent-id"; - doReturn(Optional.of(expectedParentId)).when(repository).getParentId(anyString()); - when(repository.isCompleteIfSubsCompleted(anyString())).thenReturn(true); + void shouldReturnResult() { + var parentId = "parent-id"; + var command = CommandTestFactory.createBuilder().bodyObject(Map.of(PersistedCommand.PROPERTY_PARENT_ID, parentId)).build(); - var result = service.getCompletableParentId(CommandTestFactory.ID); + var result = service.getParentId(command); - assertThat(result).contains(expectedParentId); + assertThat(result).contains(parentId); } @Test - void shouldReturnEmptyWhenNoParentId() { - var result = service.getCompletableParentId(CommandTestFactory.ID); + void shouldReturnEmptyWhenNoBodyObject() { + var command = CommandTestFactory.createBuilder().bodyObject(null).build(); + + var result = service.getParentId(command); assertThat(result).isEmpty(); } @Test - void shouldReturnEmptyWhenNotCompletableBySubCommands() { - doReturn(Optional.of("parent-id")).when(repository).getParentId(anyString()); + void shouldReturnEmptyWhenNoParentId() { + var command = CommandTestFactory.createBuilder().bodyObject(Map.of()).build(); - var result = service.getCompletableParentId(CommandTestFactory.ID); + var result = service.getParentId(command); assertThat(result).isEmpty(); } @@ -402,111 +538,171 @@ class CommandServiceTest { class TestSetCommandError { private static final String ERROR_MESSAGE = "error message"; - - @Captor - private ArgumentCaptor<CommandFailedEvent> eventCaptor; + private static final String PARENT_ID = "parent-id"; @Test - void shouldCallRepository() { + void shouldCallRepositorySetErrorMessage() { service.setCommandError(CommandTestFactory.ID, ERROR_MESSAGE); verify(repository).setErrorMessage(CommandTestFactory.ID, ERROR_MESSAGE); } @Test - void shouldCallFindNotFailedSubCommandIds() { + void shouldCallRepositoryGetParentId() { service.setCommandError(CommandTestFactory.ID, ERROR_MESSAGE); - verify(repository).findNotFailedSubCommandIds(CommandTestFactory.ID); + verify(repository).getParentId(CommandTestFactory.ID); } - @Test - void shouldPublishCommandFailedEvent() { - var notFailedCommandId = "not-failed-command-id"; - when(repository.findNotFailedSubCommandIds(anyString())).thenReturn(Stream.of(notFailedCommandId)); + @Nested + class TestFailParentAndRevokeSubCommands { - service.setCommandError(CommandTestFactory.ID, ERROR_MESSAGE); + private static final Command COMMAND_BEFORE_SET_ERROR_STATUS = CommandTestFactory.createBuilder().id(PARENT_ID).build(); - verify(publisher).publishEvent(eventCaptor.capture()); - assertThat(eventCaptor.getValue().getSource()).isEqualTo(notFailedCommandId); + @BeforeEach + void init() { + when(repository.getParentId(anyString())).thenReturn(Optional.of(PARENT_ID)); + doReturn(COMMAND_BEFORE_SET_ERROR_STATUS).when(service).setErrorStatus(anyString()); + } + + @Test + void shouldCallSetErrorStatus() { + service.setCommandError(CommandTestFactory.ID, ERROR_MESSAGE); + + verify(service).setErrorStatus(PARENT_ID); + } + + @Test + void shouldCallNotErrorStatus() { + service.setCommandError(CommandTestFactory.ID, ERROR_MESSAGE); + + verify(service).notErrorStatus(COMMAND_BEFORE_SET_ERROR_STATUS); + } + + @Test + void shouldCallHandleCommandError() { + service.setCommandError(CommandTestFactory.ID, ERROR_MESSAGE); + + verify(service).handleCommandError(CommandTestFactory.ID, PARENT_ID); + } } - @Test - void shouldFilterOwnId() { - when(repository.findNotFailedSubCommandIds(anyString())).thenReturn(Stream.of(CommandTestFactory.ID)); + @Nested + class TestParentIdMissing { - service.setCommandError(CommandTestFactory.ID, ERROR_MESSAGE); + @BeforeEach + void init() { + doReturn(Optional.empty()).when(repository).getParentId(anyString()); + } - verify(publisher, never()).publishEvent(any()); + @Test + void shouldNotCallHandleCommandError() { + service.setCommandError(CommandTestFactory.ID, ERROR_MESSAGE); + + verify(service, never()).handleCommandError(anyString(), anyString()); + } } - @Test - void shouldNotCallPublishCommandFailedEventIfNoNotFailedSubCommands() { - service.setCommandError(CommandTestFactory.ID, ERROR_MESSAGE); + @Nested + class TestParentAlreadyFailed { + + @Mock + private Command failedParentCommand; + + @BeforeEach + void init() { + when(repository.getParentId(anyString())).thenReturn(Optional.of(PARENT_ID)); + doReturn(failedParentCommand).when(service).setErrorStatus(anyString()); + doReturn(false).when(service).notErrorStatus(any()); + } - verify(publisher, never()).publishEvent(any()); + @Test + void shouldNotCallHandleCommandError() { + service.setCommandError(CommandTestFactory.ID, ERROR_MESSAGE); + + verify(service, never()).handleCommandError(anyString(), anyString()); + } } + } + + @Nested + class TestSetErrorStatus { + @Test - void shouldCallGetNotFailedParentId() { - service.setCommandError(CommandTestFactory.ID, ERROR_MESSAGE); + void shouldCallRepositoryUpdateStatus() { + when(repository.updateCommandStatusAndReturnPrevious(anyString(), any())).thenReturn(CommandTestFactory.create()); + + service.setErrorStatus(CommandTestFactory.ID); - verify(repository).getNotFailedParentId(CommandTestFactory.ID); + verify(repository).updateCommandStatusAndReturnPrevious(CommandTestFactory.ID, CommandStatus.ERROR); } @Test - void shouldCallPublishCommandFailedEventForParent() { - var parentId = "parent-id"; - when(repository.getNotFailedParentId(anyString())).thenReturn(Optional.of(parentId)); + void shouldReturnCommand() { + var command = CommandTestFactory.create(); + when(repository.updateCommandStatusAndReturnPrevious(anyString(), any())).thenReturn(command); - service.setCommandError(CommandTestFactory.ID, ERROR_MESSAGE); + var result = service.setErrorStatus(CommandTestFactory.ID); - verify(publisher).publishEvent(eventCaptor.capture()); - assertThat(eventCaptor.getValue().getSource()).isEqualTo(parentId); + assertThat(result).isSameAs(command); } @Test - void shouldNotCallPublishCommandFailedEventForParentIfNoParent() { - service.setCommandError(CommandTestFactory.ID, ERROR_MESSAGE); - - verify(publisher, never()).publishEvent(any()); + void shouldThrowExceptionIfNotFound() { + assertThrows(NotFoundException.class, () -> service.setErrorStatus(CommandTestFactory.ID)); } } @Nested - class TestCreateCommandFailedEventPublisher { + class TestNotErrorStatus { - private static final String EXPECTED_ERROR_MESSAGE = "error message"; + @DisplayName("should return true when") + @ParameterizedTest(name = "command status is {0}") + @EnumSource(value = CommandStatus.class, names = { "ERROR" }, mode = EnumSource.Mode.EXCLUDE) + void shouldReturnTrue(CommandStatus status) { + var command = CommandTestFactory.createBuilder().status(status).build(); - @Captor - private ArgumentCaptor<CommandFailedEvent> eventCaptor; + var result = service.notErrorStatus(command); + + assertThat(result).isTrue(); + } @Test - void shouldPublishCommandFailedEvent() { - publishCommandFailedEvent(); + void shouldReturnFalse() { + var command = CommandTestFactory.createBuilder().status(CommandStatus.ERROR).build(); - verify(publisher).publishEvent(eventCaptor.capture()); - assertThat(eventCaptor.getValue()).isInstanceOf(CommandFailedEvent.class); + var result = service.notErrorStatus(command); + + assertThat(result).isFalse(); } + } + + @Nested + class TestHandleCommandError { + + private static final String PARENT_ID = "parent-id"; + + @Captor + private ArgumentCaptor<String> errorMessageCaptor; @Test - void shouldSetCommandIdInFailedEvent() { - publishCommandFailedEvent(); + void shouldCallPublishCommandFailedEvent() { + handleCommandError(); - verify(publisher).publishEvent(eventCaptor.capture()); - assertThat(eventCaptor.getValue().getErrorMessage()).isEqualTo(EXPECTED_ERROR_MESSAGE); + verify(service).publishCommandFailedEvent(eq(PARENT_ID), errorMessageCaptor.capture()); + assertThat(errorMessageCaptor.getValue()).contains(PARENT_ID, CommandTestFactory.ID); } @Test - void shouldSetErrorMessageInFailedEvent() { - publishCommandFailedEvent(); + void shouldCallRevokeSubCommands() { + handleCommandError(); - verify(publisher).publishEvent(eventCaptor.capture()); - assertThat(eventCaptor.getValue().getErrorMessage()).isEqualTo(EXPECTED_ERROR_MESSAGE); + verify(service).revokeSubCommands(PARENT_ID); } - private void publishCommandFailedEvent() { - service.createCommandFailedEventPublisher(EXPECTED_ERROR_MESSAGE).accept(CommandTestFactory.ID); + private void handleCommandError() { + service.handleCommandError(CommandTestFactory.ID, PARENT_ID); } } @@ -524,40 +720,106 @@ class CommandServiceTest { @Nested class TestRevokeCommand { - final String commandId = CommandTestFactory.ID; + private static final Command REVOKE_PENDING_COMMAND = CommandTestFactory.createBuilder().status(CommandStatus.REVOKE_PENDING).build(); @Test - void shouldThrowException() { - assertThatExceptionOfType(NotFoundException.class).isThrownBy(() -> service.revokeCommand(commandId)); + void shouldCallRevokeCommandSilent() { + doReturn(Optional.of(REVOKE_PENDING_COMMAND)).when(service).revokeCommandSilent(anyString()); + + service.revokeCommand(CommandTestFactory.ID); + + verify(service).revokeCommandSilent(CommandTestFactory.ID); + } + + @Test + void shouldCallIsRevokePending() { + doReturn(Optional.of(REVOKE_PENDING_COMMAND)).when(service).revokeCommandSilent(anyString()); + + service.revokeCommand(CommandTestFactory.ID); + + verify(service).isRevokePending(REVOKE_PENDING_COMMAND); + } + + @Test + void shouldReturnCommand() { + doReturn(Optional.of(REVOKE_PENDING_COMMAND)).when(service).revokeCommandSilent(anyString()); + + var result = service.revokeCommand(CommandTestFactory.ID); + + assertThat(result).isSameAs(REVOKE_PENDING_COMMAND); + } + + @Test + void shouldThrowNotFoundException() { + doReturn(Optional.empty()).when(service).revokeCommandSilent(anyString()); + + assertThrows(NotFoundException.class, () -> service.revokeCommand(CommandTestFactory.ID)); } @Test - void shouldCallSetRevokeStatus() { - doReturn(CommandTestFactory.create()).when(repository).setRevokeStatus(anyString()); + void shouldThrowRevokeFailedException() { + doReturn(Optional.of(CommandTestFactory.create())).when(service).revokeCommandSilent(anyString()); + doReturn(false).when(service).isRevokePending(any()); - service.revokeCommand(commandId); + assertThrows(RevokeFailedException.class, () -> service.revokeCommand(CommandTestFactory.ID)); + } + } + + @Nested + class TestRevokeCommandSilent { + + private static final Command COMMAND = CommandTestFactory.create(); + + @BeforeEach + void init() { + when(repository.setRevokeStatusIfNotPending(anyString())).thenReturn(Optional.of(COMMAND)); + } + + @Test + void shouldCallRepository() { + service.revokeCommandSilent(CommandTestFactory.ID); + + verify(repository).setRevokeStatusIfNotPending(CommandTestFactory.ID); + } + + @Test + void shouldCallIsRevokePending() { + service.revokeCommandSilent(CommandTestFactory.ID); - verify(service).setRevokeStatus(CommandTestFactory.ID); + verify(service).isRevokePending(COMMAND); } @Test void shouldCallPublishRevokeCommandEvent() { - var command = CommandTestFactory.create(); - doReturn(command).when(repository).setRevokeStatus(anyString()); + doReturn(true).when(service).isRevokePending(any()); - service.revokeCommand(commandId); + service.revokeCommandSilent(CommandTestFactory.ID); - verify(service).publishRevokeCommandEvent(command); + verify(service).publishRevokeCommandEvent(COMMAND); } + } + + @Nested + class TestIsRevokePending { @Test - void shouldReturnCommand() { - var command = CommandTestFactory.create(); - doReturn(command).when(repository).setRevokeStatus(anyString()); + void shouldReturnTrue() { + var command = CommandTestFactory.createBuilder().status(CommandStatus.REVOKE_PENDING).build(); - var result = service.revokeCommand(commandId); + var result = service.isRevokePending(command); - assertThat(result).isSameAs(command); + assertThat(result).isTrue(); + } + + @DisplayName("should return false when") + @ParameterizedTest(name = "command status is {0}") + @EnumSource(value = CommandStatus.class, names = { "REVOKE_PENDING" }, mode = EnumSource.Mode.EXCLUDE) + void shouldReturnFalse(CommandStatus status) { + var command = CommandTestFactory.createBuilder().status(status).build(); + + var result = service.isRevokePending(command); + + assertThat(result).isFalse(); } } @@ -664,14 +926,14 @@ class CommandServiceTest { @BeforeEach void init() { doReturn(SUB_COMMAND_VALUES).when(service).buildSubCommandValues(any()); - doReturn(PARENT_COMMAND).when(service).getNotFailedCommand(any()); + doReturn(PARENT_COMMAND).when(service).getPendingCommand(any()); } @Test void shouldCallGetNotFailedCommand() { service.createSubCommands(CreateSubCommandsRequestTestFactory.create()); - verify(service).getNotFailedCommand(CreateSubCommandsRequestTestFactory.PARENT_ID); + verify(service).getPendingCommand(CreateSubCommandsRequestTestFactory.PARENT_ID); } @Test @@ -722,30 +984,25 @@ class CommandServiceTest { } @Nested - class TestGetNotFailedCommand { + class TestGetPendingCommand { @Test void shouldCallFindCommand() { doReturn(Optional.of(CommandTestFactory.create())).when(service).findById(anyString()); - service.getNotFailedCommand(CommandTestFactory.ID); + service.getPendingCommand(CommandTestFactory.ID); verify(service).findById(CommandTestFactory.ID); } - @Test - void shouldThrowExceptionIfCommandNotFound() { - doReturn(Optional.empty()).when(service).findById(anyString()); - - assertThrows(NotFoundException.class, () -> service.getNotFailedCommand(CommandTestFactory.ID)); - } - - @Test - void shouldThrowExceptionIfCommandFailed() { - var command = CommandTestFactory.createBuilder().status(CommandStatus.ERROR).build(); - doReturn(Optional.of(command)).when(service).findById(anyString()); + @DisplayName("should throw exception when") + @ParameterizedTest(name = "command status is {0}") + @EnumSource(value = CommandStatus.class, names = { "PENDING" }, mode = EnumSource.Mode.EXCLUDE) + void shouldThrowExceptionIfCommandFailed(CommandStatus status) { + var command = CommandTestFactory.createBuilder().status(status).build(); + doReturn(command).when(service).getById(anyString()); - assertThrows(TechnicalException.class, () -> service.getNotFailedCommand(CommandTestFactory.ID)); + assertThrows(TechnicalException.class, () -> service.getPendingCommand(CommandTestFactory.ID)); } @Test @@ -753,7 +1010,7 @@ class CommandServiceTest { var command = CommandTestFactory.create(); doReturn(Optional.of(command)).when(service).findById(anyString()); - var result = service.getNotFailedCommand(CommandTestFactory.ID); + var result = service.getPendingCommand(CommandTestFactory.ID); assertThat(result).isSameAs(command); } @@ -857,24 +1114,4 @@ class CommandServiceTest { } } - @Nested - class TestSetStatusRevokePending { - - @Test - void shouldCallRepository() { - service.setRevokeStatus(CommandTestFactory.ID); - - verify(repository).setRevokeStatus(CommandTestFactory.ID); - } - - @Test - void shouldReturnUpdatedCommand() { - var expectedCommand = CommandTestFactory.create(); - when(repository.setRevokeStatus(anyString())).thenReturn(expectedCommand); - - var result = service.setRevokeStatus(CommandTestFactory.ID); - - assertThat(result).isSameAs(expectedCommand); - } - } } \ No newline at end of file diff --git a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/GrpcCommandServiceITCase.java b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/GrpcCommandServiceITCase.java index 3053fad919a61cbdc56dfc0a5e0a46fd92a193e8..7df40567d8e858171053fe352454a7e67decde01 100644 --- a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/GrpcCommandServiceITCase.java +++ b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/GrpcCommandServiceITCase.java @@ -21,7 +21,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.annotation.DirtiesContext; import de.ozgcloud.command.Command; @@ -29,20 +28,20 @@ import de.ozgcloud.command.CommandStatus; import de.ozgcloud.common.test.DataITCase; import de.ozgcloud.vorgang.attached_item.VorgangAttachedItem; import de.ozgcloud.vorgang.attached_item.VorgangAttachedItemTestFactory; +import de.ozgcloud.vorgang.callcontext.WithMockCustomUser; import de.ozgcloud.vorgang.clientattribute.ClientAttributeReadPermitted; import de.ozgcloud.vorgang.common.grpc.GrpcObjectMapper; import de.ozgcloud.vorgang.grpc.command.CommandServiceGrpc.CommandServiceBlockingStub; import de.ozgcloud.vorgang.grpc.command.GrpcAddSubCommandsRequest; import de.ozgcloud.vorgang.grpc.command.GrpcCommand; -import de.ozgcloud.vorgang.grpc.command.GrpcCommandsResponse; import de.ozgcloud.vorgang.grpc.command.GrpcCreateCommand; import de.ozgcloud.vorgang.grpc.command.GrpcCreateCommandRequest; import de.ozgcloud.vorgang.grpc.command.GrpcFindCommandsRequest; -import de.ozgcloud.vorgang.grpc.command.GrpcGetCommandRequest; import de.ozgcloud.vorgang.grpc.command.GrpcRevokeCommandRequest; import de.ozgcloud.vorgang.vorgang.Vorgang; import de.ozgcloud.vorgang.vorgang.VorgangTestFactory; import io.grpc.StatusRuntimeException; +import lombok.extern.log4j.Log4j2; import net.devh.boot.grpc.client.inject.GrpcClient; @SpringBootTest(properties = { @@ -51,7 +50,8 @@ import net.devh.boot.grpc.client.inject.GrpcClient; }) @DirtiesContext @DataITCase -@WithMockUser +@WithMockCustomUser +@Log4j2 class GrpcCommandServiceITCase { @GrpcClient("inProcess") @@ -155,7 +155,6 @@ class GrpcCommandServiceITCase { } @Nested - // TODO Tests überarbeiten: asserts in Test-Methoden verschieben class TestAddSubCommands { private static final String PARENT_TEST_ORDER = "PARENT_TEST_ORDER"; @@ -181,51 +180,39 @@ class GrpcCommandServiceITCase { var grpcCommandsResponse = serviceBlockingStub.addSubCommands(request); - await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> assertCommandIsFinished(parentCommand)); - grpcCommandsResponse.getCommandList().forEach(this::assertCommandIsFinished); + await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> { + var command = loadCommand(parentCommand); + assertThat(command.getStatus()).isEqualTo(CommandStatus.FINISHED); + }); + grpcCommandsResponse.getCommandList().stream().map(this::loadCommand).forEach(command -> + assertThat(command.getStatus()).isEqualTo(CommandStatus.FINISHED) + ); } @Test - void shouldSetErrorWhenOneOfParallelsFails() { + void shouldSetErrorStatusForParentCommand() { var request = buildAddSubCommandsRequest(parentCommand.getId(), buildCreateCommand(), buildFailedCreateCommand(), buildCreateCommand()); - var grpcCommandsResponse = serviceBlockingStub.addSubCommands(request); + serviceBlockingStub.addSubCommands(request); - await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> assertCommandIsFailed(parentCommand)); - grpcCommandsResponse.getCommandList().forEach(this::assertCommandIsFailed); - } - - @Test - void shouldSetErrorWhenOneSequentialFails() { - var executedCommands = addSuccessfullySubCommands(); - - var failedCommand = addFailedSubCommands(); - - failedCommand.getCommandList().forEach(this::assertCommandIsFailed); - executedCommands.getCommandList().forEach(this::assertCommandIsFailed); + await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> { + var command = loadCommand(parentCommand); + assertThat(command.getStatus()).isEqualTo(CommandStatus.ERROR); + assertThat(command.getErrorMessage()).isNotBlank(); + }); } @Test - void shouldThrowErrorWhenParentFailedAlready() { - addFailedSubCommands(); - - assertThrows(StatusRuntimeException.class, this::addSuccessfullySubCommands); - } - - private GrpcCommandsResponse addSuccessfullySubCommands() { - var successfullyRequest = buildAddSubCommandsRequest(parentCommand.getId(), buildCreateCommand()); - var executedCommands = serviceBlockingStub.addSubCommands(successfullyRequest); - await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> assertCommandIsFinished(parentCommand)); - return executedCommands; - } - - private GrpcCommandsResponse addFailedSubCommands() { - var failedRequest = buildAddSubCommandsRequest(parentCommand.getId(), buildFailedCreateCommand()); + void shouldRevokeSubCommands() { + var request = buildAddSubCommandsRequest(parentCommand.getId(), buildCreateCommand(), buildFailedCreateCommand(), buildCreateCommand()); - var executedCommands = serviceBlockingStub.addSubCommands(failedRequest); + var grpcCommandsResponse = serviceBlockingStub.addSubCommands(request); - await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> assertCommandIsFailed(parentCommand)); - return executedCommands; + await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> { + var revokedCommand = grpcCommandsResponse.getCommandList().stream().map(this::loadCommand) + .filter(command -> command.getStatus() != CommandStatus.ERROR).toList(); + assertThat(revokedCommand).hasSize(2); + }); } private GrpcCommand createParentCommand() { @@ -268,15 +255,13 @@ class GrpcCommandServiceITCase { .build(); } - private void assertCommandIsFinished(GrpcCommand command) { - var commandResponse = serviceBlockingStub.getCommand(GrpcGetCommandRequest.newBuilder().setId(command.getId()).build()); - assertThat(commandResponse.getStatus()).isEqualTo(CommandStatus.FINISHED.name()); + private Command loadCommand(GrpcCommand grpcCommand) { + return mongoOperations.findById(grpcCommand.getId(), Command.class); } - private void assertCommandIsFailed(GrpcCommand command) { - var commandResponse = serviceBlockingStub.getCommand(GrpcGetCommandRequest.newBuilder().setId(command.getId()).build()); - assertThat(commandResponse.getStatus()).isEqualTo(CommandStatus.ERROR.name()); + private void assertCommandIsFinished(GrpcCommand grpcCommand) { + var command = mongoOperations.findById(grpcCommand.getId(), Command.class); + assertThat(command.getStatus()).isEqualTo(CommandStatus.FINISHED); } - } } diff --git a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/GrpcCommandServiceTest.java b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/GrpcCommandServiceTest.java index 149bd64d92c2013acd67bd8502b7dd195b662347..6807b332b138383be4bff92001a809aba224d3ac 100644 --- a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/GrpcCommandServiceTest.java +++ b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/command/GrpcCommandServiceTest.java @@ -199,16 +199,29 @@ class GrpcCommandServiceTest { @Nested class TestRevokeCommand { + private static final GrpcRevokeCommandRequest REVOKE_COMMAND_REQUEST = GrpcRevokeCommandRequest.newBuilder().setId(CommandTestFactory.ID) + .build(); + @Mock private StreamObserver<GrpcCommandResponse> responseObserver; - private final Command updatedCommand = CommandTestFactory.createBuilder().id(CommandTestFactory.ID).build(); + @Mock private GrpcCommandResponse commandsResponse; - private final GrpcRevokeCommandRequest request = GrpcRevokeCommandRequest.newBuilder().setId(CommandTestFactory.ID).build(); @Captor private ArgumentCaptor<CommandResponse> responseCaptor; + private Command updatedCommand; + + private void callRevokeCommand() { + service.revokeCommand(REVOKE_COMMAND_REQUEST, responseObserver); + } + + @BeforeEach + void init() { + updatedCommand = CommandTestFactory.createBuilder().id(CommandTestFactory.ID).build(); + } + @Test void shouldCallPolicyService() { callRevokeCommand(); @@ -248,10 +261,6 @@ class GrpcCommandServiceTest { verify(responseObserver).onCompleted(); } - - private void callRevokeCommand() { - service.revokeCommand(request, responseObserver); - } } @Nested