test(guest): add backend unit tests for guest checkout to fix CI
The guest checkout PR (#17) added new backend code without any unit tests, causing the jacocoTestCoverageVerification CI step to fail (coverage dropped below 70% line / 60% branch thresholds). OrderServiceTest — 10 new tests covering: - createGuestOrder: correct fields, plate normalization, email normalization (lowercase+trim), null email handling - getOrderByGuestToken: successful lookup, not-found, security check (refuses to serve user-owned orders via guest token) - confirmGuestPayment: success path (status→PROCESSING, notification), non-pending order throws, unknown token throws GuestOrderControllerTest — new file, 8 tests covering: - POST /api/guest-orders: create without auth (201), validation (bad plate, blank email, invalid email, blank letter text) - GET /api/guest-orders/{token}: lookup (200), not-found (404) - POST /api/guest-orders/{token}/pay: confirm (200), conflict (409), not-found (404) All tests follow existing patterns (MockitoExtension for service, SpringBootTest+MockMvc for controller). Cannot run backend tests locally (no JDK in agent sandbox) — CI will verify.
This commit is contained in:
parent
08fcbba580
commit
a0cefb2646
2 changed files with 297 additions and 0 deletions
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue