From f4a3afc06301095b20493b2f9baf0e938b7a87f2 Mon Sep 17 00:00:00 2001
From: OZGCloud <ozgcloud@mgm-tp.com>
Date: Mon, 10 Jun 2024 20:03:56 +0200
Subject: [PATCH] OZG-5822 validate nachricht text and subject

---
 .gitignore                                    |   1 +
 alfa-client/.nvmrc                            |   2 +-
 .../de/ozgcloud/alfa/bescheid/Bescheid.java   |   5 +
 .../alfa/bescheid/BescheidModelAssembler.java |   5 +-
 .../bescheid/BescheidNachrichtValidation.java |   4 +
 .../BescheidSendenCommandController.java      |  47 +++++
 .../BescheidSendenCommandValidator.java       |  17 ++
 .../bescheid/BescheidModelAssemblerTest.java  |  11 +-
 .../BescheidSendenCommandControllerTest.java  | 180 ++++++++++++++++++
 9 files changed, 269 insertions(+), 3 deletions(-)
 create mode 100644 alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidNachrichtValidation.java
 create mode 100644 alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidSendenCommandController.java
 create mode 100644 alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidSendenCommandValidator.java
 create mode 100644 alfa-service/src/test/java/de/ozgcloud/alfa/bescheid/BescheidSendenCommandControllerTest.java

diff --git a/.gitignore b/.gitignore
index 6a8fd526ec..a8d2e8e3d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@ target/
 .attach**
 .factorypath
 .vscode*
+.nx
\ No newline at end of file
diff --git a/alfa-client/.nvmrc b/alfa-client/.nvmrc
index 87ec8842b1..48b14e6b2b 100644
--- a/alfa-client/.nvmrc
+++ b/alfa-client/.nvmrc
@@ -1 +1 @@
-18.18.2
+20.14.0
diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/Bescheid.java b/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/Bescheid.java
index 67cb7eb4c7..76d8324789 100644
--- a/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/Bescheid.java
+++ b/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/Bescheid.java
@@ -1,7 +1,10 @@
 package de.ozgcloud.alfa.bescheid;
 
+import static de.ozgcloud.alfa.common.ValidationMessageCodes.*;
+
 import java.util.List;
 
+import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -53,7 +56,9 @@ public class Bescheid implements CommandBody {
 
 	@LinkedResource(controllerClass = BinaryFileController.class)
 	private List<FileId> attachments;
+	@NotEmpty(message = FIELD_IS_EMPTY, groups = BescheidNachrichtValidation.class)
 	private String nachrichtText;
+	@NotEmpty(message = FIELD_IS_EMPTY, groups = BescheidNachrichtValidation.class)
 	private String nachrichtSubject;
 	private SendBy sendBy;
 	private BescheidStatus status;
diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidModelAssembler.java b/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidModelAssembler.java
index f8d3c4a12e..cef975602a 100644
--- a/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidModelAssembler.java
+++ b/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidModelAssembler.java
@@ -61,6 +61,9 @@ public class BescheidModelAssembler implements RepresentationModelAssembler<Besc
 		var attachmentsLink = linkTo(methodOn(BescheidController.class).getAttachments(bescheid.getId(), bescheid.getVorgangId()));
 		var createCommandLink = buildCreateCommandLink(bescheid);
 		var vorgangWithEingang = vorgangController.getVorgang(bescheid.getVorgangId());
+		var bescheidenUndSendenLink = linkTo(
+				methodOn(BescheidSendenCommandController.class).createCommand(vorgangWithEingang.getId(), bescheid.getId(), bescheid.getVersion(),
+						null));
 
 		return ModelBuilder.fromEntity(bescheid)
 				.addLink(selfLink.withSelfRel())
@@ -75,7 +78,7 @@ public class BescheidModelAssembler implements RepresentationModelAssembler<Besc
 				.addLink(createCommandLink.withRel(REL_CREATE_DOCUMENT))
 				.addLink(createCommandLink.withRel(REL_CREATE_DOCUMENT_FROM_FILE))
 				.ifMatch(() -> canSendMessageToAntragsteller(vorgangWithEingang))
-				.addLink(createCommandLink.withRel(REL_BESCHEIDEN_UND_SENDEN))
+				.addLink(bescheidenUndSendenLink.withRel(REL_BESCHEIDEN_UND_SENDEN))
 				.addLink(createCommandLink.withRel(REL_BESCHEIDEN))
 				.buildModel();
 	}
diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidNachrichtValidation.java b/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidNachrichtValidation.java
new file mode 100644
index 0000000000..13e50b003f
--- /dev/null
+++ b/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidNachrichtValidation.java
@@ -0,0 +1,4 @@
+package de.ozgcloud.alfa.bescheid;
+
+interface BescheidNachrichtValidation {
+}
diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidSendenCommandController.java b/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidSendenCommandController.java
new file mode 100644
index 0000000000..443ade445b
--- /dev/null
+++ b/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidSendenCommandController.java
@@ -0,0 +1,47 @@
+package de.ozgcloud.alfa.bescheid;
+
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
+
+import org.springframework.hateoas.EntityModel;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import de.ozgcloud.alfa.common.command.Command;
+import de.ozgcloud.alfa.common.command.CommandController;
+import de.ozgcloud.alfa.common.command.CommandService;
+import de.ozgcloud.alfa.common.command.CreateCommand;
+import de.ozgcloud.common.errorhandling.TechnicalException;
+import lombok.RequiredArgsConstructor;
+
+@RestController
+@RequestMapping(BescheidSendenCommandController.PATH)
+@RequiredArgsConstructor
+class BescheidSendenCommandController {
+
+	static final String PATH = "/api/vorgangs/{vorgangId}/bescheids/relations/{relationId}/{relationVersion}/commands"; // NOSONAR
+
+	private final BescheidService bescheidService;
+	private final CommandService commandService;
+	private final BescheidSendenCommandValidator bescheidSendenCommandValidator;
+
+	@PostMapping
+	public ResponseEntity<EntityModel<Command>> createCommand(@PathVariable String vorgangId, @PathVariable String relationId,
+			@PathVariable long relationVersion, @RequestBody CreateCommand command) {
+		command = command.toBuilder().vorgangId(vorgangId).relationId(relationId).build();
+
+		validate(vorgangId, command);
+
+		var created = commandService.createCommand(command, relationVersion);
+		return ResponseEntity.created(linkTo(CommandController.class).slash(created.getId()).toUri()).build();
+	}
+
+	void validate(String vorgangId, CreateCommand command) {
+		var bescheid = bescheidService.getBescheidDraft(vorgangId)
+				.orElseThrow(() -> new TechnicalException("Bescheid not found for vorgang id: " + vorgangId));
+		bescheidSendenCommandValidator.validate(command.toBuilder().body(bescheid).build());
+	}
+}
diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidSendenCommandValidator.java b/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidSendenCommandValidator.java
new file mode 100644
index 0000000000..f304a0428a
--- /dev/null
+++ b/alfa-service/src/main/java/de/ozgcloud/alfa/bescheid/BescheidSendenCommandValidator.java
@@ -0,0 +1,17 @@
+package de.ozgcloud.alfa.bescheid;
+
+import jakarta.validation.Valid;
+
+import org.springframework.stereotype.Component;
+import org.springframework.validation.annotation.Validated;
+
+import de.ozgcloud.alfa.common.command.CreateCommand;
+
+@Validated(BescheidNachrichtValidation.class)
+@Component
+class BescheidSendenCommandValidator {
+
+	public void validate(@Valid CreateCommand command) {
+		// noop
+	}
+}
diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/bescheid/BescheidModelAssemblerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/bescheid/BescheidModelAssemblerTest.java
index 4372f02417..c78910e742 100644
--- a/alfa-service/src/test/java/de/ozgcloud/alfa/bescheid/BescheidModelAssemblerTest.java
+++ b/alfa-service/src/test/java/de/ozgcloud/alfa/bescheid/BescheidModelAssemblerTest.java
@@ -177,7 +177,7 @@ class BescheidModelAssemblerTest {
 
 			assertThat(model.getLink(REL_BESCHEIDEN_UND_SENDEN))
 					.isPresent().get()
-					.extracting(Link::getHref).isEqualTo(createCommandLink());
+					.extracting(Link::getHref).isEqualTo(bescheidenUndSendenLink());
 		}
 
 		@Test
