diff --git a/backend/src/test/java/se/bilhalsning/controller/GuestOrderControllerTest.java b/backend/src/test/java/se/bilhalsning/controller/GuestOrderControllerTest.java new file mode 100644 index 0000000..ddd505b --- /dev/null +++ b/backend/src/test/java/se/bilhalsning/controller/GuestOrderControllerTest.java @@ -0,0 +1,179 @@ +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; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import se.bilhalsning.entity.Order; +import se.bilhalsning.entity.OrderStatus; +import se.bilhalsning.exception.InvalidOrderStateException; +import se.bilhalsning.exception.OrderNotFoundException; +import se.bilhalsning.service.OrderService; +import se.bilhalsning.service.UserService; + +@SpringBootTest +@AutoConfigureMockMvc +@TestPropertySource(properties = "app.jwt.secret=this-is-a-test-secret-that-is-at-least-32-bytes-long!!") +class GuestOrderControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private OrderService orderService; + + @MockitoBean + private UserService userService; + + // --- POST /api/guest-orders (create) --- + + @Test + void shouldCreateGuestOrderWithoutAuth() throws Exception { + UUID orderId = UUID.fromString("d1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + UUID guestToken = UUID.fromString("e2eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + + Order savedOrder = new Order(); + savedOrder.setId(orderId); + savedOrder.setPlate("ABC123"); + savedOrder.setLetterText("Hej fin bil!"); + savedOrder.setStatus(OrderStatus.PENDING_PAYMENT); + savedOrder.setGuestEmail("guest@example.com"); + savedOrder.setGuestToken(guestToken); + + when(orderService.createGuestOrder("ABC123", "Hej fin bil!", "guest@example.com")) + .thenReturn(savedOrder); + + mockMvc.perform(post("/api/guest-orders") + .contentType("application/json") + .content("{\"plate\":\"ABC123\",\"letterText\":\"Hej fin bil!\",\"email\":\"guest@example.com\"}")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(orderId.toString())) + .andExpect(jsonPath("$.plate").value("ABC123")) + .andExpect(jsonPath("$.letterText").value("Hej fin bil!")) + .andExpect(jsonPath("$.status").value("pending_payment")) + .andExpect(jsonPath("$.guestToken").value(guestToken.toString())); + } + + @Test + void shouldRejectInvalidPlateFormat() throws Exception { + mockMvc.perform(post("/api/guest-orders") + .contentType("application/json") + .content("{\"plate\":\"INVALID\",\"letterText\":\"Hej\",\"email\":\"guest@example.com\"}")) + .andExpect(status().isBadRequest()); + } + + @Test + void shouldRejectBlankEmail() throws Exception { + mockMvc.perform(post("/api/guest-orders") + .contentType("application/json") + .content("{\"plate\":\"ABC123\",\"letterText\":\"Hej\",\"email\":\"\"}")) + .andExpect(status().isBadRequest()); + } + + @Test + void shouldRejectInvalidEmail() throws Exception { + mockMvc.perform(post("/api/guest-orders") + .contentType("application/json") + .content("{\"plate\":\"ABC123\",\"letterText\":\"Hej\",\"email\":\"not-an-email\"}")) + .andExpect(status().isBadRequest()); + } + + @Test + void shouldRejectBlankLetterText() throws Exception { + mockMvc.perform(post("/api/guest-orders") + .contentType("application/json") + .content("{\"plate\":\"ABC123\",\"letterText\":\"\",\"email\":\"guest@example.com\"}")) + .andExpect(status().isBadRequest()); + } + + // --- GET /api/guest-orders/{token} --- + + @Test + void shouldGetGuestOrderByToken() throws Exception { + UUID token = UUID.fromString("e2eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + UUID orderId = UUID.fromString("d1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + + Order order = new Order(); + order.setId(orderId); + order.setPlate("ABC123"); + order.setLetterText("Hej bil!"); + order.setStatus(OrderStatus.PROCESSING); + order.setGuestToken(token); + order.setAmountPaid(new BigDecimal("49.00")); + order.setCreatedAt(Instant.parse("2026-06-19T19:00:00Z")); + + when(orderService.getOrderByGuestToken(token)).thenReturn(order); + + mockMvc.perform(get("/api/guest-orders/{token}", token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(orderId.toString())) + .andExpect(jsonPath("$.plate").value("ABC123")) + .andExpect(jsonPath("$.status").value("processing")) + .andExpect(jsonPath("$.guestToken").value(token.toString())) + .andExpect(jsonPath("$.amountPaid").value(49.00)); + } + + @Test + void shouldReturn404WhenGuestOrderTokenNotFound() throws Exception { + UUID token = UUID.randomUUID(); + when(orderService.getOrderByGuestToken(token)) + .thenThrow(new OrderNotFoundException(token)); + + mockMvc.perform(get("/api/guest-orders/{token}", token)) + .andExpect(status().isNotFound()); + } + + // --- POST /api/guest-orders/{token}/pay --- + + @Test + void shouldConfirmGuestPayment() throws Exception { + UUID token = UUID.fromString("e2eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + UUID orderId = UUID.fromString("d1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + + Order order = new Order(); + order.setId(orderId); + order.setPlate("ABC123"); + order.setLetterText("Hej bil!"); + order.setStatus(OrderStatus.PROCESSING); + order.setGuestToken(token); + + when(orderService.confirmGuestPayment(token)).thenReturn(order); + + mockMvc.perform(post("/api/guest-orders/{token}/pay", token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(orderId.toString())) + .andExpect(jsonPath("$.status").value("processing")); + } + + @Test + void shouldReturn409WhenPaymentAlreadyConfirmed() throws Exception { + UUID token = UUID.randomUUID(); + when(orderService.confirmGuestPayment(token)) + .thenThrow(new InvalidOrderStateException("Beställningen kan inte ändras i detta tillstånd")); + + mockMvc.perform(post("/api/guest-orders/{token}/pay", token)) + .andExpect(status().isConflict()); + } + + @Test + void shouldReturn404WhenPayingWithUnknownToken() throws Exception { + UUID token = UUID.randomUUID(); + when(orderService.confirmGuestPayment(token)) + .thenThrow(new OrderNotFoundException(token)); + + mockMvc.perform(post("/api/guest-orders/{token}/pay", token)) + .andExpect(status().isNotFound()); + } +} diff --git a/backend/src/test/java/se/bilhalsning/service/OrderServiceTest.java b/backend/src/test/java/se/bilhalsning/service/OrderServiceTest.java index 1c61aa5..48a3a51 100644 --- a/backend/src/test/java/se/bilhalsning/service/OrderServiceTest.java +++ b/backend/src/test/java/se/bilhalsning/service/OrderServiceTest.java @@ -253,4 +253,122 @@ class OrderServiceTest { () -> orderService.confirmPayment(orderId, otherUserId)); } + // --- Guest order: createGuestOrder --- + + @Test + void shouldCreateGuestOrderWithPendingPaymentStatus() { + when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0)); + + Order result = orderService.createGuestOrder("ABC123", "Hej fin bil!", "guest@example.com"); + + assertEquals(OrderStatus.PENDING_PAYMENT, result.getStatus()); + assertEquals("ABC123", result.getPlate()); + assertEquals("Hej fin bil!", result.getLetterText()); + assertEquals("guest@example.com", result.getGuestEmail()); + assertNull(result.getUserId()); + } + + @Test + void shouldNormalizeGuestPlateToUppercaseAndTrim() { + when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0)); + + Order result = orderService.createGuestOrder(" abc123 ", "Test", "guest@example.com"); + + assertEquals("ABC123", result.getPlate()); + } + + @Test + void shouldNormalizeGuestEmailToLowercaseAndTrim() { + when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0)); + + Order result = orderService.createGuestOrder("ABC123", "Test", " Guest@EXAMPLE.COM "); + + assertEquals("guest@example.com", result.getGuestEmail()); + } + + @Test + void shouldHandleNullGuestEmail() { + when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0)); + + Order result = orderService.createGuestOrder("ABC123", "Test", null); + + assertNull(result.getGuestEmail()); + } + + // --- Guest order: getOrderByGuestToken --- + + @Test + void shouldGetGuestOrderByToken() { + UUID token = UUID.randomUUID(); + Order order = new Order(); + order.setGuestToken(token); + order.setStatus(OrderStatus.PENDING_PAYMENT); + when(orderRepository.findByGuestToken(token)).thenReturn(Optional.of(order)); + + Order result = orderService.getOrderByGuestToken(token); + + assertSame(order, result); + } + + @Test + void shouldThrowWhenGuestTokenNotFound() { + UUID token = UUID.randomUUID(); + when(orderRepository.findByGuestToken(token)).thenReturn(Optional.empty()); + + assertThrows(OrderNotFoundException.class, + () -> orderService.getOrderByGuestToken(token)); + } + + @Test + void shouldThrowWhenGuestTokenPointsToUserOwnedOrder() { + UUID token = UUID.randomUUID(); + Order order = new Order(); + order.setGuestToken(token); + order.setUserId(UUID.randomUUID()); // security: user-owned order must not be exposed + when(orderRepository.findByGuestToken(token)).thenReturn(Optional.of(order)); + + assertThrows(OrderNotFoundException.class, + () -> orderService.getOrderByGuestToken(token)); + } + + // --- Guest order: confirmGuestPayment --- + + @Test + void shouldConfirmGuestPaymentForPendingOrder() { + UUID token = UUID.randomUUID(); + Order order = new Order(); + order.setGuestToken(token); + order.setStatus(OrderStatus.PENDING_PAYMENT); + when(orderRepository.findByGuestToken(token)).thenReturn(Optional.of(order)); + when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0)); + + Order result = orderService.confirmGuestPayment(token); + + assertEquals(OrderStatus.PROCESSING, result.getStatus()); + verify(orderNotificationService).notifyOrderProcessing(result); + } + + @Test + void shouldThrowWhenConfirmingGuestPaymentForNonPendingOrder() { + UUID token = UUID.randomUUID(); + Order order = new Order(); + order.setGuestToken(token); + order.setStatus(OrderStatus.CANCELLED); + when(orderRepository.findByGuestToken(token)).thenReturn(Optional.of(order)); + + assertThrows(InvalidOrderStateException.class, + () -> orderService.confirmGuestPayment(token)); + verify(orderRepository, never()).save(any(Order.class)); + verify(orderNotificationService, never()).notifyOrderProcessing(any()); + } + + @Test + void shouldThrowWhenConfirmingGuestPaymentForUnknownToken() { + UUID token = UUID.randomUUID(); + when(orderRepository.findByGuestToken(token)).thenReturn(Optional.empty()); + + assertThrows(OrderNotFoundException.class, + () -> orderService.confirmGuestPayment(token)); + } + }