now finish 2nd practical for real.

This commit is contained in:
2025-12-17 04:40:20 +02:00
parent cfc6f170ad
commit 1c162907fd
13 changed files with 267 additions and 407 deletions
@@ -5,6 +5,8 @@ import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils; import org.apache.http.util.EntityUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.ObjectMapper;
import tools.jackson.dataformat.xml.XmlMapper; import tools.jackson.dataformat.xml.XmlMapper;
import ua.com.dxrkness.model.Freight; import ua.com.dxrkness.model.Freight;
@@ -29,7 +31,7 @@ public class ApplicationClient {
testInterServiceValidation(client); testInterServiceValidation(client);
} catch (Exception e) { } catch (Exception e) {
System.err.println("error: " + e.getMessage()); System.out.println("error: " + e.getMessage());
e.printStackTrace(); e.printStackTrace();
} }
} }
@@ -53,18 +55,12 @@ public class ApplicationClient {
execute(client, new HttpGet(BASE_URL + "/vehicles/999"), "GET vehicle by ID=999 (not found)"); execute(client, new HttpGet(BASE_URL + "/vehicles/999"), "GET vehicle by ID=999 (not found)");
Vehicle updateVehicle = new Vehicle(1, "Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.MAINTENANCE); Vehicle updateVehicle = new Vehicle(1, "Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.MAINTENANCE);
String updateJson = jsonMapper.writeValueAsString(updateVehicle); var vehiclePut = createHttpRequest(updateVehicle, "/vehicles/0", HttpMethod.PUT);
HttpPut put = new HttpPut(BASE_URL + "/vehicles/0"); execute(client, vehiclePut, "PUT update vehicle ID=0");
put.setEntity(new StringEntity(updateJson, "UTF-8"));
put.setHeader("Content-Type", "application/json");
execute(client, put, "PUT update vehicle ID=0");
Vehicle patchVehicle = new Vehicle(1, "Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.IN_TRANSIT); Vehicle patchVehicle = new Vehicle(1, "Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.IN_TRANSIT);
String patchJson = jsonMapper.writeValueAsString(patchVehicle); var vehiclePatch = createHttpRequest(patchVehicle, "/vehicles/0", HttpMethod.PATCH);
HttpPatch patch = new HttpPatch(BASE_URL + "/vehicles/0"); execute(client, vehiclePatch, "PATCH update vehicle ID=0");
patch.setEntity(new StringEntity(patchJson, "UTF-8"));
patch.setHeader("Content-Type", "application/json");
execute(client, patch, "PATCH update vehicle ID=0");
execute(client, new HttpDelete(BASE_URL + "/vehicles/999"), "DELETE vehicle ID=999 (not found)"); execute(client, new HttpDelete(BASE_URL + "/vehicles/999"), "DELETE vehicle ID=999 (not found)");
@@ -78,28 +74,19 @@ public class ApplicationClient {
Freight.Dimensions dims = new Freight.Dimensions(120, 100, 200); Freight.Dimensions dims = new Freight.Dimensions(120, 100, 200);
Freight newFreight = new Freight(0, "Electronics", "Laptops and monitors", 500, dims, Freight.Status.PENDING); Freight newFreight = new Freight(0, "Electronics", "Laptops and monitors", 500, dims, Freight.Status.PENDING);
String freightJson = jsonMapper.writeValueAsString(newFreight); var freightPost = createHttpRequest(newFreight, "/freights", HttpMethod.POST);
HttpPost post = new HttpPost(BASE_URL + "/freights"); execute(client, freightPost, "POST new freight");
post.setEntity(new StringEntity(freightJson, "UTF-8"));
post.setHeader("Content-Type", "application/json");
execute(client, post, "POST new freight");
execute(client, new HttpGet(BASE_URL + "/freights/0"), "GET freight by ID=1"); execute(client, new HttpGet(BASE_URL + "/freights/0"), "GET freight by ID=0");
execute(client, new HttpGet(BASE_URL + "/freights/999"), "GET freight by ID=999 (not found)"); execute(client, new HttpGet(BASE_URL + "/freights/999"), "GET freight by ID=999 (not found)");
Freight updateFreight = new Freight(1, "Furniture", "Office desks", 800, dims, Freight.Status.IN_TRANSIT); Freight updateFreight = new Freight(1, "Furniture", "Office desks", 800, dims, Freight.Status.IN_TRANSIT);
String updateJson = jsonMapper.writeValueAsString(updateFreight); var freightPut = createHttpRequest(updateFreight, "/freights/0", HttpMethod.PUT);
HttpPut put = new HttpPut(BASE_URL + "/freights/0"); execute(client, freightPut, "PUT update freight ID=0");
put.setEntity(new StringEntity(updateJson, "UTF-8"));
put.setHeader("Content-Type", "application/json");
execute(client, put, "PUT update freight ID=1");
Freight patchFreight = new Freight(1, "Furniture", "Office desks", 800, dims, Freight.Status.DELIVERED); Freight patchFreight = new Freight(1, "Furniture", "Office desks", 800, dims, Freight.Status.DELIVERED);
String patchJson = jsonMapper.writeValueAsString(patchFreight); var freightPatch = createHttpRequest(patchFreight, "/freights/0", HttpMethod.PATCH);
HttpPatch patch = new HttpPatch(BASE_URL + "/freights/0"); execute(client, freightPatch, "PATCH update freight ID=0");
patch.setEntity(new StringEntity(patchJson, "UTF-8"));
patch.setHeader("Content-Type", "application/json");
execute(client, patch, "PATCH update freight ID=1");
execute(client, new HttpDelete(BASE_URL + "/freights/999"), "DELETE freight ID=999 (not found)"); execute(client, new HttpDelete(BASE_URL + "/freights/999"), "DELETE freight ID=999 (not found)");
@@ -109,34 +96,22 @@ public class ApplicationClient {
private static void testRoutes(CloseableHttpClient client) throws Exception { private static void testRoutes(CloseableHttpClient client) throws Exception {
System.out.println("--- ROUTES ---"); System.out.println("--- ROUTES ---");
Route newRoute = new Route(0, 0, List.of(0L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
var routePost = createHttpRequest(newRoute, "/routes", HttpMethod.POST);
execute(client, routePost, "POST new route");
execute(client, new HttpGet(BASE_URL + "/routes"), "GET all routes"); execute(client, new HttpGet(BASE_URL + "/routes"), "GET all routes");
execute(client, new HttpGet(BASE_URL + "/routes?vehicleId=0"), "GET routes by vehicleId=1");
execute(client, new HttpGet(BASE_URL + "/routes?freightId=0"), "GET routes by freightId=1");
Route newRoute = new Route(0, 0, List.of(0L, 1L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
String routeJson = jsonMapper.writeValueAsString(newRoute);
HttpPost post = new HttpPost(BASE_URL + "/routes");
post.setEntity(new StringEntity(routeJson, "UTF-8"));
post.setHeader("Content-Type", "application/json");
execute(client, post, "POST new route");
execute(client, new HttpGet(BASE_URL + "/routes/0"), "GET route by ID=0"); execute(client, new HttpGet(BASE_URL + "/routes/0"), "GET route by ID=0");
execute(client, new HttpGet(BASE_URL + "/routes/999"), "GET route by ID=999 (not found)"); execute(client, new HttpGet(BASE_URL + "/routes/999"), "GET route by ID=999 (not found)");
Route updateRoute = new Route(1, 1, List.of(1L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.IN_PROGRESS); Route updateRoute = new Route(0, 0, List.of(0L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.IN_PROGRESS);
String updateJson = jsonMapper.writeValueAsString(updateRoute); var routePut = createHttpRequest(updateRoute, "/routes/0", HttpMethod.PUT);
HttpPut put = new HttpPut(BASE_URL + "/routes/0"); execute(client, routePut, "PUT update route ID=0");
put.setEntity(new StringEntity(updateJson, "UTF-8"));
put.setHeader("Content-Type", "application/json");
execute(client, put, "PUT update route ID=0");
Route patchRoute = new Route(1, 1, List.of(1L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.COMPLETED); Route patchRoute = new Route(0, 0, List.of(0L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.COMPLETED);
String patchJson = jsonMapper.writeValueAsString(patchRoute); var routePatch = createHttpRequest(patchRoute, "/routes/0", HttpMethod.PATCH);
HttpPatch patch = new HttpPatch(BASE_URL + "/routes/0"); execute(client, routePatch, "PATCH update route ID=0");
patch.setEntity(new StringEntity(patchJson, "UTF-8"));
patch.setHeader("Content-Type", "application/json");
execute(client, patch, "PATCH update route ID=0");
execute(client, new HttpDelete(BASE_URL + "/routes/999"), "DELETE route ID=999 (not found)"); execute(client, new HttpDelete(BASE_URL + "/routes/999"), "DELETE route ID=999 (not found)");
@@ -146,9 +121,8 @@ public class ApplicationClient {
private static void testSubResources(CloseableHttpClient client) throws Exception { private static void testSubResources(CloseableHttpClient client) throws Exception {
System.out.println("--- SUB-RESOURCES ---"); 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/0/freights"), "GET freights for route ID=0 (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 + "/routes/0/vehicle"), "GET vehicle for route ID=0 (sub-resource)");
execute(client, new HttpGet(BASE_URL + "/vehicles/999/routes"), "GET routes for vehicle ID=999 (not found)");
System.out.println(); System.out.println();
} }
@@ -178,48 +152,52 @@ public class ApplicationClient {
private static void testInterServiceValidation(CloseableHttpClient client) throws Exception { private static void testInterServiceValidation(CloseableHttpClient client) throws Exception {
System.out.println("--- INTER-SERVICE COMMUNICATION & VALIDATION ---"); 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) // Test 1: Create route with valid references (should succeed)
System.out.println("[INTER-SERVICE TEST 1] Creating route with valid vehicle and freight references:"); 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); Route validRoute = new Route(0, 0, List.of(0L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
String validRouteJson = jsonMapper.writeValueAsString(validRoute); var validRoutePost = createHttpRequest(validRoute, "/routes", HttpMethod.POST);
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)"); execute(client, validRoutePost, "POST route with valid vehicle and freights (should succeed)");
// Test 2: Create route with invalid vehicle (should fail with 404) // Test 2: Create route with invalid vehicle (should fail with 404)
System.out.println("[INTER-SERVICE TEST 2] Creating route with INVALID vehicle ID:"); 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); Route invalidVehicleRoute = new Route(0, -999, List.of(1L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
String invalidVehicleJson = jsonMapper.writeValueAsString(invalidVehicleRoute); var invalidRoutePost = createHttpRequest(invalidVehicleRoute, "/routes", HttpMethod.POST);
HttpPost invalidVehiclePost = new HttpPost(BASE_URL + "/routes"); execute(client, invalidRoutePost, "POST route with invalid vehicle ID, should result in 404");
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) // Test 3: Create route with invalid freight (should fail with 404)
System.out.println("[INTER-SERVICE TEST 3] Creating route with INVALID freight ID:"); 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); Route invalidFreightRoute = new Route(0, 0, List.of(-999L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
String invalidFreightJson = jsonMapper.writeValueAsString(invalidFreightRoute); var invalidFreightPost = createHttpRequest(invalidFreightRoute, "/routes", HttpMethod.POST);
HttpPost invalidFreightPost = new HttpPost(BASE_URL + "/routes"); execute(client, invalidFreightPost, "POST route with invalid freight ID, should result in 404");
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(); private static <T> HttpUriRequest createHttpRequest(T entity, String path, HttpMethod method) {
System.out.println("Summary: The above tests demonstrate that:"); String entityJson = jsonMapper.writeValueAsString(entity);
System.out.println("✓ RouteService uses inter-service calls to VehicleService and FreightService"); HttpEntityEnclosingRequestBase req;
System.out.println("✓ DTOs are used for data transfer across service boundaries");
System.out.println("✓ Validation happens before route is created"); if (method == HttpMethod.GET) {
System.out.println("✓ Proper error codes (404) are returned for validation failures"); return new HttpGet(BASE_URL + path);
System.out.println(); } else if (method == HttpMethod.POST) {
req = new HttpPost(BASE_URL + path);
req.setEntity(new StringEntity(entityJson, "UTF-8"));
req.setHeader("Content-Type", "application/json");
return req;
} else if (method == HttpMethod.PUT) {
req = new HttpPut(BASE_URL + path);
req.setEntity(new StringEntity(entityJson, "UTF-8"));
req.setHeader("Content-Type", "application/json");
return req;
} else if (method == HttpMethod.PATCH) {
req = new HttpPatch(BASE_URL + path);
req.setEntity(new StringEntity(entityJson, "UTF-8"));
req.setHeader("Content-Type", "application/json");
return req;
} else if (method == HttpMethod.DELETE) {
return new HttpDelete(BASE_URL + path);
} else {
throw new IllegalArgumentException("method is invalid");
}
} }
private static String execute(CloseableHttpClient client, HttpUriRequest request, String description) { private static String execute(CloseableHttpClient client, HttpUriRequest request, String description) {
@@ -234,14 +212,14 @@ public class ApplicationClient {
System.out.println("Response: " + body); System.out.println("Response: " + body);
} }
if (statusCode >= 400) { if (HttpStatus.valueOf(statusCode).isError()) {
System.err.println("ERROR: Request failed with status " + statusCode); System.out.println("ERROR: Request failed with status " + statusCode);
} }
System.out.println(); System.out.println();
return body; return body;
} catch (Exception e) { } catch (Exception e) {
System.err.println("EXCEPTION: " + e.getMessage()); System.out.println("EXCEPTION: " + e.getMessage());
e.printStackTrace(); e.printStackTrace();
System.out.println(); System.out.println();
return null; return null;
@@ -2,11 +2,8 @@ package ua.com.dxrkness.controller;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; 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.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -23,7 +20,6 @@ import java.util.List;
consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE} consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}
) )
@Tag(name = "Freight", description = "Freights management") @Tag(name = "Freight", description = "Freights management")
@NullMarked
public class FreightController { public class FreightController {
private final FreightService service; private final FreightService service;
@@ -37,11 +33,7 @@ public class FreightController {
) )
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "Successfully retrieved list of freights (may be empty!)", description = "Successfully retrieved list of freights (may be empty!)"
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Freight.class)
)
) )
@GetMapping(consumes = MediaType.ALL_VALUE) @GetMapping(consumes = MediaType.ALL_VALUE)
public List<Freight> getAll( public List<Freight> getAll(
@@ -59,42 +51,32 @@ public class FreightController {
) )
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "Successfully retrieved freight", description = "Successfully retrieved freight"
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Freight.class)
)
) )
@ApiResponse( @ApiResponse(
responseCode = "404", responseCode = "404",
description = "Freight not found" description = "Freight not found"
) )
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE) @GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
public ResponseEntity<Freight> getById(@PathVariable("id") long id) { public Freight getById(@PathVariable("id") long id) {
return ResponseEntity.ok(service.getById(id)); return service.getById(id);
} }
@Operation( @Operation(
summary = "Create a new freight", summary = "Create a new freight",
description = "Adds a new freight. The FreightService can validate freight properties " + description = "Adds a new freight"
"against vehicle capacity through inter-service communication with VehicleService. " +
"This demonstrates the service layer design pattern for business logic validation."
) )
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "Freight successfully created", description = "Freight successfully created"
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Freight.class)
)
) )
@ApiResponse( @ApiResponse(
responseCode = "400", responseCode = "400",
description = "Invalid freight data (e.g., weight exceeds vehicle capacity) - inter-service validation failure" description = "Invalid freight data (e.g., weight exceeds vehicle capacity)"
) )
@PostMapping @PostMapping
public ResponseEntity<Freight> add(@RequestBody Freight newFreight) { public Freight add(@RequestBody Freight newFreight) {
return ResponseEntity.ok(service.add(newFreight)); return service.add(newFreight);
} }
@Operation( @Operation(
@@ -103,19 +85,15 @@ public class FreightController {
) )
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "Freight successfully updated", description = "Freight successfully updated"
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Freight.class)
)
) )
@ApiResponse( @ApiResponse(
responseCode = "400", responseCode = "400",
description = "Invalid input" description = "Invalid input"
) )
@PutMapping("/{id}") @PutMapping("/{id}")
public ResponseEntity<Freight> update(@PathVariable("id") long id, @RequestBody Freight newFreight) { public Freight update(@PathVariable("id") long id, @RequestBody Freight newFreight) {
return ResponseEntity.ok(service.update(id, newFreight)); return service.update(id, newFreight);
} }
@Operation( @Operation(
@@ -124,19 +102,15 @@ public class FreightController {
) )
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "Freight successfully updated", description = "Freight successfully updated"
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Freight.class)
)
) )
@ApiResponse( @ApiResponse(
responseCode = "400", responseCode = "400",
description = "Invalid input" description = "Invalid input"
) )
@PatchMapping("/{id}") @PatchMapping("/{id}")
public ResponseEntity<Freight> updatePatch(@PathVariable("id") long id, @RequestBody Freight newFreight) { public Freight updatePatch(@PathVariable("id") long id, @RequestBody Freight newFreight) {
return ResponseEntity.ok(service.update(id, newFreight)); return service.update(id, newFreight);
} }
@Operation( @Operation(
@@ -145,18 +119,14 @@ public class FreightController {
) )
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "Freight successfully deleted", description = "Freight successfully deleted"
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Freight.class)
)
) )
@ApiResponse( @ApiResponse(
responseCode = "404", responseCode = "404",
description = "Freight not found" description = "Freight not found"
) )
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE) @DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
public ResponseEntity<Freight> delete(@PathVariable("id") long id) { public Freight delete(@PathVariable("id") long id) {
return ResponseEntity.ofNullable(service.delete(id)); return service.delete(id);
} }
} }
@@ -19,7 +19,7 @@ public class GlobalExceptionHandler {
FreightNotFoundException.class, FreightNotFoundException.class,
RouteNotFoundException.class RouteNotFoundException.class
}) })
public ErrorResponse handleVehicleNotFoundException(VehicleNotFoundException ex) { public ErrorResponse handleVehicleNotFoundException(Exception ex) {
return constructExceptionBody(HttpStatus.NOT_FOUND, ex); return constructExceptionBody(HttpStatus.NOT_FOUND, ex);
} }
@@ -2,18 +2,20 @@ package ua.com.dxrkness.controller;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; 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.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import ua.com.dxrkness.model.Freight;
import ua.com.dxrkness.model.Route; import ua.com.dxrkness.model.Route;
import ua.com.dxrkness.model.Vehicle;
import ua.com.dxrkness.service.FreightService;
import ua.com.dxrkness.service.RouteService; import ua.com.dxrkness.service.RouteService;
import ua.com.dxrkness.service.VehicleService;
import java.util.ArrayList;
import java.util.List; import java.util.List;
@RestController @RestController
@@ -25,10 +27,16 @@ import java.util.List;
@Tag(name = "Route", description = "Routes management") @Tag(name = "Route", description = "Routes management")
@NullMarked @NullMarked
public class RouteController { public class RouteController {
private final RouteService service; private final RouteService routeService;
private final VehicleService vehicleService;
private final FreightService freightService;
public RouteController(RouteService service) { public RouteController(RouteService routeService,
this.service = service; VehicleService vehicleService,
FreightService freightService) {
this.routeService = routeService;
this.vehicleService = vehicleService;
this.freightService = freightService;
} }
@Operation( @Operation(
@@ -37,10 +45,7 @@ public class RouteController {
) )
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "Successfully retrieved routes", description = "Successfully retrieved routes"
content = @Content(
schema = @Schema(implementation = Route.class)
)
) )
@GetMapping(consumes = MediaType.ALL_VALUE) @GetMapping(consumes = MediaType.ALL_VALUE)
public List<Route> getAll( public List<Route> getAll(
@@ -51,111 +56,19 @@ public class RouteController {
@Parameter(description = "Filter routes by status (PLANNED, IN_PROGRESS, COMPLETED, CANCELLED)") @Parameter(description = "Filter routes by status (PLANNED, IN_PROGRESS, COMPLETED, CANCELLED)")
@RequestParam(name = "status", required = false) Route.@Nullable Status status) { @RequestParam(name = "status", required = false) Route.@Nullable Status status) {
if (status != null) { if (status != null) {
return service.getByStatus(status); return routeService.getByStatus(status);
} }
if (vehicleId != null) { if (vehicleId != null) {
return service.getByVehicleId(vehicleId); return routeService.getByVehicleId(vehicleId);
} }
if (freightId != null) { if (freightId != null) {
var route = service.getByFreightId(freightId); var route = routeService.getByFreightId(freightId);
if (route == null) { if (route == null) {
return List.of(); return List.of();
} }
return List.of(route); return List.of(route);
} }
return service.getAll(); return routeService.getAll();
}
@Operation(
summary = "Create new route",
description = "Add new route"
)
@ApiResponse(
responseCode = "200",
description = "Route successfully created",
content = @Content(
schema = @Schema(implementation = Route.class)
)
)
@ApiResponse(
responseCode = "400",
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. Must include valid vehicleId and freightId list.",
required = true,
content = @Content(
schema = @Schema(implementation = Route.class)
)
)
@RequestBody Route newRoute) {
return ResponseEntity.ok(service.add(newRoute));
}
@Operation(
summary = "Update a route (full)",
description = "Update all fields of an existing route by ID"
)
@ApiResponse(
responseCode = "200",
description = "Route successfully updated",
content = @Content(
schema = @Schema(implementation = Route.class)
)
)
@ApiResponse(
responseCode = "400",
description = "Invalid route data provided"
)
@PutMapping("/{id}")
public ResponseEntity<Route> update(
@Parameter(description = "ID of the route to update", required = true)
@PathVariable("id") long id,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Updated route object",
required = true,
content = @Content(
schema = @Schema(implementation = Route.class)
)
)
@RequestBody Route newRoute) {
return ResponseEntity.ok(service.update(id, newRoute));
}
@Operation(
summary = "Update a route (partial)",
description = "Update specific fields of an existing route by ID"
)
@ApiResponse(
responseCode = "200",
description = "Route successfully updated",
content = @Content(
schema = @Schema(implementation = Route.class)
)
)
@ApiResponse(
responseCode = "400",
description = "Invalid route data provided"
)
@PatchMapping("/{id}")
public ResponseEntity<Route> updatePatch(
@Parameter(description = "ID of the route to update", required = true)
@PathVariable("id") long id,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Route object with fields to update",
required = true,
content = @Content(
schema = @Schema(implementation = Route.class)
)
)
@RequestBody Route newRoute) {
return ResponseEntity.ok(service.update(id, newRoute));
} }
@Operation( @Operation(
@@ -164,40 +77,151 @@ public class RouteController {
) )
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "Route found", description = "Route found"
content = @Content(
schema = @Schema(implementation = Route.class)
)
) )
@ApiResponse( @ApiResponse(
responseCode = "404", responseCode = "404",
description = "Route not found" description = "Route not found"
) )
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE) @GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
public ResponseEntity<Route> getById( public Route getById(
@Parameter(description = "ID of the route to retrieve", required = true) @Parameter(description = "ID of the route to retrieve", required = true)
@PathVariable("id") long id) { @PathVariable("id") long id) {
return ResponseEntity.ofNullable(service.getById(id)); return routeService.getById(id);
} }
@Operation(
summary = "Retrieve a vehicle by route ID"
)
@ApiResponse(
responseCode = "200",
description = "Route and vehicle found"
)
@ApiResponse(
responseCode = "404",
description = "Route or vehicle not found"
)
@GetMapping(value = "/{id}/vehicle", consumes = MediaType.ALL_VALUE)
public Vehicle getVehicleById(
@Parameter(description = "ID of the route to retrieve", required = true)
@PathVariable("id") long id) {
return vehicleService.getById(routeService.getById(id).vehicleId());
}
@Operation(
summary = "Retrieve freights by route ID"
)
@ApiResponse(
responseCode = "200",
description = "Route and freights found"
)
@ApiResponse(
responseCode = "404",
description = "Route or freight not found"
)
@GetMapping(value = "/{id}/freights", consumes = MediaType.ALL_VALUE)
public List<Freight> getFreightsById(
@Parameter(description = "ID of the route to retrieve", required = true)
@PathVariable("id") long id) {
var route = routeService.getById(id);
var freights = new ArrayList<Freight>();
for (var freightId : route.freightId()) {
freights.add(freightService.getById(freightId));
}
return freights;
}
@Operation(
summary = "Create new route",
description = "Add new route"
)
@ApiResponse(
responseCode = "200",
description = "Route successfully created"
)
@ApiResponse(
responseCode = "400",
description = "Invalid route data: vehicle or freight not found"
)
@ApiResponse(
responseCode = "404",
description = "Referenced vehicle or freight not found"
)
@PostMapping
public Route add(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Route object to be created. Must include valid vehicleId and freightId list.",
required = true
)
@RequestBody Route newRoute) {
return routeService.add(newRoute);
}
@Operation(
summary = "Update a route (full)",
description = "Update all fields of an existing route by ID"
)
@ApiResponse(
responseCode = "200",
description = "Route successfully updated"
)
@ApiResponse(
responseCode = "400",
description = "Invalid route data provided"
)
@PutMapping("/{id}")
public Route update(
@Parameter(description = "ID of the route to update", required = true)
@PathVariable("id") long id,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Updated route object",
required = true
)
@RequestBody Route newRoute) {
return routeService.update(id, newRoute);
}
@Operation(
summary = "Update a route (partial)",
description = "Update specific fields of an existing route by ID"
)
@ApiResponse(
responseCode = "200",
description = "Route successfully updated"
)
@ApiResponse(
responseCode = "400",
description = "Invalid route data provided"
)
@PatchMapping("/{id}")
public Route updatePatch(
@Parameter(description = "ID of the route to update", required = true)
@PathVariable("id") long id,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Route object with fields to update",
required = true
)
@RequestBody Route newRoute) {
return routeService.update(id, newRoute);
}
@Operation( @Operation(
summary = "Delete a route", summary = "Delete a route",
description = "Delete a route by ID" description = "Delete a route by ID"
) )
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "Route successfully deleted", description = "Route successfully deleted"
content = @Content(schema = @Schema(implementation = Route.class)
)
) )
@ApiResponse( @ApiResponse(
responseCode = "404", responseCode = "404",
description = "Route not found" description = "Route not found"
) )
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE) @DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
public ResponseEntity<Route> delete( public Route delete(
@Parameter(description = "ID of the route to delete", required = true) @Parameter(description = "ID of the route to delete", required = true)
@PathVariable("id") long id) { @PathVariable("id") long id) {
return ResponseEntity.ofNullable(service.delete(id)); return routeService.delete(id);
} }
} }
@@ -2,14 +2,11 @@ package ua.com.dxrkness.controller;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; 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.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import ua.com.dxrkness.model.Vehicle; import ua.com.dxrkness.model.Vehicle;
import ua.com.dxrkness.service.VehicleService; import ua.com.dxrkness.service.VehicleService;
@@ -35,8 +32,7 @@ public class VehicleController {
summary = "Retrieve all vehicles", summary = "Retrieve all vehicles",
description = "Get a list of all vehicles or filter by status" description = "Get a list of all vehicles or filter by status"
) )
@ApiResponse(responseCode = "200", description = "Successfully retrieved list of vehicles", @ApiResponse(responseCode = "200", description = "Successfully retrieved list of vehicles")
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@GetMapping(consumes = MediaType.ALL_VALUE) @GetMapping(consumes = MediaType.ALL_VALUE)
public List<Vehicle> getAll( public List<Vehicle> getAll(
@Parameter(description = "Filter vehicles by status (AVAILABLE, IN_TRANSIT, MAINTENANCE)") @Parameter(description = "Filter vehicles by status (AVAILABLE, IN_TRANSIT, MAINTENANCE)")
@@ -51,69 +47,65 @@ public class VehicleController {
summary = "Retrieve a vehicle by ID", summary = "Retrieve a vehicle by ID",
description = "Get a single vehicle by its ID" description = "Get a single vehicle by its ID"
) )
@ApiResponse(responseCode = "200", description = "Vehicle found", @ApiResponse(responseCode = "200", description = "Vehicle found")
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@ApiResponse(responseCode = "404", description = "Vehicle not found") @ApiResponse(responseCode = "404", description = "Vehicle not found")
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE) @GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
public ResponseEntity<Vehicle> getById( public Vehicle getById(
@Parameter(description = "ID of the vehicle to retrieve", required = true) @Parameter(description = "ID of the vehicle to retrieve", required = true)
@PathVariable("id") long id) { @PathVariable("id") long id) {
return ResponseEntity.ofNullable(service.getById(id)); return service.getById(id);
} }
@Operation( @Operation(
summary = "Create a new vehicle", summary = "Create a new vehicle",
description = "Add a new vehicle to the system" description = "Add a new vehicle to the system"
) )
@ApiResponse(responseCode = "200", description = "Vehicle successfully created", @ApiResponse(responseCode = "200", description = "Vehicle successfully created")
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided") @ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
@PostMapping @PostMapping
public ResponseEntity<Vehicle> add( public Vehicle add(
@io.swagger.v3.oas.annotations.parameters.RequestBody( @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Vehicle object to be created", description = "Vehicle object to be created",
required = true, required = true
content = @Content(schema = @Schema(implementation = Vehicle.class))) )
@RequestBody Vehicle newVehicle) { @RequestBody Vehicle newVehicle) {
return ResponseEntity.ok(service.add(newVehicle)); return service.add(newVehicle);
} }
@Operation( @Operation(
summary = "Update a vehicle (full)", summary = "Update a vehicle (full)",
description = "Update all fields of an existing vehicle by ID" description = "Update all fields of an existing vehicle by ID"
) )
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated", @ApiResponse(responseCode = "200", description = "Vehicle successfully updated")
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided") @ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
@PutMapping("/{id}") @PutMapping("/{id}")
public ResponseEntity<Vehicle> update( public Vehicle update(
@Parameter(description = "ID of the vehicle to update", required = true) @Parameter(description = "ID of the vehicle to update", required = true)
@PathVariable("id") long id, @PathVariable("id") long id,
@io.swagger.v3.oas.annotations.parameters.RequestBody( @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Updated vehicle object", description = "Updated vehicle object",
required = true, required = true
content = @Content(schema = @Schema(implementation = Vehicle.class))) )
@RequestBody Vehicle newVehicle) { @RequestBody Vehicle newVehicle) {
return ResponseEntity.ok(service.update(id, newVehicle)); return service.update(id, newVehicle);
} }
@Operation( @Operation(
summary = "Update a vehicle (partial)", summary = "Update a vehicle (partial)",
description = "Update specific fields of an existing vehicle by ID" description = "Update specific fields of an existing vehicle by ID"
) )
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated", @ApiResponse(responseCode = "200", description = "Vehicle successfully updated")
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided") @ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
@PatchMapping("/{id}") @PatchMapping("/{id}")
public ResponseEntity<Vehicle> updatePatch( public Vehicle updatePatch(
@Parameter(description = "ID of the vehicle to update", required = true) @Parameter(description = "ID of the vehicle to update", required = true)
@PathVariable("id") long id, @PathVariable("id") long id,
@io.swagger.v3.oas.annotations.parameters.RequestBody( @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Vehicle object with fields to update", description = "Vehicle object with fields to update",
required = true, required = true
content = @Content(schema = @Schema(implementation = Vehicle.class))) )
@RequestBody Vehicle newVehicle) { @RequestBody Vehicle newVehicle) {
return ResponseEntity.ok(service.update(id, newVehicle)); return service.update(id, newVehicle);
} }
@@ -121,13 +113,12 @@ public class VehicleController {
summary = "Delete a vehicle", summary = "Delete a vehicle",
description = "Delete a vehicle by its ID" description = "Delete a vehicle by its ID"
) )
@ApiResponse(responseCode = "200", description = "Vehicle successfully deleted", @ApiResponse(responseCode = "200", description = "Vehicle successfully deleted")
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@ApiResponse(responseCode = "404", description = "Vehicle not found") @ApiResponse(responseCode = "404", description = "Vehicle not found")
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE) @DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
public ResponseEntity<Vehicle> delete( public Vehicle delete(
@Parameter(description = "ID of the vehicle to delete", required = true) @Parameter(description = "ID of the vehicle to delete", required = true)
@PathVariable("id") long id) { @PathVariable("id") long id) {
return ResponseEntity.ofNullable(service.delete(id)); return service.delete(id);
} }
} }
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.fasterxml.jackson.databind.annotation.JsonNaming;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Freight(long id, public record Freight(long id,
String name, String name,
String description, String description,
@@ -12,6 +13,7 @@ public record Freight(long id,
Dimensions dimensions, Dimensions dimensions,
Status status) implements Identifiable { Status status) implements Identifiable {
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Dimensions(int widthCm, public record Dimensions(int widthCm,
int heightCm, int heightCm,
int lengthCm) { int lengthCm) {
@@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming;
import java.util.List; import java.util.List;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Route(long id, public record Route(long id,
long vehicleId, long vehicleId,
List<Long> freightId, List<Long> freightId,
@@ -1,10 +1,10 @@
package ua.com.dxrkness.model; package ua.com.dxrkness.model;
import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.fasterxml.jackson.databind.annotation.JsonNaming;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Vehicle(long id, public record Vehicle(long id,
String brand, String brand,
String model, String model,
@@ -44,7 +44,11 @@ public final class FreightService {
return repo.update(id, newFreight); return repo.update(id, newFreight);
} }
public @Nullable Freight delete(long id) { public Freight delete(long id) {
return repo.delete(id); var deleted = repo.delete(id);
if (deleted == null) {
throw new FreightNotFoundException("Freight with ID " + id + " not found.");
}
return deleted;
} }
} }
@@ -106,7 +106,11 @@ public final class RouteService {
return repo.update(id, newRoute); return repo.update(id, newRoute);
} }
public @Nullable Route delete(long id) { public Route delete(long id) {
return repo.delete(id); var deleted = repo.delete(id);
if (deleted == null) {
throw new RouteNotFoundException("Route with ID " + id + " not found.");
}
return deleted;
} }
} }
@@ -1,6 +1,5 @@
package ua.com.dxrkness.service; package ua.com.dxrkness.service;
import org.jspecify.annotations.Nullable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ua.com.dxrkness.exception.VehicleNotFoundException; import ua.com.dxrkness.exception.VehicleNotFoundException;
import ua.com.dxrkness.model.Vehicle; import ua.com.dxrkness.model.Vehicle;
@@ -50,7 +49,11 @@ public final class VehicleService {
return repo.update(id, newVehicle); return repo.update(id, newVehicle);
} }
public @Nullable Vehicle delete(long id) { public Vehicle delete(long id) {
return repo.delete(id); var deleted = repo.delete(id);
if (deleted == null) {
throw new VehicleNotFoundException("Vehicle with ID " + id + " not found.");
}
return deleted;
} }
} }
@@ -18,15 +18,6 @@ import java.util.stream.Stream;
import static org.assertj.core.api.BDDAssertions.then; 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 @SpringBootTest
@NullMarked @NullMarked
class InterServiceCommunicationTest { class InterServiceCommunicationTest {
@@ -204,40 +195,4 @@ class InterServiceCommunicationTest {
.expectStatus() .expectStatus()
.isOk(); .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());
}
} }
@@ -48,78 +48,6 @@ class SubResourcesAndFilteringTest {
return Stream.of(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML); 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 @ParameterizedTest
@MethodSource("mediaTypes") @MethodSource("mediaTypes")
void getById_VehicleNotFound_Returns404WithErrorResponse(MediaType mediaType) { void getById_VehicleNotFound_Returns404WithErrorResponse(MediaType mediaType) {