test: Add Test for ApiController

This commit is contained in:
js0ny 2025-10-19 02:40:19 +01:00
parent 0706d8966f
commit 6d14e5c2aa
4 changed files with 285 additions and 26 deletions

View file

@ -3,18 +3,29 @@ package io.github.js0ny.ilp_coursework.exception;
import java.util.Map;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* Class that handles exception or failed request. Map all error requests to 400.
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/// Use a logger to save logs instead of passing them to user
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private final Map<String, String> badRequestMap = Map.of("status", "400", "error", "Bad Request");
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleHttpMessageNotReadable(HttpMessageNotReadableException ex) {
return Map.of("status", "400", "error", "Invalid JSON request body.");
log.warn("Malformed JSON received: {}", ex.getMessage());
return badRequestMap;
}
@ExceptionHandler(IllegalArgumentException.class)
@ -22,7 +33,21 @@ public class GlobalExceptionHandler {
public Map<String, String> handleIllegalArgument(IllegalArgumentException ex) {
String errorMessage = Optional.ofNullable(ex.getMessage())
.orElse("Invalid argument provided.");
return Map.of("status", "400", "error", errorMessage);
log.warn("Illegal argument in request: {}", errorMessage);
return badRequestMap;
}
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleNullPointerException(Exception ex) {
log.error("NullPointerException occurred. Return 400 by default.", ex);
return badRequestMap;
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleGeneralException(Exception ex) {
log.error("Fallback exception received: {}", ex.getMessage());
return badRequestMap;
}
}

View file

@ -1,20 +1,28 @@
package io.github.js0ny.ilp_coursework.controller;
import static org.mockito.ArgumentMatchers.*;
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 com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.js0ny.ilp_coursework.data.DistanceRequestDto;
import io.github.js0ny.ilp_coursework.data.LngLatDto;
import io.github.js0ny.ilp_coursework.data.*;
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryDependsOnPostProcessor;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import java.util.List;
@WebMvcTest(ApiController.class)
public class ApiControllerTest {
@ -25,31 +33,207 @@ public class ApiControllerTest {
private ObjectMapper objectMapper;
@MockitoBean
private GpsCalculationService gpsCalculationService;
private GpsCalculationService service;
@Test
void getUid_shouldReturnStudentIdFromService() throws Exception {
String endpoint = "/api/v1/uid";
String expected = "s2522255";
var mock = mockMvc.perform(get(endpoint));
mock.andExpect(MockMvcResultMatchers.status().isOk());
mock.andExpect(MockMvcResultMatchers.content().string(expected));
@Nested
@DisplayName("GET /uid")
class GetUidTests {
@Test
@DisplayName("GET /uid -> 200 OK")
void getUid_shouldReturn200AndStudentIdFromService() throws Exception {
String endpoint = "/api/v1/uid";
String expected = "s2522255";
var mock = mockMvc.perform(get(endpoint));
mock.andExpect(MockMvcResultMatchers.status().isOk());
mock.andExpect(MockMvcResultMatchers.content().string(expected));
}
}
@Test
void getDistance_shouldReturnDoubleFromService_whenCorrectInput() throws Exception {
double expected = 5.0;
String endpoint = "/api/v1/distanceTo";
LngLatDto p1 = new LngLatDto(0, 4.0);
LngLatDto p2 = new LngLatDto(3.0, 0);
var req = new DistanceRequestDto(p1, p2);
var mock = mockMvc.perform(
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req))
);
@Nested
@DisplayName("POST /distanceTo")
class GetDistanceTests {
@Test
@DisplayName("POST /distanceTo -> 200 OK")
void getDistance_shouldReturn200AndDistance_whenCorrectInput() throws Exception {
double expected = 5.0;
String endpoint = "/api/v1/distanceTo";
LngLatDto p1 = new LngLatDto(0, 4.0);
LngLatDto p2 = new LngLatDto(3.0, 0);
var req = new DistanceRequestDto(p1, p2);
when(service.calculateDistance(any(LngLatDto.class), any(LngLatDto.class))).thenReturn(expected);
var mock = mockMvc.perform(
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)));
mock.andExpect(MockMvcResultMatchers.status().isOk());
mock.andExpect(MockMvcResultMatchers.content().string(String.valueOf(expected)));
}
@Test
@DisplayName("POST /distanceTo -> 400 Bad Request: Missing Field")
void getDistance_shouldReturn400_whenMissingField() throws Exception {
double expected = 5.0;
String endpoint = "/api/v1/distanceTo";
String req = """
{
"position1": {
"lng": 3.0,
"lat": 4.0
}
}
""";
when(service.calculateDistance(any(LngLatDto.class), isNull())).thenThrow(new NullPointerException());
var mock = mockMvc.perform(post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(req))
.andExpect(MockMvcResultMatchers.status().isBadRequest());
}
mock.andExpect(MockMvcResultMatchers.status().isOk());
mock.andExpect(MockMvcResultMatchers.content().string(String.valueOf(expected)));
}
@Nested
@DisplayName("POST /isCloseTo")
class IsCloseToTests {
@Test
@DisplayName("POST /isCloseTo -> 200 OK")
void getIsCloseTo_shouldReturn200AndBoolean_whenCorrectInput() throws Exception {
boolean expected = false;
String endpoint = "/api/v1/isCloseTo";
LngLatDto p1 = new LngLatDto(0, 4.0);
LngLatDto p2 = new LngLatDto(3.0, 0);
var req = new DistanceRequestDto(p1, p2);
when(service.isCloseTo(any(LngLatDto.class), any(LngLatDto.class))).thenReturn(expected);
var mock = mockMvc.perform(
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)));
mock.andExpect(MockMvcResultMatchers.status().isOk());
mock.andExpect(MockMvcResultMatchers.content().string(String.valueOf(expected)));
}
@Test
@DisplayName("POST /isCloseTo -> 400 Bad Request: Malformed JSON ")
void getIsCloseTo_shouldReturn400_whenJsonIsMalformed() throws Exception {
// json without a bracket
String malformedJson = """
{
"position1": { "lng": 0.0, "lat": 3.0 }
""";
mockMvc.perform(post("/api/v1/isCloseTo")
.contentType(MediaType.APPLICATION_JSON)
.content(malformedJson))
.andExpect(MockMvcResultMatchers.status().isBadRequest());
}
}
@Nested
@DisplayName("POST /nextPosition")
class GetNextPositionTests {
String endpoint = "/api/v1/nextPosition";
@Test
@DisplayName("POST /nextPosition -> 200 OK")
void getNextPosition_shouldReturn200AndCoordinate_whenCorrectInput() throws Exception {
LngLatDto expected = new LngLatDto(0.00015, 0.0);
LngLatDto p = new LngLatDto(0, 0);
var req = new MovementRequestDto(p, 0);
when(service.nextPosition(any(LngLatDto.class), anyDouble())).thenReturn(expected);
var mock = mockMvc.perform(
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)));
mock.andExpect(MockMvcResultMatchers.status().isOk());
mock.andExpect(MockMvcResultMatchers.content().json(
objectMapper.writeValueAsString(expected)));
}
@Test
@DisplayName("POST /nextPosition -> 400 Bad Request: Missing Field")
void getNextPosition_shouldReturn400_whenKeyNameError() throws Exception {
// "position" should be "start"
String malformedJson = """
{
"position": { "lng": 0.0, "lat": 3.0 },
"angle": 180
}
""";
when(service.nextPosition(isNull(), anyDouble())).thenThrow(new NullPointerException());
mockMvc.perform(post("/api/v1/nextPosition")
.contentType(MediaType.APPLICATION_JSON)
.content(malformedJson))
.andExpect(MockMvcResultMatchers.status().isBadRequest());
}
}
@Nested
@DisplayName("POST /isInRegion")
class GetIsInRegionTests {
@Test
@DisplayName("POST /isInRegion -> 200 OK")
void getIsInRegion_shouldReturn200AndBoolean_whenCorrectInput() throws Exception {
boolean expected = false;
String endpoint = "/api/v1/isInRegion";
var position = new LngLatDto(1.234, 1.222);
var region = new RegionDto("central",
List.of(new LngLatDto(-3.192473, 55.946233), new LngLatDto(-3.192473, 55.942617),
new LngLatDto(-3.184319, 55.942617), new LngLatDto(-3.184319, 55.946233),
new LngLatDto(-3.192473, 55.946233)));
var req = new RegionCheckRequestDto(position, region);
when(service.checkIsInRegion(any(LngLatDto.class), any(RegionDto.class))).thenReturn(expected);
var mock = mockMvc.perform(
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)));
mock.andExpect(MockMvcResultMatchers.status().isOk());
mock.andExpect(MockMvcResultMatchers.content().string(String.valueOf(expected)));
}
@Test
@DisplayName("POST /isInRegion -> 400 Bad Request: Passing a list of empty vertices to isInRegion")
void getIsInRegion_shouldReturn400_whenPassingIllegalArguments() throws Exception {
var position = new LngLatDto(1, 1);
var region = new RegionDto("illegal", List.of());
var request = new RegionCheckRequestDto(position, region);
when(service.checkIsInRegion(any(LngLatDto.class), any(RegionDto.class)))
.thenThrow(new IllegalArgumentException("Region is not closed."));
mockMvc.perform(post("/api/v1/isInRegion")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(MockMvcResultMatchers.status().isBadRequest());
}
@Test
@DisplayName("POST /isInRegion -> 400 Bad Request: Passing a list of not-closing vertices to isInRegion")
void getIsInRegion_shouldReturn400_whenPassingNotClosingVertices() throws Exception {
var position = new LngLatDto(1, 1);
var region = new RegionDto("illegal", List.of(
new LngLatDto(1, 2),
new LngLatDto(3, 4),
new LngLatDto(5, 6),
new LngLatDto(7, 8),
new LngLatDto(9, 10)
));
var request = new RegionCheckRequestDto(position, region);
when(service.checkIsInRegion(any(LngLatDto.class), any(RegionDto.class)))
.thenThrow(new IllegalArgumentException("Region is not closed."));
mockMvc.perform(post("/api/v1/isInRegion")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(MockMvcResultMatchers.status().isBadRequest());
}
}
}

View file

@ -382,5 +382,15 @@ public class GpsCalculationServiceTest {
service.checkIsInRegion(position, region);
}).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed.");
}
@Test
@DisplayName("Edge Case: Vertex list is empty")
void isInRegion_shouldThrowExceptions_whenListIsEmpty() {
var position = new LngLatDto(2.0, 2.0);
var region = new RegionDto("rectangle", List.of());
assertThatThrownBy(() -> {
service.checkIsInRegion(position, region);
}).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed.");
}
}
}