@@ -225,6 +225,11 @@ class BescheidModelAssemblerTest {
 					.expand(VorgangHeaderTestFactory.ID, BescheidTestFactory.ID, BescheidTestFactory.VERSION).toString();
 		}
 
+		private String bescheidenUndSendenLink() {
+			return new UriTemplate(BescheidSendenCommandController.PATH)
+					.expand(VorgangHeaderTestFactory.ID, BescheidTestFactory.ID, BescheidTestFactory.VERSION).toString();
+		}
+
 		private EntityModel<Bescheid> callToModel() {
 			return callToModel(bescheid);
 		}
@@ -240,6 +245,8 @@ class BescheidModelAssemblerTest {
 
 		@Test
 		void shouldCallToModel() {
+			when(vorgangController.getVorgang(VorgangHeaderTestFactory.ID)).thenReturn(VorgangWithEingangTestFactory.create());
+
 			callMethod();
 
 			verify(assembler).toCollectionModel(List.of(bescheid));
@@ -257,6 +264,8 @@ class BescheidModelAssemblerTest {
 
 		@Test
 		void shouldHaveSelfLink() {
+			when(vorgangController.getVorgang(VorgangHeaderTestFactory.ID)).thenReturn(VorgangWithEingangTestFactory.create());
+
 			var collectionModel = callMethod();
 
 			assertThat(collectionModel.getLinks())
diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/bescheid/BescheidSendenCommandControllerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/bescheid/BescheidSendenCommandControllerTest.java
new file mode 100644
index 0000000000..a6ee7a8966
--- /dev/null
+++ b/alfa-service/src/test/java/de/ozgcloud/alfa/bescheid/BescheidSendenCommandControllerTest.java
@@ -0,0 +1,180 @@
+package de.ozgcloud.alfa.bescheid;
+
+import static de.ozgcloud.alfa.common.command.CommandController.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import de.ozgcloud.alfa.common.command.Command;
+import de.ozgcloud.alfa.common.command.CommandOrder;
+import de.ozgcloud.alfa.common.command.CommandService;
+import de.ozgcloud.alfa.common.command.CommandTestFactory;
+import de.ozgcloud.alfa.common.command.CreateCommand;
+import de.ozgcloud.alfa.vorgang.VorgangHeaderTestFactory;
+import de.ozgcloud.common.errorhandling.TechnicalException;
+import de.ozgcloud.common.test.TestUtils;
+
+class BescheidSendenCommandControllerTest {
+
+	@Spy
+	@InjectMocks
+	private BescheidSendenCommandController controller;
+
+	@Mock
+	private BescheidService bescheidService;
+	@Mock
+	private CommandService commandService;
+
+	@Mock
+	private BescheidSendenCommandValidator bescheidSendenCommandValidator;
+
+	private MockMvc mockMvc;
+
+	@BeforeEach
+	void setUp() {
+		mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+	}
+
+	@Nested
+	class TestCreateCommand {
+
+		private final Command command = CommandTestFactory.create();
+
+		@Captor
+		private ArgumentCaptor<CreateCommand> createCommandArgumentCaptor;
+
+		@BeforeEach
+		void setUp() {
+			doNothing().when(controller).validate(eq(VorgangHeaderTestFactory.ID), any(CreateCommand.class));
+			when(commandService.createCommand(any(CreateCommand.class), eq(BescheidTestFactory.VERSION))).thenReturn(command);
+		}
+
+		@Test
+		void shouldValidate() throws Exception {
+			doRequest();
+
+			verify(controller).validate(eq(VorgangHeaderTestFactory.ID), any(CreateCommand.class));
+		}
+
+		@Test
+		void shouldSetVorgangIdOnCreateCommand() throws Exception {
+			doRequest();
+
+			verify(controller).validate(eq(VorgangHeaderTestFactory.ID), createCommandArgumentCaptor.capture());
+			assertThat(createCommandArgumentCaptor.getValue().getVorgangId()).isEqualTo(VorgangHeaderTestFactory.ID);
+		}
+
+		@Test
+		void shouldSetRelationIdOnCreateCommand() throws Exception {
+			doRequest();
+
+			verify(controller).validate(eq(VorgangHeaderTestFactory.ID), createCommandArgumentCaptor.capture());
+			assertThat(createCommandArgumentCaptor.getValue().getRelationId()).isEqualTo(BescheidTestFactory.ID);
+		}
+
+		@Test
+		void shouldCallCommandService() throws Exception {
+			doRequest();
+
+			verify(commandService).createCommand(any(CreateCommand.class), eq(BescheidTestFactory.VERSION));
+		}
+
+		@Test
+		void shouldReturnCreated() throws Exception {
+			doRequest().andExpect(status().isCreated());
+		}
+
+		@Test
+		void shouldReturnCommand() throws Exception {
+			doRequest()
+					.andExpect(header().stringValues("location", "http://localhost" + COMMANDS_PATH + "/" + CommandTestFactory.ID));
+		}
+
+		private ResultActions doRequest() throws Exception {
+			return mockMvc.perform(
+					post(BescheidSendenCommandController.PATH,
+							VorgangHeaderTestFactory.ID,
+							BescheidTestFactory.ID,
+							BescheidTestFactory.VERSION)
+							.content(createContent())
+							.contentType(MediaType.APPLICATION_JSON));
+		}
+
+		private String createContent() {
+			return TestUtils.loadTextFile("jsonTemplates/command/createCommandWithBody.json.tmpl",
+					CommandOrder.SEND_BESCHEID.name(),
+					null);
+		}
+
+	}
+
+	@Nested
+	class TestValidate {
+
+		private final CreateCommand createCommand = CommandTestFactory.createCreateCommand();
+		private final Bescheid bescheid = BescheidTestFactory.create();
+
+		@Captor
+		private ArgumentCaptor<CreateCommand> commandArgumentCaptor;
+
+		@Nested
+		class WhenBescheidExists {
+
+			@BeforeEach
+			void setUp() {
+				when(bescheidService.getBescheidDraft(VorgangHeaderTestFactory.ID)).thenReturn(Optional.of(bescheid));
+				doNothing().when(bescheidSendenCommandValidator).validate(any(CreateCommand.class));
+			}
+
+			@Test
+			void shouldGetBescheidDraft() {
+				controller.validate(VorgangHeaderTestFactory.ID, createCommand);
+
+				verify(bescheidService).getBescheidDraft(VorgangHeaderTestFactory.ID);
+			}
+
+			@Test
+			void shouldValidate() {
+				controller.validate(VorgangHeaderTestFactory.ID, createCommand);
+
+				verify(bescheidSendenCommandValidator).validate(any(CreateCommand.class));
+			}
+
+			@Test
+			void shouldAddBescheidToCommand() {
+				controller.validate(VorgangHeaderTestFactory.ID, createCommand);
+
+				verify(bescheidSendenCommandValidator).validate(commandArgumentCaptor.capture());
+				assertThat(commandArgumentCaptor.getValue()).usingRecursiveComparison().isEqualTo(createCommand.toBuilder().body(bescheid).build());
+			}
+		}
+
+		@Nested
+		class WhenBescheidNotExists {
+
+			@Test
+			void shouldThrowTechnicalException() {
+				when(bescheidService.getBescheidDraft(VorgangHeaderTestFactory.ID)).thenReturn(Optional.empty());
+
+				assertThatThrownBy(() -> controller.validate(VorgangHeaderTestFactory.ID, createCommand)).isInstanceOf(TechnicalException.class);
+			}
+		}
+	}
+
+}
\ No newline at end of file
-- 
GitLab