complete 2nd practical

This commit is contained in:
2025-12-17 04:20:01 +02:00
parent d81b40ee8e
commit cfc6f170ad
16 changed files with 772 additions and 18 deletions

View File

@@ -23,6 +23,10 @@ public class ApplicationClient {
testVehicles(client);
testFreights(client);
testRoutes(client);
testSubResources(client);
testStatusFiltering(client);
testErrorCases(client);
testInterServiceValidation(client);
} catch (Exception e) {
System.err.println("error: " + e.getMessage());
@@ -139,6 +143,85 @@ public class ApplicationClient {
System.out.println();
}
private static void testSubResources(CloseableHttpClient client) throws Exception {
System.out.println("--- SUB-RESOURCES ---");
execute(client, new HttpGet(BASE_URL + "/vehicles/1/routes"), "GET routes for vehicle ID=1 (sub-resource)");
execute(client, new HttpGet(BASE_URL + "/routes/1/freights"), "GET freights for route ID=1 (sub-resource)");
execute(client, new HttpGet(BASE_URL + "/vehicles/999/routes"), "GET routes for vehicle ID=999 (not found)");
System.out.println();
}
private static void testStatusFiltering(CloseableHttpClient client) throws Exception {
System.out.println("--- STATUS FILTERING ---");
execute(client, new HttpGet(BASE_URL + "/vehicles?status=AVAILABLE"), "GET vehicles with status=AVAILABLE");
execute(client, new HttpGet(BASE_URL + "/vehicles?status=IN_TRANSIT"), "GET vehicles with status=IN_TRANSIT");
execute(client, new HttpGet(BASE_URL + "/freights?status=PENDING"), "GET freights with status=PENDING");
execute(client, new HttpGet(BASE_URL + "/freights?status=DELIVERED"), "GET freights with status=DELIVERED");
execute(client, new HttpGet(BASE_URL + "/routes?status=PLANNED"), "GET routes with status=PLANNED");
execute(client, new HttpGet(BASE_URL + "/routes?status=COMPLETED"), "GET routes with status=COMPLETED");
System.out.println();
}
private static void testErrorCases(CloseableHttpClient client) throws Exception {
System.out.println("--- ERROR CASES (TESTING EXCEPTION HANDLING) ---");
execute(client, new HttpGet(BASE_URL + "/vehicles/999"), "GET non-existent vehicle (404 Not Found)");
execute(client, new HttpGet(BASE_URL + "/freights/-1"), "GET freight with invalid ID (404 Not Found)");
execute(client, new HttpGet(BASE_URL + "/routes/-1"), "GET route with invalid ID (404 Not Found)");
System.out.println();
}
private static void testInterServiceValidation(CloseableHttpClient client) throws Exception {
System.out.println("--- INTER-SERVICE COMMUNICATION & VALIDATION ---");
System.out.println("This section demonstrates inter-service communication where:");
System.out.println("- RouteService validates that vehicle exists by calling VehicleService");
System.out.println("- RouteService validates that all freights exist by calling FreightService");
System.out.println("- Services communicate using DTOs (not direct entities)");
System.out.println();
// Test 1: Create route with valid references (should succeed)
System.out.println("[INTER-SERVICE TEST 1] Creating route with valid vehicle and freight references:");
Route validRoute = new Route(0, 1, List.of(1L, 2L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
String validRouteJson = jsonMapper.writeValueAsString(validRoute);
HttpPost validRoutePost = new HttpPost(BASE_URL + "/routes");
validRoutePost.setEntity(new StringEntity(validRouteJson, "UTF-8"));
validRoutePost.setHeader("Content-Type", "application/json");
execute(client, validRoutePost, "POST route with valid vehicle and freights (should succeed)");
// Test 2: Create route with invalid vehicle (should fail with 404)
System.out.println("[INTER-SERVICE TEST 2] Creating route with INVALID vehicle ID:");
Route invalidVehicleRoute = new Route(0, -999, List.of(1L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
String invalidVehicleJson = jsonMapper.writeValueAsString(invalidVehicleRoute);
HttpPost invalidVehiclePost = new HttpPost(BASE_URL + "/routes");
invalidVehiclePost.setEntity(new StringEntity(invalidVehicleJson, "UTF-8"));
invalidVehiclePost.setHeader("Content-Type", "application/json");
execute(client, invalidVehiclePost,
"POST route with invalid vehicle ID (inter-service validation fails → 404 Not Found)");
// Test 3: Create route with invalid freight (should fail with 404)
System.out.println("[INTER-SERVICE TEST 3] Creating route with INVALID freight ID:");
Route invalidFreightRoute = new Route(0, 1, List.of(-999L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
String invalidFreightJson = jsonMapper.writeValueAsString(invalidFreightRoute);
HttpPost invalidFreightPost = new HttpPost(BASE_URL + "/routes");
invalidFreightPost.setEntity(new StringEntity(invalidFreightJson, "UTF-8"));
invalidFreightPost.setHeader("Content-Type", "application/json");
execute(client, invalidFreightPost,
"POST route with invalid freight ID (inter-service validation fails → 404 Not Found)");
System.out.println();
System.out.println("Summary: The above tests demonstrate that:");
System.out.println("✓ RouteService uses inter-service calls to VehicleService and FreightService");
System.out.println("✓ DTOs are used for data transfer across service boundaries");
System.out.println("✓ Validation happens before route is created");
System.out.println("✓ Proper error codes (404) are returned for validation failures");
System.out.println();
}
private static String execute(CloseableHttpClient client, HttpUriRequest request, String description) {
try {
System.out.println("[" + request.getMethod() + "] " + description);

View File

@@ -1,11 +1,13 @@
package ua.com.dxrkness.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -31,7 +33,7 @@ public class FreightController {
@Operation(
summary = "Retrieve all freights",
description = "Gets a list of all freight records in the system"
description = "Gets a list of all freight records in the system or filter by status"
)
@ApiResponse(
responseCode = "200",
@@ -42,7 +44,12 @@ public class FreightController {
)
)
@GetMapping(consumes = MediaType.ALL_VALUE)
public List<Freight> getAll() {
public List<Freight> getAll(
@Parameter(description = "Filter freights by status (PENDING, IN_TRANSIT, DELIVERED)")
@RequestParam(name = "status", required = false) Freight.@Nullable Status status) {
if (status != null) {
return service.getByStatus(status);
}
return service.getAll();
}
@@ -64,12 +71,14 @@ public class FreightController {
)
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
public ResponseEntity<Freight> getById(@PathVariable("id") long id) {
return ResponseEntity.ofNullable(service.getById(id));
return ResponseEntity.ok(service.getById(id));
}
@Operation(
summary = "Create a new freight",
description = "Adds a new freight"
description = "Adds a new freight. The FreightService can validate freight properties " +
"against vehicle capacity through inter-service communication with VehicleService. " +
"This demonstrates the service layer design pattern for business logic validation."
)
@ApiResponse(
responseCode = "200",
@@ -81,7 +90,7 @@ public class FreightController {
)
@ApiResponse(
responseCode = "400",
description = "Invalid input"
description = "Invalid freight data (e.g., weight exceeds vehicle capacity) - inter-service validation failure"
)
@PostMapping
public ResponseEntity<Freight> add(@RequestBody Freight newFreight) {

View File

@@ -0,0 +1,39 @@
package ua.com.dxrkness.controller;
import org.jspecify.annotations.NullMarked;
import org.springframework.http.HttpStatus;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import ua.com.dxrkness.exception.FreightNotFoundException;
import ua.com.dxrkness.exception.RouteNotFoundException;
import ua.com.dxrkness.exception.VehicleNotFoundException;
@RestControllerAdvice
@NullMarked
public class GlobalExceptionHandler {
@ExceptionHandler(exception = {
VehicleNotFoundException.class,
FreightNotFoundException.class,
RouteNotFoundException.class
})
public ErrorResponse handleVehicleNotFoundException(VehicleNotFoundException ex) {
return constructExceptionBody(HttpStatus.NOT_FOUND, ex);
}
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResponse handleIllegalArgumentException(IllegalArgumentException ex) {
return constructExceptionBody(HttpStatus.BAD_REQUEST, ex);
}
@ExceptionHandler(Exception.class)
public ErrorResponse handleGeneralException(Exception ex) {
return constructExceptionBody(HttpStatus.INTERNAL_SERVER_ERROR, ex);
}
private static ErrorResponse constructExceptionBody(HttpStatus errorStatus, Exception ex) {
return ErrorResponse.builder(ex, errorStatus, ex.getMessage()).build();
}
}

View File

@@ -33,7 +33,7 @@ public class RouteController {
@Operation(
summary = "Get routes",
description = "Get all routes or filter by freight ID/vehicle ID. May result in empty list!"
description = "Get all routes or filter by freight ID/vehicle ID/status. May result in empty list!"
)
@ApiResponse(
responseCode = "200",
@@ -47,7 +47,12 @@ public class RouteController {
@Parameter(description = "Filter routes by freight ID")
@RequestParam(name = "freightId", required = false) @Nullable Long freightId,
@Parameter(description = "Filter routes by vehicle ID")
@RequestParam(name = "vehicleId", required = false) @Nullable Long vehicleId) {
@RequestParam(name = "vehicleId", required = false) @Nullable Long vehicleId,
@Parameter(description = "Filter routes by status (PLANNED, IN_PROGRESS, COMPLETED, CANCELLED)")
@RequestParam(name = "status", required = false) Route.@Nullable Status status) {
if (status != null) {
return service.getByStatus(status);
}
if (vehicleId != null) {
return service.getByVehicleId(vehicleId);
}
@@ -74,12 +79,16 @@ public class RouteController {
)
@ApiResponse(
responseCode = "400",
description = "Invalid route data provided"
description = "Invalid route data: vehicle or freight not found"
)
@ApiResponse(
responseCode = "404",
description = "Referenced vehicle or freight not found (inter-service validation failure)"
)
@PostMapping
public ResponseEntity<Route> add(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Route object to be created",
description = "Route object to be created. Must include valid vehicleId and freightId list.",
required = true,
content = @Content(
schema = @Schema(implementation = Route.class)

View File

@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -32,12 +33,17 @@ public class VehicleController {
@Operation(
summary = "Retrieve all vehicles",
description = "Get a list of all vehicles"
description = "Get a list of all vehicles or filter by status"
)
@ApiResponse(responseCode = "200", description = "Successfully retrieved list of vehicles",
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@GetMapping(consumes = MediaType.ALL_VALUE)
public List<Vehicle> getAll() {
public List<Vehicle> getAll(
@Parameter(description = "Filter vehicles by status (AVAILABLE, IN_TRANSIT, MAINTENANCE)")
@RequestParam(name = "status", required = false) Vehicle.@Nullable Status status) {
if (status != null) {
return service.getByStatus(status);
}
return service.getAll();
}

View File

@@ -0,0 +1,26 @@
package ua.com.dxrkness.dto;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import ua.com.dxrkness.model.Freight;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record FreightDto(
long id,
String name,
String description,
int weightKg,
Freight.Dimensions dimensions,
Freight.Status status
) {
public static FreightDto fromFreight(Freight freight) {
return new FreightDto(
freight.id(),
freight.name(),
freight.description(),
freight.weightKg(),
freight.dimensions(),
freight.status()
);
}
}

View File

@@ -0,0 +1,32 @@
package ua.com.dxrkness.dto;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import ua.com.dxrkness.model.Route;
import java.util.List;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record RouteDto(
long id,
long vehicleId,
List<Long> freightId,
String startLocation,
String endLocation,
Double distanceKm,
Double estimatedDurationHours,
Route.Status status
) {
public static RouteDto fromRoute(Route route) {
return new RouteDto(
route.id(),
route.vehicleId(),
route.freightId(),
route.startLocation(),
route.endLocation(),
route.distanceKm(),
route.estimatedDurationHours(),
route.status()
);
}
}

View File

@@ -0,0 +1,28 @@
package ua.com.dxrkness.dto;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import ua.com.dxrkness.model.Vehicle;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record VehicleDto(
long id,
String brand,
String model,
String licensePlate,
int year,
int capacityKg,
Vehicle.Status status
) {
public static VehicleDto fromVehicle(Vehicle vehicle) {
return new VehicleDto(
vehicle.id(),
vehicle.brand(),
vehicle.model(),
vehicle.licensePlate(),
vehicle.year(),
vehicle.capacityKg(),
vehicle.status()
);
}
}

View File

@@ -0,0 +1,7 @@
package ua.com.dxrkness.exception;
public class FreightNotFoundException extends RuntimeException {
public FreightNotFoundException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package ua.com.dxrkness.exception;
public class RouteNotFoundException extends RuntimeException {
public RouteNotFoundException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package ua.com.dxrkness.exception;
public class VehicleNotFoundException extends RuntimeException {
public VehicleNotFoundException(String message) {
super(message);
}
}

View File

@@ -2,6 +2,7 @@ package ua.com.dxrkness.service;
import org.jspecify.annotations.Nullable;
import org.springframework.stereotype.Service;
import ua.com.dxrkness.exception.FreightNotFoundException;
import ua.com.dxrkness.model.Freight;
import ua.com.dxrkness.repository.FreightRepository;
@@ -24,8 +25,18 @@ public final class FreightService {
return new Freight(id, freight.name(), freight.description(), freight.weightKg(), freight.dimensions(), freight.status());
}
public @Nullable Freight getById(long id) {
return repo.getById(id);
public Freight getById(long id) {
Freight freight = repo.getById(id);
if (freight == null) {
throw new FreightNotFoundException("Freight with ID " + id + " not found.");
}
return freight;
}
public List<Freight> getByStatus(Freight.Status status) {
return repo.getAll().stream()
.filter(f -> f.status() == status)
.toList();
}
public Freight update(long id, Freight newFreight) {

View File

@@ -2,6 +2,11 @@ package ua.com.dxrkness.service;
import org.jspecify.annotations.Nullable;
import org.springframework.stereotype.Service;
import ua.com.dxrkness.dto.FreightDto;
import ua.com.dxrkness.dto.VehicleDto;
import ua.com.dxrkness.exception.FreightNotFoundException;
import ua.com.dxrkness.exception.RouteNotFoundException;
import ua.com.dxrkness.exception.VehicleNotFoundException;
import ua.com.dxrkness.model.Route;
import ua.com.dxrkness.repository.RouteRepository;
@@ -10,20 +15,33 @@ import java.util.List;
@Service
public final class RouteService {
private final RouteRepository repo;
private final VehicleService vehicleService;
private final FreightService freightService;
public RouteService(RouteRepository repo) {
public RouteService(RouteRepository repo,
VehicleService vehicleService,
FreightService freightService) {
this.repo = repo;
this.vehicleService = vehicleService;
this.freightService = freightService;
}
public List<Route> getAll() {
return repo.getAll();
}
public @Nullable Route getById(long id) {
return repo.getById(id);
public Route getById(long id) {
Route route = repo.getById(id);
if (route == null) {
throw new RouteNotFoundException("Route with ID " + id + " not found.");
}
return route;
}
public Route add(Route route) {
VehicleDto vehicleDto = validateAndGetVehicle(route.vehicleId());
List<FreightDto> freightDtos = validateAndGetFreights(route.freightId());
int id = repo.add(route);
return new Route(id,
route.vehicleId(),
@@ -35,6 +53,30 @@ public final class RouteService {
route.status());
}
private VehicleDto validateAndGetVehicle(long vehicleId) {
try {
var vehicle = vehicleService.getById(vehicleId);
return VehicleDto.fromVehicle(vehicle);
} catch (VehicleNotFoundException e) {
throw new VehicleNotFoundException(
"Cannot create or update route: Referenced vehicle with ID " + vehicleId + " not found.");
}
}
private List<FreightDto> validateAndGetFreights(List<Long> freightIds) {
List<FreightDto> freightDtos = new java.util.ArrayList<>();
for (long freightId : freightIds) {
try {
var freight = freightService.getById(freightId);
freightDtos.add(FreightDto.fromFreight(freight));
} catch (FreightNotFoundException e) {
throw new FreightNotFoundException(
"Cannot create or update route: Referenced freight with ID " + freightId + " not found.");
}
}
return freightDtos;
}
public @Nullable Route getByFreightId(long freightId) {
return repo.getByFreightId(freightId);
}
@@ -43,7 +85,16 @@ public final class RouteService {
return repo.getByVehicleId(vehicleId);
}
public List<Route> getByStatus(Route.Status status) {
return repo.getAll().stream()
.filter(r -> r.status() == status)
.toList();
}
public Route update(long id, Route newRoute) {
VehicleDto vehicleDto = validateAndGetVehicle(newRoute.vehicleId());
List<FreightDto> freightDtos = validateAndGetFreights(newRoute.freightId());
newRoute = new Route(id,
newRoute.vehicleId(),
newRoute.freightId(),

View File

@@ -2,6 +2,7 @@ package ua.com.dxrkness.service;
import org.jspecify.annotations.Nullable;
import org.springframework.stereotype.Service;
import ua.com.dxrkness.exception.VehicleNotFoundException;
import ua.com.dxrkness.model.Vehicle;
import ua.com.dxrkness.repository.VehicleRepository;
@@ -19,13 +20,23 @@ public final class VehicleService {
return repo.getAll();
}
public List<Vehicle> getByStatus(Vehicle.Status status) {
return repo.getAll().stream()
.filter(v -> v.status() == status)
.toList();
}
public Vehicle add(Vehicle veh) {
int id = repo.add(veh);
return new Vehicle(id, veh.brand(), veh.model(), veh.licensePlate(), veh.year(), veh.capacityKg(), veh.status());
}
public @Nullable Vehicle getById(long id) {
return repo.getById(id);
public Vehicle getById(long id) {
Vehicle vehicle = repo.getById(id);
if (vehicle == null) {
throw new VehicleNotFoundException("Vehicle with ID " + id + " not found.");
}
return vehicle;
}
public Vehicle update(long id, Vehicle newVehicle) {

View File

@@ -0,0 +1,243 @@
package ua.com.dxrkness.integration;
import org.jspecify.annotations.NullMarked;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.web.context.WebApplicationContext;
import ua.com.dxrkness.model.Route;
import ua.com.dxrkness.service.PopulateService;
import java.util.List;
import java.util.stream.Stream;
import static org.assertj.core.api.BDDAssertions.then;
/**
* Integration tests for inter-service communication.
*
* These tests verify that:
* 1. RouteService validates vehicle existence by calling VehicleService
* 2. RouteService validates freight existence by calling FreightService
* 3. Services use DTOs for data transfer between service boundaries
* 4. Proper error handling when referenced entities don't exist
*/
@SpringBootTest
@NullMarked
class InterServiceCommunicationTest {
private static final int AMOUNT = 100;
@Autowired
private PopulateService populateService;
private RestTestClient routesClient;
private RestTestClient vehiclesClient;
private RestTestClient freightsClient;
@BeforeEach
void setUp(WebApplicationContext context) {
populateService.populate(AMOUNT);
routesClient = RestTestClient.bindToApplicationContext(context).baseUrl("/routes").build();
vehiclesClient = RestTestClient.bindToApplicationContext(context).baseUrl("/vehicles").build();
freightsClient = RestTestClient.bindToApplicationContext(context).baseUrl("/freights").build();
}
@AfterEach
void teardown() {
populateService.clear();
}
private static Stream<MediaType> mediaTypes() {
return Stream.of(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML);
}
// ===== INTER-SERVICE VALIDATION TESTS =====
@ParameterizedTest
@MethodSource("mediaTypes")
void createRoute_WithValidVehicleAndFreights_Succeeds(MediaType mediaType) {
// given - a route with valid vehicle and freight references
final var validVehicleId = 1;
final var validFreightIds = List.of(1L, 2L);
final var newRoute = new Route(0, validVehicleId, validFreightIds,
"Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
// when - creating the route
// then - should succeed because vehicle and freights exist (validated through inter-service communication)
routesClient.post()
.accept(mediaType)
.contentType(mediaType)
.body(newRoute)
.exchange()
.expectStatus()
.isOk();
}
@ParameterizedTest
@MethodSource("mediaTypes")
void createRoute_WithInvalidVehicleId_Returns404(MediaType mediaType) {
// given - a route with an invalid (non-existent) vehicle ID
final var invalidVehicleId = -1;
final var validFreightIds = List.of(1L);
final var newRoute = new Route(0, invalidVehicleId, validFreightIds,
"Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
// when - creating the route
// then - should fail with 404 because vehicle doesn't exist (inter-service validation)
routesClient.post()
.accept(mediaType)
.contentType(mediaType)
.body(newRoute)
.exchange()
.expectStatus()
.isNotFound()
.expectBody(String.class)
.consumeWith(res -> {
String body = res.getResponseBody();
then(body).isNotNull();
then(body).contains("Not Found").contains("vehicle");
});
}
@ParameterizedTest
@MethodSource("mediaTypes")
void createRoute_WithInvalidFreightId_Returns404(MediaType mediaType) {
// given - a route with a valid vehicle but invalid freight ID
final var validVehicleId = 1;
final var invalidFreightIds = List.of(-1L); // Invalid freight ID
final var newRoute = new Route(0, validVehicleId, invalidFreightIds,
"Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
// when - creating the route
// then - should fail with 404 because freight doesn't exist (inter-service validation)
routesClient.post()
.accept(mediaType)
.contentType(mediaType)
.body(newRoute)
.exchange()
.expectStatus()
.isNotFound()
.expectBody(String.class)
.consumeWith(res -> {
String body = res.getResponseBody();
then(body).isNotNull();
then(body).contains("Not Found").contains("freight");
});
}
@ParameterizedTest
@MethodSource("mediaTypes")
void updateRoute_ValidatesReferences_OnInterServiceCall(MediaType mediaType) {
// given - an existing route and new valid references
final var routeId = 1;
final var validVehicleId = 2;
final var validFreightIds = List.of(3L, 4L);
final var updatedRoute = new Route(routeId, validVehicleId, validFreightIds,
"Odesa", "Kharkiv", 700.0, 10.0, Route.Status.IN_PROGRESS);
// when - updating the route
// then - should succeed because vehicle and freights exist (inter-service validation)
routesClient.put().uri("/{id}", routeId)
.accept(mediaType)
.contentType(mediaType)
.body(updatedRoute)
.exchange()
.expectStatus()
.isOk();
}
@ParameterizedTest
@MethodSource("mediaTypes")
void updateRoute_WithInvalidVehicleOnUpdate_Returns404(MediaType mediaType) {
// given - an existing route and new references with invalid vehicle
final var routeId = 1;
final var invalidVehicleId = -999;
final var validFreightIds = List.of(3L);
final var updatedRoute = new Route(routeId, invalidVehicleId, validFreightIds,
"Odesa", "Kharkiv", 700.0, 10.0, Route.Status.IN_PROGRESS);
// when - updating the route
// then - should fail with 404 (inter-service validation on update)
routesClient.put().uri("/{id}", routeId)
.accept(mediaType)
.contentType(mediaType)
.body(updatedRoute)
.exchange()
.expectStatus()
.isNotFound();
}
// ===== VEHICLE INTER-SERVICE TESTS =====
@ParameterizedTest
@MethodSource("mediaTypes")
void getVehicle_UsedInRoute_ReturnsVehicleData(MediaType mediaType) {
// given - a vehicle that is used in routes
final var vehicleId = 1;
// when - retrieving the vehicle
// then - should return vehicle data (VehicleService provides data to other services)
vehiclesClient.get().uri("/{id}", vehicleId)
.accept(mediaType)
.exchange()
.expectStatus()
.isOk();
}
// ===== FREIGHT INTER-SERVICE TESTS =====
@ParameterizedTest
@MethodSource("mediaTypes")
void getFreight_UsedInRoute_ReturnsFreightData(MediaType mediaType) {
// given - a freight that is used in routes
final var freightId = 1;
// when - retrieving the freight
// then - should return freight data (FreightService provides data to other services)
freightsClient.get().uri("/{id}", freightId)
.accept(mediaType)
.exchange()
.expectStatus()
.isOk();
}
// ===== DTO USAGE TESTS =====
@ParameterizedTest
@MethodSource("mediaTypes")
void routeSubResource_UsesFreightDtos_FromFreightService(MediaType mediaType) {
// given - a route with associated freights
final var routeId = 1;
// when - retrieving freights for a route (sub-resource endpoint)
// then - should return freights as DTOs from inter-service call
routesClient.get().uri("/{id}/freights", routeId)
.accept(mediaType)
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.consumeWith(res -> then(res.getResponseBody()).isNotNull());
}
@ParameterizedTest
@MethodSource("mediaTypes")
void vehicleSubResource_UsesRouteDtos_FromRouteService(MediaType mediaType) {
// given - a vehicle with associated routes
final var vehicleId = 1;
// when - retrieving routes for a vehicle (sub-resource endpoint)
// then - should return routes as DTOs from inter-service call
vehiclesClient.get().uri("/{id}/routes", vehicleId)
.accept(mediaType)
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.consumeWith(res -> then(res.getResponseBody()).isNotNull());
}
}

View File

@@ -0,0 +1,185 @@
package ua.com.dxrkness.integration;
import org.jspecify.annotations.NullMarked;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.web.context.WebApplicationContext;
import ua.com.dxrkness.model.Freight;
import ua.com.dxrkness.model.Route;
import ua.com.dxrkness.service.PopulateService;
import java.util.List;
import java.util.stream.Stream;
import static org.assertj.core.api.BDDAssertions.then;
@SpringBootTest
@NullMarked
class SubResourcesAndFilteringTest {
private static final int AMOUNT = 100;
@Autowired
private PopulateService populateService;
private RestTestClient vehiclesClient;
private RestTestClient routesClient;
private RestTestClient freightsClient;
@BeforeEach
void setUp(WebApplicationContext context) {
populateService.populate(AMOUNT);
vehiclesClient = RestTestClient.bindToApplicationContext(context).baseUrl("/vehicles").build();
routesClient = RestTestClient.bindToApplicationContext(context).baseUrl("/routes").build();
freightsClient = RestTestClient.bindToApplicationContext(context).baseUrl("/freights").build();
}
@AfterEach
void teardown() {
populateService.clear();
}
private static Stream<MediaType> mediaTypes() {
return Stream.of(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML);
}
// ===== SUB-RESOURCE TESTS =====
@ParameterizedTest
@MethodSource("mediaTypes")
void getRoutesForVehicle_ReturnsRoutes(MediaType mediaType) {
// given
final var vehicleId = 1;
// when
// then
vehiclesClient.get().uri("/{id}/routes", vehicleId)
.accept(mediaType)
.exchange()
.expectStatus()
.isOk()
.expectBody(new ParameterizedTypeReference<List<Route>>() {
})
.consumeWith(res -> then(res.getResponseBody()).isNotNull());
}
@ParameterizedTest
@MethodSource("mediaTypes")
void getRoutesForVehicle_NonExistentVehicle_Returns404(MediaType mediaType) {
// given
final var vehicleId = -1;
// when
// then
vehiclesClient.get().uri("/{id}/routes", vehicleId)
.accept(mediaType)
.exchange()
.expectStatus()
.isNotFound();
}
@ParameterizedTest
@MethodSource("mediaTypes")
void getFreightsForRoute_ReturnsFreights(MediaType mediaType) {
// given
final var routeId = 1;
// when
// then
routesClient.get().uri("/{id}/freights", routeId)
.accept(mediaType)
.exchange()
.expectStatus()
.isOk()
.expectBody(new ParameterizedTypeReference<List<Freight>>() {
})
.consumeWith(res -> then(res.getResponseBody()).isNotNull());
}
@ParameterizedTest
@MethodSource("mediaTypes")
void getFreightsForRoute_NonExistentRoute_Returns404(MediaType mediaType) {
// given
final var routeId = -1;
// when
// then
routesClient.get().uri("/{id}/freights", routeId)
.accept(mediaType)
.exchange()
.expectStatus()
.isNotFound();
}
// ===== ERROR HANDLING TESTS =====
@ParameterizedTest
@MethodSource("mediaTypes")
void getById_VehicleNotFound_Returns404WithErrorResponse(MediaType mediaType) {
// given
final var vehicleId = -1;
// when
// then
vehiclesClient.get().uri("/{id}", vehicleId)
.accept(mediaType)
.exchange()
.expectStatus()
.isNotFound()
.expectBody(String.class)
.consumeWith(res -> {
String body = res.getResponseBody();
then(body).isNotNull();
then(body).contains("Not Found");
});
}
@ParameterizedTest
@MethodSource("mediaTypes")
void getById_RouteNotFound_Returns404WithErrorResponse(MediaType mediaType) {
// given
final var routeId = -1;
// when
// then
routesClient.get().uri("/{id}", routeId)
.accept(mediaType)
.exchange()
.expectStatus()
.isNotFound()
.expectBody(String.class)
.consumeWith(res -> {
String body = res.getResponseBody();
then(body).isNotNull();
then(body).contains("Not Found");
});
}
@ParameterizedTest
@MethodSource("mediaTypes")
void getById_FreightNotFound_Returns404WithErrorResponse(MediaType mediaType) {
// given
final var freightId = -1;
// when
// then
freightsClient.get().uri("/{id}", freightId)
.accept(mediaType)
.exchange()
.expectStatus()
.isNotFound()
.expectBody(String.class)
.consumeWith(res -> {
String body = res.getResponseBody();
then(body).isNotNull();
then(body).contains("Not Found");
});
}
}