package se.bilhalsning.controller; import static org.mockito.ArgumentMatchers.any; 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.patch; 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.time.Instant; import java.util.List; import java.util.Optional; 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.security.test.context.support.WithMockUser; 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.dto.OrderResponse; import se.bilhalsning.entity.User; import se.bilhalsning.security.JwtService; 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 OrderControllerTest { private static final String TEST_SECRET = "this-is-a-test-secret-that-is-at-least-32-bytes-long!!"; @Autowired private MockMvc mockMvc; @MockitoBean private OrderService orderService; @MockitoBean private UserService userService; @Test void shouldReturn401WhenNotAuthenticated() throws Exception { mockMvc.perform(get("/api/orders")) .andExpect(status().isUnauthorized()) .andExpect(jsonPath("$.message").exists()); } @Test @WithMockUser(username = "test@bilhej.se") void shouldReturnOrdersForAuthenticatedUser() throws Exception { UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); User user = new User(); user.setId(userId); user.setEmail("test@bilhej.se"); when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user)); when(orderService.getOrdersByUserId(userId)).thenReturn(List.of()); mockMvc.perform(get("/api/orders")) .andExpect(status().isOk()) .andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$").isEmpty()); } @Test @WithMockUser(username = "test@bilhej.se") void shouldReturnOrderWithAllFields() throws Exception { UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); User user = new User(); user.setId(userId); user.setEmail("test@bilhej.se"); when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user)); se.bilhalsning.entity.Order order = new se.bilhalsning.entity.Order(); order.setId(UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")); order.setUserId(userId); order.setPlate("ABC123"); order.setLetterText("Test letter"); order.setStatus(se.bilhalsning.entity.OrderStatus.SENT); order.setTrackingId("PN123456789"); when(orderService.getOrdersByUserId(userId)).thenReturn(List.of(order)); mockMvc.perform(get("/api/orders")) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].id").value("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")) .andExpect(jsonPath("$[0].plate").value("ABC123")) .andExpect(jsonPath("$[0].letterText").value("Test letter")) .andExpect(jsonPath("$[0].status").value("sent")) .andExpect(jsonPath("$[0].trackingId").value("PN123456789")); } @Test @WithMockUser(username = "unknown@example.com") void shouldReturn401WhenUserNotFound() throws Exception { when(userService.findByEmail("unknown@example.com")).thenReturn(Optional.empty()); mockMvc.perform(get("/api/orders")) .andExpect(status().isUnauthorized()); } @Test void shouldReturn401WhenPostingWithoutAuth() throws Exception { mockMvc.perform(post("/api/orders") .contentType("application/json") .content("{\"plate\":\"ABC123\",\"letterText\":\"Hej\"}")) .andExpect(status().isUnauthorized()) .andExpect(jsonPath("$.message").exists()); } @Test void shouldReturn401WithSwedishMessageWhenTokenExpired() throws Exception { JwtService expiredJwtService = new JwtService(TEST_SECRET, -1000L); String expiredToken = expiredJwtService.generateToken("test@bilhej.se"); mockMvc.perform(get("/api/orders") .header("Authorization", "Bearer " + expiredToken)) .andExpect(status().isUnauthorized()) .andExpect(jsonPath("$.message").exists()); } @Test void shouldReturn401WithMessageWhenNoAuthHeader() throws Exception { mockMvc.perform(get("/api/orders")) .andExpect(status().isUnauthorized()) .andExpect(jsonPath("$.message") .value(org.hamcrest.Matchers.containsString("session"))); } @Test @WithMockUser(username = "test@bilhej.se") void shouldCreateOrderSuccessfully() throws Exception { UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); User user = new User(); user.setId(userId); user.setEmail("test@bilhej.se"); when(userService.findByEmail("test@bilhej.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.setLetterText("Hej fin bil!"); savedOrder.setStatus(se.bilhalsning.entity.OrderStatus.PENDING_PAYMENT); when(orderService.createOrder(userId, "ABC123", "Hej fin bil!")) .thenReturn(savedOrder); mockMvc.perform(post("/api/orders") .contentType("application/json") .content("{\"plate\":\"ABC123\",\"letterText\":\"Hej fin bil!\"}")) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").value("d1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")) .andExpect(jsonPath("$.plate").value("ABC123")) .andExpect(jsonPath("$.letterText").value("Hej fin bil!")) .andExpect(jsonPath("$.status").value("pending_payment")); } @Test @WithMockUser(username = "test@bilhej.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@bilhej.se") void shouldRejectEmptyLetterText() throws Exception { mockMvc.perform(post("/api/orders") .contentType("application/json") .content("{\"plate\":\"ABC123\",\"letterText\":\"\"}")) .andExpect(status().isBadRequest()); } @Test @WithMockUser(username = "test@bilhej.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()); } @Test @WithMockUser(username = "test@bilhej.se") void shouldGetSingleOrderForOwner() throws Exception { UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); User user = new User(); user.setId(userId); user.setEmail("test@bilhej.se"); when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user)); se.bilhalsning.entity.Order order = new se.bilhalsning.entity.Order(); order.setId(orderId); order.setUserId(userId); order.setPlate("ABC123"); order.setLetterText("Test letter"); order.setStatus(se.bilhalsning.entity.OrderStatus.PENDING_PAYMENT); when(orderService.getOrderById(orderId)).thenReturn(order); mockMvc.perform(get("/api/orders/" + orderId)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(orderId.toString())) .andExpect(jsonPath("$.plate").value("ABC123")) .andExpect(jsonPath("$.status").value("pending_payment")); } @Test @WithMockUser(username = "test@bilhej.se") void shouldReturn404WhenGettingOtherUsersOrder() throws Exception { UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); User user = new User(); user.setId(userId); user.setEmail("test@bilhej.se"); when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user)); se.bilhalsning.entity.Order order = new se.bilhalsning.entity.Order(); order.setId(orderId); order.setUserId(UUID.randomUUID()); when(orderService.getOrderById(orderId)).thenReturn(order); mockMvc.perform(get("/api/orders/" + orderId)) .andExpect(status().isNotFound()); } @Test @WithMockUser(username = "test@bilhej.se") void shouldPatchOrderSuccessfully() throws Exception { UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); User user = new User(); user.setId(userId); user.setEmail("test@bilhej.se"); when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user)); se.bilhalsning.entity.Order order = new se.bilhalsning.entity.Order(); order.setId(orderId); order.setUserId(userId); order.setPlate("ABC123"); order.setLetterText("Updated text"); order.setStatus(se.bilhalsning.entity.OrderStatus.PENDING_PAYMENT); when(orderService.updatePendingOrder(any(), any(), any())).thenReturn(order); mockMvc.perform(patch("/api/orders/" + orderId) .contentType("application/json") .content("{\"letterText\":\"Updated text\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.letterText").value("Updated text")); } @Test @WithMockUser(username = "test@bilhej.se") void shouldRejectPatchWithEmptyLetterText() throws Exception { UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); mockMvc.perform(patch("/api/orders/" + orderId) .contentType("application/json") .content("{\"letterText\":\"\"}")) .andExpect(status().isBadRequest()); } @Test @WithMockUser(username = "test@bilhej.se") void shouldCancelOrderSuccessfully() throws Exception { UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); User user = new User(); user.setId(userId); user.setEmail("test@bilhej.se"); when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user)); se.bilhalsning.entity.Order order = new se.bilhalsning.entity.Order(); order.setId(orderId); order.setUserId(userId); order.setPlate("ABC123"); order.setLetterText("Test letter"); order.setStatus(se.bilhalsning.entity.OrderStatus.CANCELLED); when(orderService.cancelOrder(orderId, userId)).thenReturn(order); mockMvc.perform(post("/api/orders/" + orderId + "/cancel")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("cancelled")); } }