test(guest): add backend unit tests for guest checkout to fix CI
Some checks failed
CI / Lint, type check, unit tests, coverage (pull_request) Failing after 1m21s
CI / E2E browser tests (pull_request) Successful in 3m52s

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:
Hermes Agent 2026-06-19 19:33:31 +00:00
parent 08fcbba580
commit a0cefb2646
2 changed files with 297 additions and 0 deletions

View file

@ -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());
}
}

View file

@ -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));
}
}