test: Add Test for ApiController
This commit is contained in:
parent
0706d8966f
commit
6d14e5c2aa
4 changed files with 285 additions and 26 deletions
40
flake.nix
Normal file
40
flake.nix
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
description = "Flake for environment building ILP CW";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
}: {
|
||||||
|
devShells =
|
||||||
|
nixpkgs.lib.genAttrs [
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-darwin"
|
||||||
|
] (system: let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
jdk21
|
||||||
|
gradle
|
||||||
|
docker
|
||||||
|
docker-compose
|
||||||
|
httpie
|
||||||
|
podman
|
||||||
|
podman-compose
|
||||||
|
newman
|
||||||
|
];
|
||||||
|
shellHook = ''
|
||||||
|
export JAVA_HOME=${pkgs.jdk21}
|
||||||
|
echo "Java: $(java-version | head -n 1)"
|
||||||
|
echo "Docker: $(docker --version)"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -3,18 +3,29 @@ package io.github.js0ny.ilp_coursework.exception;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that handles exception or failed request. Map all error requests to 400.
|
||||||
|
*/
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
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)
|
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
public Map<String, String> handleHttpMessageNotReadable(HttpMessageNotReadableException ex) {
|
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)
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
|
@ -22,7 +33,21 @@ public class GlobalExceptionHandler {
|
||||||
public Map<String, String> handleIllegalArgument(IllegalArgumentException ex) {
|
public Map<String, String> handleIllegalArgument(IllegalArgumentException ex) {
|
||||||
String errorMessage = Optional.ofNullable(ex.getMessage())
|
String errorMessage = Optional.ofNullable(ex.getMessage())
|
||||||
.orElse("Invalid argument provided.");
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,28 @@
|
||||||
package io.github.js0ny.ilp_coursework.controller;
|
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.get;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
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 com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.github.js0ny.ilp_coursework.data.DistanceRequestDto;
|
import io.github.js0ny.ilp_coursework.data.*;
|
||||||
import io.github.js0ny.ilp_coursework.data.LngLatDto;
|
|
||||||
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
|
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.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@WebMvcTest(ApiController.class)
|
@WebMvcTest(ApiController.class)
|
||||||
public class ApiControllerTest {
|
public class ApiControllerTest {
|
||||||
|
|
||||||
|
|
@ -25,31 +33,207 @@ public class ApiControllerTest {
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
@MockitoBean
|
@MockitoBean
|
||||||
private GpsCalculationService gpsCalculationService;
|
private GpsCalculationService service;
|
||||||
|
|
||||||
@Test
|
@Nested
|
||||||
void getUid_shouldReturnStudentIdFromService() throws Exception {
|
@DisplayName("GET /uid")
|
||||||
String endpoint = "/api/v1/uid";
|
class GetUidTests {
|
||||||
String expected = "s2522255";
|
@Test
|
||||||
var mock = mockMvc.perform(get(endpoint));
|
@DisplayName("GET /uid -> 200 OK")
|
||||||
mock.andExpect(MockMvcResultMatchers.status().isOk());
|
void getUid_shouldReturn200AndStudentIdFromService() throws Exception {
|
||||||
mock.andExpect(MockMvcResultMatchers.content().string(expected));
|
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
|
@Nested
|
||||||
void getDistance_shouldReturnDoubleFromService_whenCorrectInput() throws Exception {
|
@DisplayName("POST /distanceTo")
|
||||||
double expected = 5.0;
|
class GetDistanceTests {
|
||||||
String endpoint = "/api/v1/distanceTo";
|
@Test
|
||||||
LngLatDto p1 = new LngLatDto(0, 4.0);
|
@DisplayName("POST /distanceTo -> 200 OK")
|
||||||
LngLatDto p2 = new LngLatDto(3.0, 0);
|
void getDistance_shouldReturn200AndDistance_whenCorrectInput() throws Exception {
|
||||||
var req = new DistanceRequestDto(p1, p2);
|
double expected = 5.0;
|
||||||
var mock = mockMvc.perform(
|
String endpoint = "/api/v1/distanceTo";
|
||||||
post(endpoint)
|
LngLatDto p1 = new LngLatDto(0, 4.0);
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
LngLatDto p2 = new LngLatDto(3.0, 0);
|
||||||
.content(objectMapper.writeValueAsString(req))
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -382,5 +382,15 @@ public class GpsCalculationServiceTest {
|
||||||
service.checkIsInRegion(position, region);
|
service.checkIsInRegion(position, region);
|
||||||
}).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed.");
|
}).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.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue