diff --git a/vorgang-manager-command/src/main/java/de/ozgcloud/command/VorgangUnlockedEvent.java b/vorgang-manager-command/src/main/java/de/ozgcloud/command/VorgangUnlockedEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..167a7e49b7590fb40f6b7764c0a7feaf1a5d4170 --- /dev/null +++ b/vorgang-manager-command/src/main/java/de/ozgcloud/command/VorgangUnlockedEvent.java @@ -0,0 +1,31 @@ +/* + * 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.command; + +public class VorgangUnlockedEvent extends CommandExecutedEvent { + + public VorgangUnlockedEvent(Command command) { + super(command); + } +} diff --git a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/command/Order.java b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/command/Order.java index a48af6f57e82f77afb0ad312262c0367449d6f86..1cf3b522c278418a0a83234779189e63513416bb 100644 --- a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/command/Order.java +++ b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/command/Order.java @@ -41,6 +41,7 @@ public enum Order { VORGANG_ZUM_LOESCHEN_MARKIEREN, VORGANG_LOESCHEN, VORGANG_LOCK, + UNLOCK_VORGANG, ASSIGN_USER, diff --git a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/VorgangEventListener.java b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/VorgangEventListener.java index d8d18d10a35f0af062a13f6163c2322e432c8cef..991258adfafb4913e056d2924909d4f4ac78a646 100644 --- a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/VorgangEventListener.java +++ b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/VorgangEventListener.java @@ -26,7 +26,6 @@ package de.ozgcloud.vorgang.vorgang; import java.util.Collections; import java.util.function.Predicate; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -36,42 +35,45 @@ import de.ozgcloud.command.CommandCreatedEvent; import de.ozgcloud.command.CommandExecutedEvent; import de.ozgcloud.command.CommandFailedEvent; import de.ozgcloud.command.CommandRevokeFailedEvent; +import de.ozgcloud.command.CommandRevokedEvent; import de.ozgcloud.command.RevokeCommandEvent; +import de.ozgcloud.command.VorgangUnlockedEvent; import de.ozgcloud.vorgang.command.CommandService; import de.ozgcloud.vorgang.command.Order; import de.ozgcloud.vorgang.files.FileService; import de.ozgcloud.vorgang.status.StatusService; import de.ozgcloud.vorgang.vorgang.redirect.VorgangForwardFailedEvent; import de.ozgcloud.vorgang.vorgang.redirect.VorgangRedirectedEvent; +import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @Component +@RequiredArgsConstructor @Log4j2 public class VorgangEventListener { private static final String IS_ASSIGN_COMMAND_CONDITION = "{T(de.ozgcloud.vorgang.vorgang.VorgangEventListener).IS_ASSIGN_COMMAND.test(event.getSource())}"; - private static final String IS_LOESCHEN_COMMAND_CONDITION = "{T(de.ozgcloud.vorgang.vorgang.VorgangEventListener).IS_LOESCHEN_COMMAND.test(event.getSource())}"; - private static final String IS_SET_AKTENZEICHEN_COMMAND_CONDITION = "{T(de.ozgcloud.vorgang.vorgang.VorgangEventListener).IS_SET_AKTENZEICHEN_COMMAND.test(event.getSource())}"; public static final Predicate<Command> IS_ASSIGN_COMMAND = command -> Order.ASSIGN_USER.isMeant(command.getOrder()); + + private static final String IS_LOESCHEN_COMMAND_CONDITION = "{T(de.ozgcloud.vorgang.vorgang.VorgangEventListener).IS_LOESCHEN_COMMAND.test(event.getSource())}"; public static final Predicate<Command> IS_LOESCHEN_COMMAND = command -> Order.VORGANG_LOESCHEN.isMeant(command.getOrder()); + + private static final String IS_SET_AKTENZEICHEN_COMMAND_CONDITION = "{T(de.ozgcloud.vorgang.vorgang.VorgangEventListener).IS_SET_AKTENZEICHEN_COMMAND.test(event.getSource())}"; public static final Predicate<Command> IS_SET_AKTENZEICHEN_COMMAND = command -> Order.SET_AKTENZEICHEN.isMeant(command.getOrder()); private static final String IS_LOCK_VORGANG_COMMAND = "{T(de.ozgcloud.vorgang.vorgang.VorgangEventListener).IS_LOCK_VORGANG_ORDER.test(event.getSource())}"; public static final Predicate<Command> IS_LOCK_VORGANG_ORDER = command -> Order.VORGANG_LOCK.isMeant(command.getOrder()); - @Autowired - private StatusService statusService; - @Autowired - private VorgangService vorgangService; - @Autowired - private VorgangHeaderService vorgangHeaderService; - @Autowired - private FileService fileService; - @Autowired - private CommandService commandService; + private static final String IS_UNLOCK_VORGANG_COMMAND_CONDITION = "{T(de.ozgcloud.vorgang.vorgang.VorgangEventListener).IS_UNLOCK_VORGANG_COMMAND.test(event.getSource())}"; + public static final Predicate<Command> IS_UNLOCK_VORGANG_COMMAND = command -> Order.UNLOCK_VORGANG.isMeant(command.getOrder()); - @Autowired - private ApplicationEventPublisher publisher; + private final StatusService statusService; + private final VorgangService vorgangService; + private final VorgangHeaderService vorgangHeaderService; + private final FileService fileService; + private final CommandService commandService; + + private final ApplicationEventPublisher publisher; @EventListener public void updateStatus(VorgangRedirectedEvent event) { @@ -132,6 +134,7 @@ public class VorgangEventListener { var command = event.getSource(); try { vorgangService.unlockVorgang(command); + publisher.publishEvent(new CommandRevokedEvent(command)); } catch (RuntimeException e) { LOG.error("Error unlocking vorgang.", e); publisher.publishEvent(new CommandRevokeFailedEvent(command.getId(), e.getMessage())); @@ -151,4 +154,16 @@ public class VorgangEventListener { private String getAktenzeichen(String vorgangId) { return vorgangHeaderService.getById(vorgangId).getAktenzeichen(); } + + @EventListener(condition = IS_UNLOCK_VORGANG_COMMAND_CONDITION) + public void onUnlockVorgang(CommandCreatedEvent event) { + var command = event.getSource(); + try { + vorgangService.unlockVorgang(command); + publisher.publishEvent(new VorgangUnlockedEvent(command)); + } catch (RuntimeException e) { + LOG.error("Error unlocking vorgang.", e); + publisher.publishEvent(new CommandFailedEvent(command.getId(), e.getMessage())); + } + } } \ No newline at end of file diff --git a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/VorgangRepository.java b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/VorgangRepository.java index 4038e151625096360b7d0f813e3513741add4776..cd3b6833ce56a6d3076c0103c248446bd9a65bef 100644 --- a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/VorgangRepository.java +++ b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/VorgangRepository.java @@ -83,6 +83,13 @@ class VorgangRepository { updateFirst(vorgangId, whereIdAndVersion(vorgangId, version), new Update().set(Vorgang.FIELD_AKTENZEICHEN, akteneinsicht)); } + public void removeLock(String vorgangId, long version) { + var update = new Update().inc(Vorgang.MONGODB_FIELDNAME_VERSION, 1) + .unset(VorgangService.KEY_HEADER_LOCK); + + updateFirst(vorgangId, whereIdAndVersion(vorgangId, version), update); + } + private void updateFirst(String vorgangId, Criteria queryObj, Update update) { var updateResult = mongoOperations.updateFirst(query(queryObj), update, Vorgang.class); collisionVerifier.verify(updateResult, vorgangId); diff --git a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/VorgangService.java b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/VorgangService.java index 801685f1f65300819e724da8b4811399b75a6a49..9655a784fb33f3936c5c3bbeef6b2f94cdb841ea 100644 --- a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/VorgangService.java +++ b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/VorgangService.java @@ -38,7 +38,6 @@ import jakarta.validation.Valid; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; @@ -55,33 +54,28 @@ import de.ozgcloud.vorgang.clientattribute.ClientAttributesMap; import de.ozgcloud.vorgang.common.errorhandling.NotFoundException; import de.ozgcloud.vorgang.servicekonto.ServiceKonto; import lombok.NonNull; +import lombok.RequiredArgsConstructor; @Validated @Service +@RequiredArgsConstructor public class VorgangService { private static final String BODY_ASSIGNED_TO_FIELD = "assignedTo"; public static final String BODY_OBJECT_AKTENZEICHEN = "aktenzeichen"; static final String KEY_HEADER_LOCK = "%s.%s".formatted(Vorgang.MONGODB_FIELDNAME_HEADER, VorgangHead.FIELD_LOCK); - @Autowired - private VorgangAuthorizationService vorgangAuthenticationService; - @Autowired - private VorgangRepository repository; + private final VorgangAuthorizationService vorgangAuthenticationService; + private final VorgangRepository repository; - @Autowired - private ApplicationEventPublisher publisher; - @Autowired - private AktenzeichenProvider aktenzeichenProvider; + private final ApplicationEventPublisher publisher; + private final AktenzeichenProvider aktenzeichenProvider; - @Autowired - private ClientAttributeReadPermitted readIsPermitted; + private final ClientAttributeReadPermitted readIsPermitted; - @Autowired - private LabelProcessor kopControlDataMapper; + private final LabelProcessor kopControlDataMapper; - @Autowired - private VorgangStubMapper stubMapper; + private final VorgangStubMapper stubMapper; public Vorgang startCreation(@Valid Eingang eingang) { var mappedEingang = kopControlDataMapper.moveLabelsToControlData(eingang); @@ -257,8 +251,21 @@ public class VorgangService { } public void unlockVorgang(Command command) { - // TODO wird implementiert in OZG-6992 - // publisher.publishEvent(new CommandRevokedEvent(command)); - throw new UnsupportedOperationException("Not yet implemented"); + validateUnlockingClient(command); + repository.removeLock(command.getVorgangId(), command.getRelationVersion()); + } + + void validateUnlockingClient(Command command) { + var vorgang = repository.findById(command.getVorgangId()); + if (vorgang.isEmpty()) { + throw new NotFoundException(Vorgang.class, command.getVorgangId()); + } + vorgang.map(Vorgang::getHeader).map(VorgangHead::getLock).map(Lock::getClientName) + .filter(lockingClient -> !lockingClient.equals(command.getCreatedByClient())) + .ifPresent(lockingClient -> { + throw new AccessDeniedException( + "Vorgang was locked by %s, but client name is %s!".formatted(lockingClient, command.getCreatedByClient())); + }); + } } \ No newline at end of file diff --git a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/VorgangEventListenerTest.java b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/VorgangEventListenerTest.java index 1777bbfa47376f46a96918f6ef76721d0e68b0b3..83a5075ea4593f49c34d2fb2786caed01a7b733a 100644 --- a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/VorgangEventListenerTest.java +++ b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/VorgangEventListenerTest.java @@ -44,10 +44,13 @@ import org.springframework.context.ApplicationEventPublisher; import com.thedeanda.lorem.LoremIpsum; import de.ozgcloud.command.Command; +import de.ozgcloud.command.CommandCreatedEvent; import de.ozgcloud.command.CommandExecutedEvent; import de.ozgcloud.command.CommandFailedEvent; import de.ozgcloud.command.CommandRevokeFailedEvent; +import de.ozgcloud.command.CommandRevokedEvent; import de.ozgcloud.command.RevokeCommandEvent; +import de.ozgcloud.command.VorgangUnlockedEvent; import de.ozgcloud.common.errorhandling.TechnicalException; import de.ozgcloud.vorgang.command.CommandCreatedEventTestFactory; import de.ozgcloud.vorgang.command.CommandService; @@ -269,19 +272,25 @@ class VorgangEventListenerTest { @Captor private ArgumentCaptor<CommandRevokeFailedEvent> eventCaptor; + private Command command = CommandTestFactory.create(); @Test void shouldCallUnlockVorgang() { - var command = CommandTestFactory.create(); - listener.onRevokeLockVorgang(new RevokeCommandEvent(command)); verify(service).unlockVorgang(command); } + @Test + void shouldPublishCommandRevokedEvent() { + listener.onRevokeLockVorgang(new RevokeCommandEvent(command)); + + verify(publisher).publishEvent(argThat(event -> event instanceof CommandRevokedEvent && event.getSource().equals(command))); + + } + @Test void shouldPublishCommandFailedEvent() { - var command = CommandTestFactory.create(); var errorMessage = "error message"; doThrow(new RuntimeException(errorMessage)).when(service).unlockVorgang(command); @@ -294,4 +303,41 @@ class VorgangEventListenerTest { }); } } + + @Nested + class TestOnUnlockVorgang { + + private Command command = CommandTestFactory.create(); + + @Test + void shouldUnlockVorgangOnUnlockVorgangOrder() { + onUnlockVorgang(); + + verify(service).unlockVorgang(command); + } + + @Test + void shouldPublishVorgangUnlockedEvent() { + onUnlockVorgang(); + + verify(publisher) + .publishEvent( + argThat(event -> event instanceof VorgangUnlockedEvent && ((CommandExecutedEvent) event).getCommand().equals(command))); + } + + @Test + void shouldPublishCommandFailedEvent() { + var errorMessage = "error message"; + doThrow(new RuntimeException(errorMessage)).when(service).unlockVorgang(command); + + onUnlockVorgang(); + + verify(publisher).publishEvent(argThat(event -> event.getSource().equals(CommandTestFactory.ID) + && ((CommandFailedEvent) event).getErrorMessage().equals(errorMessage))); + } + + private void onUnlockVorgang() { + listener.onUnlockVorgang(new CommandCreatedEvent(command)); + } + } } \ No newline at end of file diff --git a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/VorgangRepositoryITCase.java b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/VorgangRepositoryITCase.java index f8e53b21c3330f11204100db26086e1d5274c4c7..f58e0c900184c8287ee82b8868e8e870fac0f5b3 100644 --- a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/VorgangRepositoryITCase.java +++ b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/VorgangRepositoryITCase.java @@ -340,7 +340,7 @@ class VorgangRepositoryITCase { } @Test - void should() { + void should() { assertThat(mongoOperations.findAll(Vorgang.class)).isEmpty(); repository.setAktenzeichen(VorgangTestFactory.ID, VorgangTestFactory.VERSION, VorgangTestFactory.AKTENZEICHEN); @@ -360,4 +360,46 @@ class VorgangRepositoryITCase { return mongoOperations.findById(VorgangTestFactory.ID, Vorgang.class); } } + + @Nested + class TestRemoveLock { + + private final Vorgang vorgang = VorgangTestFactory.create(); + + private Vorgang persistedVorgang; + + @BeforeEach + void persistVorgang() { + persistedVorgang = repository.save(vorgang); + } + + @Test + void shouldRemoveLock() { + removeLock(); + + assertThat(persistedVorgang.getHeader().getLock()).isNotNull(); + assertThat(getVorgang().getHeader().getLock()).isNull(); + } + + @Test + void shouldIncrementVersion() { + removeLock(); + + assertThat(getVorgang().getVersion()).isEqualTo(VorgangTestFactory.VERSION + 1); + } + + private void removeLock() { + repository.removeLock(VorgangTestFactory.ID, VorgangTestFactory.VERSION); + } + + @Test + void shouldThrowExceptionByCollision() { + assertThrows(ConcurrentModificationException.class, + () -> repository.removeLock(VorgangTestFactory.ID, VorgangTestFactory.VERSION + 1)); + } + + private Vorgang getVorgang() { + return mongoOperations.findById(VorgangTestFactory.ID, Vorgang.class); + } + } } \ No newline at end of file diff --git a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/VorgangServiceTest.java b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/VorgangServiceTest.java index db34a67ab574fbeb28cf458ef75766e20714943c..62389f66bf46e1b803e2112693f3d9a939698455 100644 --- a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/VorgangServiceTest.java +++ b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/VorgangServiceTest.java @@ -352,7 +352,7 @@ class VorgangServiceTest { void shouldThrowAccessDeniedException() { when(vorgangAuthorizationService.authorizeByOrganisationseinheitenId(any(), any())).thenReturn(false); - assertThrows(AccessDeniedException.class, () -> service.getById(VorgangTestFactory.ID, FilterCriteriaTestFactory.create())); //NOSONAR + assertThrows(AccessDeniedException.class, () -> service.getById(VorgangTestFactory.ID, FilterCriteriaTestFactory.create())); // NOSONAR } } @@ -616,4 +616,82 @@ class VorgangServiceTest { assertThat(result).extractingByKey(VorgangService.KEY_HEADER_LOCK, MAP).containsEntry(Lock.FIELD_REASON, LockTestFactory.REASON); } } + + @Nested + class TestUnlockVorgang { + + private final Command command = CommandTestFactory.create(); + + @BeforeEach + void mock() { + doNothing().when(service).validateUnlockingClient(any()); + } + + @Test + void shouldCallValidateUnlockingClient() { + unlockVorgang(); + + verify(service).validateUnlockingClient(command); + } + + @Test + void shouldCallRepository() { + unlockVorgang(); + + verify(repository).removeLock(VorgangTestFactory.ID, CommandTestFactory.RELATION_VERSION); + } + + private void unlockVorgang() { + service.unlockVorgang(command); + } + } + + @Nested + class TestValidateUnlockingClient { + + @Test + void shouldGetVorgangFromRepository() { + var command = CommandTestFactory.create(); + + try { + service.validateUnlockingClient(command); + } catch (Exception e) { + } + + verify(repository).findById(VorgangTestFactory.ID); + } + + @Test + void shouldThrowAccessDeniedExceptionOnNotMatchingClient() { + when(repository.findById(VorgangTestFactory.ID)).thenReturn(Optional.of(VorgangTestFactory.create())); + var command = CommandTestFactory.createBuilder().createdByClient("wrong client").build(); + + assertThrows(AccessDeniedException.class, () -> service.validateUnlockingClient(command)); + } + + @Test + void shouldThrowNotFoundException() { + var command = CommandTestFactory.create(); + when(repository.findById(VorgangTestFactory.ID)).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> service.validateUnlockingClient(command)); + } + + @Test + void shouldNotThrowExceptionOnMatchingClient() { + when(repository.findById(VorgangTestFactory.ID)).thenReturn(Optional.of(VorgangTestFactory.create())); + var command = CommandTestFactory.createBuilder().createdByClient(LockTestFactory.CLIENT_NAME).build(); + + assertDoesNotThrow(() -> service.validateUnlockingClient(command)); + } + + @Test + void shouldNotThrowExceptionOnVorgangNotLocked() { + when(repository.findById(VorgangTestFactory.ID)).thenReturn( + Optional.of(VorgangTestFactory.createBuilder().header(VorgangHeadTestFactory.createBuilder().lock(null).build()).build())); + var command = CommandTestFactory.createBuilder().createdByClient(LockTestFactory.CLIENT_NAME).build(); + + assertDoesNotThrow(() -> service.validateUnlockingClient(command)); + } + } } \ No newline at end of file