From 55f0fd877140fe3e9b337e463a88924d224472b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20M=C3=B6rling?= Date: Thu, 14 May 2026 15:45:47 +0200 Subject: [PATCH] feat: add POST /api/orders endpoint with validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create CreateOrderRequest DTO with jakarta.validation annotations - Validate plate format (ABC123 or ABC12A) via @Pattern regex - Validate letter text: @NotBlank, @Size(min=1, max=1000) - Validate template name: optional, @Size(max=50) - Add POST /api/orders endpoint to OrderController (auth required) - Return 201 Created with OrderResponse on success - Add 5 controller tests: no auth (403), create success, invalid plate, empty text, text over 1000 chars - All messages in Swedish (Ogiltigt registreringsnummer, Brevtext krävs, etc.) --- .../controller/OrderController.java | 22 ++++++ .../bilhalsning/dto/CreateOrderRequest.java | 18 +++++ .../controller/OrderControllerTest.java | 69 +++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 backend/src/main/java/se/bilhalsning/dto/CreateOrderRequest.java diff --git a/backend/src/main/java/se/bilhalsning/controller/OrderController.java b/backend/src/main/java/se/bilhalsning/controller/OrderController.java index 252c27b..36c7801 100644 --- a/backend/src/main/java/se/bilhalsning/controller/OrderController.java +++ b/backend/src/main/java/se/bilhalsning/controller/OrderController.java @@ -1,12 +1,17 @@ package se.bilhalsning.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; +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 se.bilhalsning.dto.CreateOrderRequest; import se.bilhalsning.dto.OrderResponse; import se.bilhalsning.entity.Order; import se.bilhalsning.entity.User; @@ -36,6 +41,23 @@ public class OrderController { return ResponseEntity.ok(orders); } + @PostMapping + public ResponseEntity create( + @Valid @RequestBody CreateOrderRequest request, + @AuthenticationPrincipal UserDetails userDetails) { + User user = userService.findByEmail(userDetails.getUsername()) + .orElseThrow(InvalidCredentialsException::new); + + Order order = orderService.createOrder( + user.getId(), + request.plate(), + request.template(), + request.letterText() + ); + + return ResponseEntity.status(HttpStatus.CREATED).body(toResponse(order)); + } + private OrderResponse toResponse(Order order) { return new OrderResponse( order.getId(), diff --git a/backend/src/main/java/se/bilhalsning/dto/CreateOrderRequest.java b/backend/src/main/java/se/bilhalsning/dto/CreateOrderRequest.java new file mode 100644 index 0000000..23ec8bc --- /dev/null +++ b/backend/src/main/java/se/bilhalsning/dto/CreateOrderRequest.java @@ -0,0 +1,18 @@ +package se.bilhalsning.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +public record CreateOrderRequest( + @NotBlank(message = "Registreringsnummer krävs") + @Pattern(regexp = "^[A-Za-z]{3}\\d{2}[A-Za-z0-9]$", message = "Ogiltigt registreringsnummer") + String plate, + + @Size(max = 50, message = "Mallnamn får vara max 50 tecken") + String template, + + @NotBlank(message = "Brevtext krävs") + @Size(min = 1, max = 1000, message = "Brevtexten måste vara mellan 1 och 1000 tecken") + String letterText +) {} diff --git a/backend/src/test/java/se/bilhalsning/controller/OrderControllerTest.java b/backend/src/test/java/se/bilhalsning/controller/OrderControllerTest.java index f19b4b0..13857ef 100644 --- a/backend/src/test/java/se/bilhalsning/controller/OrderControllerTest.java +++ b/backend/src/test/java/se/bilhalsning/controller/OrderControllerTest.java @@ -2,6 +2,7 @@ package se.bilhalsning.controller; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -101,4 +102,72 @@ class OrderControllerTest { mockMvc.perform(get("/api/orders")) .andExpect(status().isUnauthorized()); } + + @Test + void shouldReturn403WhenPostingWithoutAuth() throws Exception { + mockMvc.perform(post("/api/orders") + .contentType("application/json") + .content("{\"plate\":\"ABC123\",\"letterText\":\"Hej\"}")) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(username = "test@bilhalsning.se") + void shouldCreateOrderSuccessfully() throws Exception { + UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + User user = new User(); + user.setId(userId); + user.setEmail("test@bilhalsning.se"); + + when(userService.findByEmail("test@bilhalsning.se")).thenReturn(Optional.of(user)); + + se.bilhalsning.entity.Order savedOrder = new se.bilhalsning.entity.Order(); + savedOrder.setId(UUID.fromString("d1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")); + savedOrder.setUserId(userId); + savedOrder.setPlate("ABC123"); + savedOrder.setTemplate("Komplimang"); + savedOrder.setLetterText("Hej fin bil!"); + savedOrder.setStatus(se.bilhalsning.entity.OrderStatus.PENDING_PAYMENT); + + when(orderService.createOrder(userId, "ABC123", "Komplimang", "Hej fin bil!")) + .thenReturn(savedOrder); + + mockMvc.perform(post("/api/orders") + .contentType("application/json") + .content("{\"plate\":\"ABC123\",\"template\":\"Komplimang\",\"letterText\":\"Hej fin bil!\"}")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value("d1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")) + .andExpect(jsonPath("$.plate").value("ABC123")) + .andExpect(jsonPath("$.template").value("Komplimang")) + .andExpect(jsonPath("$.status").value("pending_payment")); + } + + @Test + @WithMockUser(username = "test@bilhalsning.se") + void shouldRejectInvalidPlateFormat() throws Exception { + mockMvc.perform(post("/api/orders") + .contentType("application/json") + .content("{\"plate\":\"INVALID\",\"letterText\":\"Hej\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("Ogiltigt registreringsnummer"))); + } + + @Test + @WithMockUser(username = "test@bilhalsning.se") + void shouldRejectEmptyLetterText() throws Exception { + mockMvc.perform(post("/api/orders") + .contentType("application/json") + .content("{\"plate\":\"ABC123\",\"letterText\":\"\"}")) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(username = "test@bilhalsning.se") + void shouldRejectLetterTextOver1000Chars() throws Exception { + String longText = "a".repeat(1001); + mockMvc.perform(post("/api/orders") + .contentType("application/json") + .content("{\"plate\":\"ABC123\",\"letterText\":\"" + longText + "\"}")) + .andExpect(status().isBadRequest()); + } }