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.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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
@Nested
|
||||
@DisplayName("GET /uid")
|
||||
class GetUidTests {
|
||||
@Test
|
||||
void getUid_shouldReturnStudentIdFromService() throws Exception {
|
||||
@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));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("POST /distanceTo")
|
||||
class GetDistanceTests {
|
||||
@Test
|
||||
void getDistance_shouldReturnDoubleFromService_whenCorrectInput() throws Exception {
|
||||
@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))
|
||||
);
|
||||
.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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@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);
|
||||
}).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