From 9606ae2a0282eb1f0e7aead8f2766487dfafbf5d Mon Sep 17 00:00:00 2001 From: kevpdev Date: Thu, 8 Oct 2026 09:57:07 +0200 Subject: [PATCH 1/5] ci: add the check, security and codeql workflows and dependabot --- .github/dependabot.yml | 10 ++++++++++ .github/workflows/ci.yml | 21 +++++++++++++++++++++ .github/workflows/codeql.yml | 29 +++++++++++++++++++++++++++++ .github/workflows/trivy.yml | 30 ++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/trivy.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..48ca5f4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + - package-ecosystem: maven + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ed7caa2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: ci +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + check: + name: check + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 + with: + distribution: temurin + java-version: '21' + cache: maven + - name: Compile and run the tests + run: sh ./mvnw -B verify diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..788e8b2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,29 @@ +name: codeql +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + analyze: + name: codeql (${{ matrix.language }}) + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: [java-kotlin, actions] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4 + with: + languages: ${{ matrix.language }} + build-mode: none + - uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4 + with: + category: /language:${{ matrix.language }} diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 0000000..903a4fa --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,30 @@ +name: trivy +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + security: + name: security + runs-on: ubuntu-24.04 + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: fs + format: sarif + output: trivy-results.sarif + exit-code: '1' + severity: CRITICAL,HIGH + ignore-unfixed: true + limit-severities-for-sarif: true + - uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4 + if: always() + with: + sarif_file: trivy-results.sarif From 6bffde1afa3ed09f67409e2bca77bf3edd192453 Mon Sep 17 00:00:00 2001 From: kevpdev Date: Thu, 8 Oct 2026 10:25:25 +0200 Subject: [PATCH 2/5] chore(format): format the code with google-java-format --- .../kevpdev/swapi/SwaggerConfiguration.java | 17 +- .../fr/kevpdev/swapi/SwapiApplication.java | 7 +- .../swapi/controllers/FilmController.java | 37 +- .../swapi/controllers/PersonController.java | 59 +-- .../swapi/controllers/PlanetController.java | 52 +-- .../java/fr/kevpdev/swapi/dtos/FilmDTO.java | 37 +- .../java/fr/kevpdev/swapi/dtos/PersonDTO.java | 58 +-- .../java/fr/kevpdev/swapi/dtos/PlanetDTO.java | 37 +- .../kevpdev/swapi/dtos/SearchResultDTO.java | 14 +- .../fr/kevpdev/swapi/dtos/SpeciesDTO.java | 45 ++- .../fr/kevpdev/swapi/dtos/StarshipDTO.java | 50 ++- .../fr/kevpdev/swapi/dtos/VehicleDTO.java | 44 ++- .../kevpdev/swapi/exceptions/ErrorDetail.java | 19 +- .../exceptions/GlobalExceptionHandler.java | 28 +- .../kevpdev/swapi/services/FilmService.java | 8 +- .../kevpdev/swapi/services/PersonService.java | 8 +- .../kevpdev/swapi/services/PlanetService.java | 8 +- .../swapi/services/impl/FilmServiceImpl.java | 83 ++--- .../services/impl/PersonServiceImpl.java | 153 ++++---- .../services/impl/PlanetServiceImpl.java | 83 ++--- .../kevpdev/swapi/SwapiApplicationTests.java | 6 +- .../swapi/controllers/FilmControllerTest.java | 187 +++++----- .../controllers/PersonControllerTest.java | 336 +++++++++--------- .../controllers/PlanetControllerTest.java | 140 ++++---- 24 files changed, 752 insertions(+), 764 deletions(-) diff --git a/src/main/java/fr/kevpdev/swapi/SwaggerConfiguration.java b/src/main/java/fr/kevpdev/swapi/SwaggerConfiguration.java index dd86c0e..be529aa 100644 --- a/src/main/java/fr/kevpdev/swapi/SwaggerConfiguration.java +++ b/src/main/java/fr/kevpdev/swapi/SwaggerConfiguration.java @@ -8,12 +8,13 @@ @Configuration public class SwaggerConfiguration { - @Bean - public OpenAPI customOpenAPI() { - return new OpenAPI() - .info(new Info() - .title("API Documentation") - .version("1.0") - .description("Documentation for Swapi API V1.0")); - } + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info( + new Info() + .title("API Documentation") + .version("1.0") + .description("Documentation for Swapi API V1.0")); + } } diff --git a/src/main/java/fr/kevpdev/swapi/SwapiApplication.java b/src/main/java/fr/kevpdev/swapi/SwapiApplication.java index b654a8d..347d701 100644 --- a/src/main/java/fr/kevpdev/swapi/SwapiApplication.java +++ b/src/main/java/fr/kevpdev/swapi/SwapiApplication.java @@ -6,8 +6,7 @@ @SpringBootApplication public class SwapiApplication { - public static void main(String[] args) { - SpringApplication.run(SwapiApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(SwapiApplication.class, args); + } } diff --git a/src/main/java/fr/kevpdev/swapi/controllers/FilmController.java b/src/main/java/fr/kevpdev/swapi/controllers/FilmController.java index dcde506..7b98281 100644 --- a/src/main/java/fr/kevpdev/swapi/controllers/FilmController.java +++ b/src/main/java/fr/kevpdev/swapi/controllers/FilmController.java @@ -11,28 +11,25 @@ @RequestMapping("/api/v1/films") public class FilmController { - private final FilmService filmService; + private final FilmService filmService; - public FilmController(FilmService filmService) { - this.filmService = filmService; - } + public FilmController(FilmService filmService) { + this.filmService = filmService; + } - @GetMapping("/{id}") - public Mono> getFilmById(@PathVariable int id) { - return filmService.getFilmById(id) - .map(ResponseEntity::ok); - } + @GetMapping("/{id}") + public Mono> getFilmById(@PathVariable int id) { + return filmService.getFilmById(id).map(ResponseEntity::ok); + } - @GetMapping - public Mono>> getFilms( - @RequestParam(value = "page", required = false, defaultValue = "1") Integer pageId) { - return filmService.getFilms(pageId) - .map(ResponseEntity::ok); - } + @GetMapping + public Mono>> getFilms( + @RequestParam(value = "page", required = false, defaultValue = "1") Integer pageId) { + return filmService.getFilms(pageId).map(ResponseEntity::ok); + } - @GetMapping("/search") - public Mono>> getFilmByName(@RequestParam String title) { - return filmService.getFilmByTitle(title) - .map(ResponseEntity::ok); - } + @GetMapping("/search") + public Mono>> getFilmByName(@RequestParam String title) { + return filmService.getFilmByTitle(title).map(ResponseEntity::ok); + } } diff --git a/src/main/java/fr/kevpdev/swapi/controllers/PersonController.java b/src/main/java/fr/kevpdev/swapi/controllers/PersonController.java index edbd4d6..9cdee3e 100644 --- a/src/main/java/fr/kevpdev/swapi/controllers/PersonController.java +++ b/src/main/java/fr/kevpdev/swapi/controllers/PersonController.java @@ -11,32 +11,35 @@ @RequestMapping("/api/v1/people") public class PersonController { - private final PersonService personService; - - public PersonController(PersonService personService) { - this.personService = personService; - } - - @GetMapping("/{id}") - public Mono> getPersonById(@PathVariable int id) { - return personService.getPersonById(id) - .map(ResponseEntity::ok) - .defaultIfEmpty(ResponseEntity.notFound().build()); - } - - @GetMapping - public Mono>> getPeople( - @RequestParam(value = "page", required = false, defaultValue = "1") Integer pageId) { - return personService.getPeople(pageId) - .map(ResponseEntity::ok) - .defaultIfEmpty(ResponseEntity.notFound().build()); - } - - @GetMapping("/search") - public Mono>> getPersonByName(@RequestParam String name) { - return personService.getPersonByName(name) - .map(ResponseEntity::ok) - .defaultIfEmpty(ResponseEntity.notFound().build()); - } - + private final PersonService personService; + + public PersonController(PersonService personService) { + this.personService = personService; + } + + @GetMapping("/{id}") + public Mono> getPersonById(@PathVariable int id) { + return personService + .getPersonById(id) + .map(ResponseEntity::ok) + .defaultIfEmpty(ResponseEntity.notFound().build()); + } + + @GetMapping + public Mono>> getPeople( + @RequestParam(value = "page", required = false, defaultValue = "1") Integer pageId) { + return personService + .getPeople(pageId) + .map(ResponseEntity::ok) + .defaultIfEmpty(ResponseEntity.notFound().build()); + } + + @GetMapping("/search") + public Mono>> getPersonByName( + @RequestParam String name) { + return personService + .getPersonByName(name) + .map(ResponseEntity::ok) + .defaultIfEmpty(ResponseEntity.notFound().build()); + } } diff --git a/src/main/java/fr/kevpdev/swapi/controllers/PlanetController.java b/src/main/java/fr/kevpdev/swapi/controllers/PlanetController.java index 8f67581..fc12302 100644 --- a/src/main/java/fr/kevpdev/swapi/controllers/PlanetController.java +++ b/src/main/java/fr/kevpdev/swapi/controllers/PlanetController.java @@ -10,32 +10,36 @@ @RestController @RequestMapping("/api/v1/planets") public class PlanetController { - - private final PlanetService planetService; - public PlanetController(PlanetService planetService) { - this.planetService = planetService; - } + private final PlanetService planetService; - @GetMapping("/{id}") - public Mono> getPlanetById(@PathVariable int id) { - return planetService.getPlanetById(id) - .map(ResponseEntity::ok) - .defaultIfEmpty(ResponseEntity.notFound().build()); - } + public PlanetController(PlanetService planetService) { + this.planetService = planetService; + } - @GetMapping - public Mono>> getPlanets( - @RequestParam(value = "page", required = false, defaultValue = "1") Integer pageId) { - return planetService.getPlanets(pageId) - .map(ResponseEntity::ok) - .defaultIfEmpty(ResponseEntity.notFound().build()); - } + @GetMapping("/{id}") + public Mono> getPlanetById(@PathVariable int id) { + return planetService + .getPlanetById(id) + .map(ResponseEntity::ok) + .defaultIfEmpty(ResponseEntity.notFound().build()); + } - @GetMapping("/search") - public Mono>> getPlanetByName(@RequestParam String name) { - return planetService.getPlanetByName(name) - .map(ResponseEntity::ok) - .defaultIfEmpty(ResponseEntity.notFound().build()); - } + @GetMapping + public Mono>> getPlanets( + @RequestParam(value = "page", required = false, defaultValue = "1") Integer pageId) { + return planetService + .getPlanets(pageId) + .map(ResponseEntity::ok) + .defaultIfEmpty(ResponseEntity.notFound().build()); + } + + @GetMapping("/search") + public Mono>> getPlanetByName( + @RequestParam String name) { + return planetService + .getPlanetByName(name) + .map(ResponseEntity::ok) + .defaultIfEmpty(ResponseEntity.notFound().build()); + } } diff --git a/src/main/java/fr/kevpdev/swapi/dtos/FilmDTO.java b/src/main/java/fr/kevpdev/swapi/dtos/FilmDTO.java index 0164c15..ae6e28f 100644 --- a/src/main/java/fr/kevpdev/swapi/dtos/FilmDTO.java +++ b/src/main/java/fr/kevpdev/swapi/dtos/FilmDTO.java @@ -1,35 +1,34 @@ package fr.kevpdev.swapi.dtos; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import lombok.Builder; import lombok.Data; -import java.util.List; - @Data @Builder public class FilmDTO { - private String title; + private String title; - @JsonProperty("episode_id") - private int episodeId; + @JsonProperty("episode_id") + private int episodeId; - @JsonProperty("opening_crawl") - private String openingCrawl; + @JsonProperty("opening_crawl") + private String openingCrawl; - private String director; - private String producer; + private String director; + private String producer; - @JsonProperty("release_date") - private String releaseDate; + @JsonProperty("release_date") + private String releaseDate; - private List characters; - private List planets; - private List starships; - private List vehicles; - private List species; + private List characters; + private List planets; + private List starships; + private List vehicles; + private List species; - private String created; - private String edited; - private String url; + private String created; + private String edited; + private String url; } diff --git a/src/main/java/fr/kevpdev/swapi/dtos/PersonDTO.java b/src/main/java/fr/kevpdev/swapi/dtos/PersonDTO.java index c551373..8a57e58 100644 --- a/src/main/java/fr/kevpdev/swapi/dtos/PersonDTO.java +++ b/src/main/java/fr/kevpdev/swapi/dtos/PersonDTO.java @@ -1,38 +1,42 @@ package fr.kevpdev.swapi.dtos; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import lombok.Builder; import lombok.Data; -import java.util.List; - @Data @Builder public class PersonDTO { - @JsonProperty("birth_year") - private String birthYear; - @JsonProperty("eye_color") - private String eyeColor; - private List films; - private String gender; - @JsonProperty("hair_color") - private String hairColor; - private String height; - private String homeworld; - private String mass; - private String name; - @JsonProperty("skin_color") - private String skinColor; - private String created; - private String edited; - private List species; - private List starships; - private String url; - private List vehicles; - - /* lists with data */ - private List filmsDTO; - private List starshipsDTO; - private List vehiclesDTO; + @JsonProperty("birth_year") + private String birthYear; + + @JsonProperty("eye_color") + private String eyeColor; + + private List films; + private String gender; + + @JsonProperty("hair_color") + private String hairColor; + + private String height; + private String homeworld; + private String mass; + private String name; + + @JsonProperty("skin_color") + private String skinColor; + + private String created; + private String edited; + private List species; + private List starships; + private String url; + private List vehicles; + /* lists with data */ + private List filmsDTO; + private List starshipsDTO; + private List vehiclesDTO; } diff --git a/src/main/java/fr/kevpdev/swapi/dtos/PlanetDTO.java b/src/main/java/fr/kevpdev/swapi/dtos/PlanetDTO.java index 48749ec..1236102 100644 --- a/src/main/java/fr/kevpdev/swapi/dtos/PlanetDTO.java +++ b/src/main/java/fr/kevpdev/swapi/dtos/PlanetDTO.java @@ -1,35 +1,34 @@ package fr.kevpdev.swapi.dtos; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import lombok.Builder; import lombok.Data; -import java.util.List; - @Data @Builder public class PlanetDTO { - private String name; + private String name; - @JsonProperty("rotation_period") - private String rotationPeriod; + @JsonProperty("rotation_period") + private String rotationPeriod; - @JsonProperty("orbital_period") - private String orbitalPeriod; + @JsonProperty("orbital_period") + private String orbitalPeriod; - private String diameter; - private String climate; - private String gravity; - private String terrain; + private String diameter; + private String climate; + private String gravity; + private String terrain; - @JsonProperty("surface_water") - private String surfaceWater; + @JsonProperty("surface_water") + private String surfaceWater; - private String population; - private List residents; - private List films; + private String population; + private List residents; + private List films; - private String created; - private String edited; - private String url; + private String created; + private String edited; + private String url; } diff --git a/src/main/java/fr/kevpdev/swapi/dtos/SearchResultDTO.java b/src/main/java/fr/kevpdev/swapi/dtos/SearchResultDTO.java index 3a89b25..dc8decc 100644 --- a/src/main/java/fr/kevpdev/swapi/dtos/SearchResultDTO.java +++ b/src/main/java/fr/kevpdev/swapi/dtos/SearchResultDTO.java @@ -1,17 +1,15 @@ package fr.kevpdev.swapi.dtos; +import java.util.List; import lombok.Builder; import lombok.Data; -import java.util.List; - @Data @Builder -public class SearchResultDTO { - - public Integer count; - public String next; - public String previous; - public List results; +public class SearchResultDTO { + public Integer count; + public String next; + public String previous; + public List results; } diff --git a/src/main/java/fr/kevpdev/swapi/dtos/SpeciesDTO.java b/src/main/java/fr/kevpdev/swapi/dtos/SpeciesDTO.java index e6144f5..8c992bc 100644 --- a/src/main/java/fr/kevpdev/swapi/dtos/SpeciesDTO.java +++ b/src/main/java/fr/kevpdev/swapi/dtos/SpeciesDTO.java @@ -1,39 +1,38 @@ package fr.kevpdev.swapi.dtos; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import lombok.Builder; import lombok.Data; -import java.util.List; - @Data @Builder public class SpeciesDTO { - private String name; - private String classification; - private String designation; + private String name; + private String classification; + private String designation; - @JsonProperty("average_height") - private String averageHeight; + @JsonProperty("average_height") + private String averageHeight; - @JsonProperty("skin_colors") - private String skinColors; + @JsonProperty("skin_colors") + private String skinColors; - @JsonProperty("hair_colors") - private String hairColors; + @JsonProperty("hair_colors") + private String hairColors; - @JsonProperty("eye_colors") - private String eyeColors; + @JsonProperty("eye_colors") + private String eyeColors; - @JsonProperty("average_lifespan") - private String averageLifespan; + @JsonProperty("average_lifespan") + private String averageLifespan; - private String homeworld; - private String language; - private List people; - private List films; + private String homeworld; + private String language; + private List people; + private List films; - private String created; - private String edited; - private String url; -} \ No newline at end of file + private String created; + private String edited; + private String url; +} diff --git a/src/main/java/fr/kevpdev/swapi/dtos/StarshipDTO.java b/src/main/java/fr/kevpdev/swapi/dtos/StarshipDTO.java index ff3fd54..a984837 100644 --- a/src/main/java/fr/kevpdev/swapi/dtos/StarshipDTO.java +++ b/src/main/java/fr/kevpdev/swapi/dtos/StarshipDTO.java @@ -1,47 +1,45 @@ package fr.kevpdev.swapi.dtos; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import lombok.Builder; import lombok.Data; -import java.util.List; - @Data @Builder public class StarshipDTO { - private String name; - private String model; - private String manufacturer; + private String name; + private String model; + private String manufacturer; - @JsonProperty("cost_in_credits") - private String costInCredits; + @JsonProperty("cost_in_credits") + private String costInCredits; - private String length; + private String length; - @JsonProperty("max_atmosphering_speed") - private String maxAtmospheringSpeed; + @JsonProperty("max_atmosphering_speed") + private String maxAtmospheringSpeed; - private String crew; - private String passengers; + private String crew; + private String passengers; - @JsonProperty("cargo_capacity") - private String cargoCapacity; + @JsonProperty("cargo_capacity") + private String cargoCapacity; - private String consumables; + private String consumables; - @JsonProperty("hyperdrive_rating") - private String hyperdriveRating; + @JsonProperty("hyperdrive_rating") + private String hyperdriveRating; - private String MGLT; + private String MGLT; - @JsonProperty("starship_class") - private String starshipClass; + @JsonProperty("starship_class") + private String starshipClass; - private List pilots; - private List films; + private List pilots; + private List films; - private String created; - private String edited; - private String url; + private String created; + private String edited; + private String url; } - diff --git a/src/main/java/fr/kevpdev/swapi/dtos/VehicleDTO.java b/src/main/java/fr/kevpdev/swapi/dtos/VehicleDTO.java index ebbdc81..34f0811 100644 --- a/src/main/java/fr/kevpdev/swapi/dtos/VehicleDTO.java +++ b/src/main/java/fr/kevpdev/swapi/dtos/VehicleDTO.java @@ -1,42 +1,40 @@ package fr.kevpdev.swapi.dtos; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import lombok.Builder; import lombok.Data; -import java.util.List; - @Data @Builder public class VehicleDTO { - private String name; - private String model; - private String manufacturer; + private String name; + private String model; + private String manufacturer; - @JsonProperty("cost_in_credits") - private String costInCredits; + @JsonProperty("cost_in_credits") + private String costInCredits; - private String length; + private String length; - @JsonProperty("max_atmosphering_speed") - private String maxAtmospheringSpeed; + @JsonProperty("max_atmosphering_speed") + private String maxAtmospheringSpeed; - private String crew; - private String passengers; + private String crew; + private String passengers; - @JsonProperty("cargo_capacity") - private String cargoCapacity; + @JsonProperty("cargo_capacity") + private String cargoCapacity; - private String consumables; + private String consumables; - @JsonProperty("vehicle_class") - private String vehicleClass; + @JsonProperty("vehicle_class") + private String vehicleClass; - private List pilots; - private List films; + private List pilots; + private List films; - private String created; - private String edited; - private String url; + private String created; + private String edited; + private String url; } - diff --git a/src/main/java/fr/kevpdev/swapi/exceptions/ErrorDetail.java b/src/main/java/fr/kevpdev/swapi/exceptions/ErrorDetail.java index 24be53c..36bd66f 100644 --- a/src/main/java/fr/kevpdev/swapi/exceptions/ErrorDetail.java +++ b/src/main/java/fr/kevpdev/swapi/exceptions/ErrorDetail.java @@ -1,19 +1,18 @@ package fr.kevpdev.swapi.exceptions; -import lombok.Data; - import java.util.Date; +import lombok.Data; @Data public class ErrorDetail { - private Date timestamp; - private int statusCode; - private String message; + private Date timestamp; + private int statusCode; + private String message; - public ErrorDetail(int statusCode, String message) { - this.timestamp = new Date(); - this.statusCode = statusCode; - this.message = message; - } + public ErrorDetail(int statusCode, String message) { + this.timestamp = new Date(); + this.statusCode = statusCode; + this.message = message; + } } diff --git a/src/main/java/fr/kevpdev/swapi/exceptions/GlobalExceptionHandler.java b/src/main/java/fr/kevpdev/swapi/exceptions/GlobalExceptionHandler.java index 00d86f0..de2a030 100644 --- a/src/main/java/fr/kevpdev/swapi/exceptions/GlobalExceptionHandler.java +++ b/src/main/java/fr/kevpdev/swapi/exceptions/GlobalExceptionHandler.java @@ -9,22 +9,22 @@ @ControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(WebClientResponseException.class) - public ResponseEntity handleWebClientResponseException(WebClientResponseException ex) { - HttpStatus status = (HttpStatus) ex.getStatusCode(); - String message = ex.getMessage(); + @ExceptionHandler(WebClientResponseException.class) + public ResponseEntity handleWebClientResponseException( + WebClientResponseException ex) { + HttpStatus status = (HttpStatus) ex.getStatusCode(); + String message = ex.getMessage(); - ErrorDetail errorDetail = new ErrorDetail(status.value(), message); + ErrorDetail errorDetail = new ErrorDetail(status.value(), message); - return ResponseEntity.status(status).body(errorDetail); - } + return ResponseEntity.status(status).body(errorDetail); + } + @ExceptionHandler(Exception.class) + public ResponseEntity handleGenericException(Exception ex) { + String message = "Erreur inattendue : " + ex.getMessage(); + ErrorDetail errorDetail = new ErrorDetail(HttpStatus.INTERNAL_SERVER_ERROR.value(), message); - @ExceptionHandler(Exception.class) - public ResponseEntity handleGenericException(Exception ex) { - String message = "Erreur inattendue : " + ex.getMessage(); - ErrorDetail errorDetail = new ErrorDetail(HttpStatus.INTERNAL_SERVER_ERROR.value(), message); - - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorDetail); - } + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorDetail); + } } diff --git a/src/main/java/fr/kevpdev/swapi/services/FilmService.java b/src/main/java/fr/kevpdev/swapi/services/FilmService.java index 5c97c42..75a511f 100644 --- a/src/main/java/fr/kevpdev/swapi/services/FilmService.java +++ b/src/main/java/fr/kevpdev/swapi/services/FilmService.java @@ -6,7 +6,9 @@ public interface FilmService { - Mono getFilmById(Integer id); - Mono> getFilms(Integer pageId); - Mono> getFilmByTitle(String title); + Mono getFilmById(Integer id); + + Mono> getFilms(Integer pageId); + + Mono> getFilmByTitle(String title); } diff --git a/src/main/java/fr/kevpdev/swapi/services/PersonService.java b/src/main/java/fr/kevpdev/swapi/services/PersonService.java index 4f690b5..d9f9226 100644 --- a/src/main/java/fr/kevpdev/swapi/services/PersonService.java +++ b/src/main/java/fr/kevpdev/swapi/services/PersonService.java @@ -4,11 +4,11 @@ import fr.kevpdev.swapi.dtos.SearchResultDTO; import reactor.core.publisher.Mono; - public interface PersonService { - Mono getPersonById(Integer id); - Mono> getPeople(Integer pageId); - Mono> getPersonByName(String name); + Mono getPersonById(Integer id); + + Mono> getPeople(Integer pageId); + Mono> getPersonByName(String name); } diff --git a/src/main/java/fr/kevpdev/swapi/services/PlanetService.java b/src/main/java/fr/kevpdev/swapi/services/PlanetService.java index 51fd4bf..568e196 100644 --- a/src/main/java/fr/kevpdev/swapi/services/PlanetService.java +++ b/src/main/java/fr/kevpdev/swapi/services/PlanetService.java @@ -6,7 +6,9 @@ public interface PlanetService { - Mono getPlanetById(Integer id); - Mono> getPlanets(Integer pageId); - Mono> getPlanetByName(String name); + Mono getPlanetById(Integer id); + + Mono> getPlanets(Integer pageId); + + Mono> getPlanetByName(String name); } diff --git a/src/main/java/fr/kevpdev/swapi/services/impl/FilmServiceImpl.java b/src/main/java/fr/kevpdev/swapi/services/impl/FilmServiceImpl.java index 5030405..8e2dcdf 100644 --- a/src/main/java/fr/kevpdev/swapi/services/impl/FilmServiceImpl.java +++ b/src/main/java/fr/kevpdev/swapi/services/impl/FilmServiceImpl.java @@ -11,49 +11,50 @@ @Service public class FilmServiceImpl implements FilmService { - private final WebClient webClient; + private final WebClient webClient; - public FilmServiceImpl(WebClient.Builder webClient) { - this.webClient = webClient.baseUrl("https://swapi.dev/api/films").build(); - } + public FilmServiceImpl(WebClient.Builder webClient) { + this.webClient = webClient.baseUrl("https://swapi.dev/api/films").build(); + } - /** - * Retrives a film by ID - * @param id film ID - * @return a Mono containing the FilmDTO object if found or empty if not - */ - @Override - public Mono getFilmById(Integer id) { - return webClient.get() - .uri("/{id}", id) - .retrieve() - .bodyToMono(FilmDTO.class); - } + /** + * Retrives a film by ID + * + * @param id film ID + * @return a Mono containing the FilmDTO object if found or empty if not + */ + @Override + public Mono getFilmById(Integer id) { + return webClient.get().uri("/{id}", id).retrieve().bodyToMono(FilmDTO.class); + } - /** - * Retrives all film with pagination - * - * @param pageId Pagination number - * @return a Mono containing the filmDTO object containing a FilmDTO array - */ - @Override - public Mono> getFilms(Integer pageId) { - return webClient.get() - .uri("/?page={pageId}", pageId) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() {}); - } + /** + * Retrives all film with pagination + * + * @param pageId Pagination number + * @return a Mono containing the filmDTO object containing a FilmDTO array + */ + @Override + public Mono> getFilms(Integer pageId) { + return webClient + .get() + .uri("/?page={pageId}", pageId) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}); + } - /** - * Retrives all films by title - * @param title film title - * @return a Mono containing the FilmDTO object list if found or empty if not - */ - @Override - public Mono> getFilmByTitle(String title) { - return webClient.get() - .uri("/?search={title}", title) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() {}); - } + /** + * Retrives all films by title + * + * @param title film title + * @return a Mono containing the FilmDTO object list if found or empty if not + */ + @Override + public Mono> getFilmByTitle(String title) { + return webClient + .get() + .uri("/?search={title}", title) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}); + } } diff --git a/src/main/java/fr/kevpdev/swapi/services/impl/PersonServiceImpl.java b/src/main/java/fr/kevpdev/swapi/services/impl/PersonServiceImpl.java index 33c5590..45c385f 100644 --- a/src/main/java/fr/kevpdev/swapi/services/impl/PersonServiceImpl.java +++ b/src/main/java/fr/kevpdev/swapi/services/impl/PersonServiceImpl.java @@ -2,6 +2,8 @@ import fr.kevpdev.swapi.dtos.*; import fr.kevpdev.swapi.services.PersonService; +import java.net.URI; +import java.util.List; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; import org.springframework.stereotype.Service; @@ -9,93 +11,98 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.net.URI; -import java.util.List; - @Service public class PersonServiceImpl implements PersonService { - private final WebClient webClient; - - @Value("${swapi.url}") - private String swapiUri; + private final WebClient webClient; - public PersonServiceImpl(WebClient.Builder client) { - this.webClient = client.baseUrl("https://swapi.dev/api/people").build(); - } + @Value("${swapi.url}") + private String swapiUri; + public PersonServiceImpl(WebClient.Builder client) { + this.webClient = client.baseUrl("https://swapi.dev/api/people").build(); + } - /** - * Retrives a person by ID - * @param id person ID - * @return a Mono containing the PersonDTO object if found or empty if not - */ - @Override - public Mono getPersonById(Integer id) { - return webClient.get() - .uri("/{id}", id) - .retrieve() - .bodyToMono(PersonDTO.class) - .flatMap(personDTO -> { + /** + * Retrives a person by ID + * + * @param id person ID + * @return a Mono containing the PersonDTO object if found or empty if not + */ + @Override + public Mono getPersonById(Integer id) { + return webClient + .get() + .uri("/{id}", id) + .retrieve() + .bodyToMono(PersonDTO.class) + .flatMap( + personDTO -> { - // Exemple pour la curiosité de récupération des DTO - // Mais je ne vais pas étendre car aucune indication des besoins du front et aussi moyen pour les perfs - // L'utilisation d'un cache serait cool pour eviter les appels inutiles - Mono> monoFilmsDTO = getResourcesByUrls(personDTO.getFilms(), FilmDTO.class); - Mono> monoStarshipsDTO = getResourcesByUrls(personDTO.getStarships(), StarshipDTO.class); - Mono> monoVehiclesDTO = getResourcesByUrls(personDTO.getStarships(), VehicleDTO.class); + // Exemple pour la curiosité de récupération des DTO + // Mais je ne vais pas étendre car aucune indication des besoins du front et aussi + // moyen pour les perfs + // L'utilisation d'un cache serait cool pour eviter les appels inutiles + Mono> monoFilmsDTO = + getResourcesByUrls(personDTO.getFilms(), FilmDTO.class); + Mono> monoStarshipsDTO = + getResourcesByUrls(personDTO.getStarships(), StarshipDTO.class); + Mono> monoVehiclesDTO = + getResourcesByUrls(personDTO.getStarships(), VehicleDTO.class); - return Mono.zip(monoFilmsDTO, monoStarshipsDTO,monoVehiclesDTO).map(data -> { + return Mono.zip(monoFilmsDTO, monoStarshipsDTO, monoVehiclesDTO) + .map( + data -> { personDTO.setFilmsDTO(data.getT1()); personDTO.setStarshipsDTO(data.getT2()); personDTO.setVehiclesDTO(data.getT3()); return personDTO; - }); - }); - } - - /** - * Retrives all people with pagination - * - * @param pageId Pagination number - * @return a Mono containing the SearchResultDTO object containing a PersonDTO array - */ - @Override - public Mono> getPeople(Integer pageId) { + }); + }); + } - return webClient.get() - .uri("/?page={pageId}", pageId) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() {}); - } + /** + * Retrives all people with pagination + * + * @param pageId Pagination number + * @return a Mono containing the SearchResultDTO object containing a PersonDTO array + */ + @Override + public Mono> getPeople(Integer pageId) { + return webClient + .get() + .uri("/?page={pageId}", pageId) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}); + } - /** - * Retrives all people by name - * @param name person name - * @return a Mono containing the PersonDTO object list if found or empty if not - */ - @Override - public Mono> getPersonByName(String name) { - return webClient.get() - .uri("/?search={name}", name) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() {}); - } + /** + * Retrives all people by name + * + * @param name person name + * @return a Mono containing the PersonDTO object list if found or empty if not + */ + @Override + public Mono> getPersonByName(String name) { + return webClient + .get() + .uri("/?search={name}", name) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}); + } - /** - * Retrieves DTO object list by hypermedia link - * @param urls hypermedialink list - * @param responseType DTO type - * @return a Mono containing the Generic DTO object list if found or empty if not - * @param - */ - public Mono> getResourcesByUrls(List urls, Class responseType) { - return Flux.fromIterable(urls) - .flatMap(url -> webClient.get() - .uri(URI.create(url)) - .retrieve() - .bodyToMono(responseType)) - .collectList(); - } + /** + * Retrieves DTO object list by hypermedia link + * + * @param urls hypermedialink list + * @param responseType DTO type + * @return a Mono containing the Generic DTO object list if found or empty if not + * @param + */ + public Mono> getResourcesByUrls(List urls, Class responseType) { + return Flux.fromIterable(urls) + .flatMap(url -> webClient.get().uri(URI.create(url)).retrieve().bodyToMono(responseType)) + .collectList(); + } } diff --git a/src/main/java/fr/kevpdev/swapi/services/impl/PlanetServiceImpl.java b/src/main/java/fr/kevpdev/swapi/services/impl/PlanetServiceImpl.java index 0fbb07b..6a09d0b 100644 --- a/src/main/java/fr/kevpdev/swapi/services/impl/PlanetServiceImpl.java +++ b/src/main/java/fr/kevpdev/swapi/services/impl/PlanetServiceImpl.java @@ -11,49 +11,50 @@ @Service public class PlanetServiceImpl implements PlanetService { - private final WebClient webClient; + private final WebClient webClient; - public PlanetServiceImpl(WebClient.Builder webClient) { - this.webClient = webClient.baseUrl("https://swapi.dev/api/planets").build(); - } + public PlanetServiceImpl(WebClient.Builder webClient) { + this.webClient = webClient.baseUrl("https://swapi.dev/api/planets").build(); + } - /** - * Retrives a planet by ID - * @param id planet ID - * @return a Mono containing the PlanetDTO object if found or empty if not - */ - @Override - public Mono getPlanetById(Integer id) { - return webClient.get() - .uri("/{id}", id) - .retrieve() - .bodyToMono(PlanetDTO.class); - } + /** + * Retrives a planet by ID + * + * @param id planet ID + * @return a Mono containing the PlanetDTO object if found or empty if not + */ + @Override + public Mono getPlanetById(Integer id) { + return webClient.get().uri("/{id}", id).retrieve().bodyToMono(PlanetDTO.class); + } - /** - * Retrives all planets with pagination - * - * @param pageId Pagination number - * @return a Mono containing the SearchResultDTO object containing a PlanetDTO array - */ - @Override - public Mono> getPlanets(Integer pageId) { - return webClient.get() - .uri("/?page={pageId}", pageId) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() {}); - } + /** + * Retrives all planets with pagination + * + * @param pageId Pagination number + * @return a Mono containing the SearchResultDTO object containing a PlanetDTO array + */ + @Override + public Mono> getPlanets(Integer pageId) { + return webClient + .get() + .uri("/?page={pageId}", pageId) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}); + } - /** - * Retrives all planets by name - * @param name planet name - * @return a Mono containing the PlanetDTO object list if found or empty if not - */ - @Override - public Mono> getPlanetByName(String name) { - return webClient.get() - .uri("/?search={name}", name) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() {}); - } + /** + * Retrives all planets by name + * + * @param name planet name + * @return a Mono containing the PlanetDTO object list if found or empty if not + */ + @Override + public Mono> getPlanetByName(String name) { + return webClient + .get() + .uri("/?search={name}", name) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}); + } } diff --git a/src/test/java/fr/kevpdev/swapi/SwapiApplicationTests.java b/src/test/java/fr/kevpdev/swapi/SwapiApplicationTests.java index b91dcb5..be8aa23 100644 --- a/src/test/java/fr/kevpdev/swapi/SwapiApplicationTests.java +++ b/src/test/java/fr/kevpdev/swapi/SwapiApplicationTests.java @@ -6,8 +6,6 @@ @SpringBootTest class SwapiApplicationTests { - @Test - void contextLoads() { - } - + @Test + void contextLoads() {} } diff --git a/src/test/java/fr/kevpdev/swapi/controllers/FilmControllerTest.java b/src/test/java/fr/kevpdev/swapi/controllers/FilmControllerTest.java index 0085390..7456328 100644 --- a/src/test/java/fr/kevpdev/swapi/controllers/FilmControllerTest.java +++ b/src/test/java/fr/kevpdev/swapi/controllers/FilmControllerTest.java @@ -1,8 +1,12 @@ package fr.kevpdev.swapi.controllers; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import fr.kevpdev.swapi.dtos.FilmDTO; import fr.kevpdev.swapi.dtos.SearchResultDTO; import fr.kevpdev.swapi.services.FilmService; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -12,103 +16,92 @@ import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; -import java.util.List; - -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @WebFluxTest(FilmController.class) class FilmControllerTest { - private static final String PATH = "/api/v1/films"; - private FilmDTO filmDTO; - - private SearchResultDTO searchResultDTO; - - @Autowired - private WebTestClient webTestClient; - - @MockBean - private FilmService filmService; - - @BeforeEach - public void setup() { - this.filmDTO = FilmDTO.builder() - .title("A New Hope") - .episodeId(4) - .openingCrawl("It is a period of civil war...") - .director("George Lucas") - .producer("Gary Kurtz, Rick McCallum") - .releaseDate("1977-05-25") - .characters(List.of( - "https://swapi.dev/api/people/1/", - "https://swapi.dev/api/people/2/", - "https://swapi.dev/api/people/3/", - "https://swapi.dev/api/people/4/" - )) - .planets(List.of( - "https://swapi.dev/api/planets/1/", - "https://swapi.dev/api/planets/2/", - "https://swapi.dev/api/planets/3/" - )) - .starships(List.of( - "https://swapi.dev/api/starships/2/", - "https://swapi.dev/api/starships/3/", - "https://swapi.dev/api/starships/5/" - )) - .vehicles(List.of( - "https://swapi.dev/api/vehicles/4/", - "https://swapi.dev/api/vehicles/6/" - )) - .species(List.of( - "https://swapi.dev/api/species/1/", - "https://swapi.dev/api/species/2/" - )) - .created("2014-12-10T14:23:31.880000Z") - .edited("2014-12-20T19:49:45.256000Z") - .url("https://swapi.dev/api/films/1/") - .build(); - - - this.searchResultDTO = SearchResultDTO.builder() - .count(6) - .previous(null) - .results(List.of(filmDTO)) - .build(); - } - - @Test - public void shouldReturnFilmByIdWhenFilmIdValid() { - - when(filmService.getFilmById(1)).thenReturn(Mono.just(filmDTO)); - - webTestClient.get().uri(PATH+"/1") - .exchange() - .expectStatus().isOk() - .expectBody(FilmDTO.class) - .isEqualTo(filmDTO); - - verify(filmService).getFilmById(1); - } - - @Test - public void shouldReturn404ErrorFilmByIdWhenFilmNotFound() { - - when(filmService.getFilmById(1)).thenReturn( - Mono.error( - WebClientResponseException - .create(404, "Film not Found", null, null, null))); - - webTestClient.get().uri(PATH+"/1") - .exchange() - .expectStatus().isNotFound(); - - verify(filmService).getFilmById(1); - - } - - /* - TODO... - */ - -} \ No newline at end of file + private static final String PATH = "/api/v1/films"; + private FilmDTO filmDTO; + + private SearchResultDTO searchResultDTO; + + @Autowired private WebTestClient webTestClient; + + @MockBean private FilmService filmService; + + @BeforeEach + public void setup() { + this.filmDTO = + FilmDTO.builder() + .title("A New Hope") + .episodeId(4) + .openingCrawl("It is a period of civil war...") + .director("George Lucas") + .producer("Gary Kurtz, Rick McCallum") + .releaseDate("1977-05-25") + .characters( + List.of( + "https://swapi.dev/api/people/1/", + "https://swapi.dev/api/people/2/", + "https://swapi.dev/api/people/3/", + "https://swapi.dev/api/people/4/")) + .planets( + List.of( + "https://swapi.dev/api/planets/1/", + "https://swapi.dev/api/planets/2/", + "https://swapi.dev/api/planets/3/")) + .starships( + List.of( + "https://swapi.dev/api/starships/2/", + "https://swapi.dev/api/starships/3/", + "https://swapi.dev/api/starships/5/")) + .vehicles( + List.of("https://swapi.dev/api/vehicles/4/", "https://swapi.dev/api/vehicles/6/")) + .species( + List.of("https://swapi.dev/api/species/1/", "https://swapi.dev/api/species/2/")) + .created("2014-12-10T14:23:31.880000Z") + .edited("2014-12-20T19:49:45.256000Z") + .url("https://swapi.dev/api/films/1/") + .build(); + + this.searchResultDTO = + SearchResultDTO.builder() + .count(6) + .previous(null) + .results(List.of(filmDTO)) + .build(); + } + + @Test + public void shouldReturnFilmByIdWhenFilmIdValid() { + + when(filmService.getFilmById(1)).thenReturn(Mono.just(filmDTO)); + + webTestClient + .get() + .uri(PATH + "/1") + .exchange() + .expectStatus() + .isOk() + .expectBody(FilmDTO.class) + .isEqualTo(filmDTO); + + verify(filmService).getFilmById(1); + } + + @Test + public void shouldReturn404ErrorFilmByIdWhenFilmNotFound() { + + when(filmService.getFilmById(1)) + .thenReturn( + Mono.error(WebClientResponseException.create(404, "Film not Found", null, null, null))); + + webTestClient.get().uri(PATH + "/1").exchange().expectStatus().isNotFound(); + + verify(filmService).getFilmById(1); + } + + /* + TODO... + */ + +} diff --git a/src/test/java/fr/kevpdev/swapi/controllers/PersonControllerTest.java b/src/test/java/fr/kevpdev/swapi/controllers/PersonControllerTest.java index 552e1ab..fc3ad21 100644 --- a/src/test/java/fr/kevpdev/swapi/controllers/PersonControllerTest.java +++ b/src/test/java/fr/kevpdev/swapi/controllers/PersonControllerTest.java @@ -1,8 +1,13 @@ package fr.kevpdev.swapi.controllers; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import fr.kevpdev.swapi.dtos.PersonDTO; import fr.kevpdev.swapi.dtos.SearchResultDTO; import fr.kevpdev.swapi.services.PersonService; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -13,179 +18,164 @@ import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; -import java.util.List; - -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @WebFluxTest(PersonController.class) class PersonControllerTest { - private static final String PATH = "/api/v1/people"; - private PersonDTO personDTO; - - private SearchResultDTO searchResultDTO; - - @Autowired - private WebTestClient webTestClient; - - @MockBean - private PersonService personService; - - @BeforeEach - public void setup() { - this.personDTO = PersonDTO.builder() - .name("Luke Skywalker") - .birthYear("19BBY") - .eyeColor("Blue") - .gender("Male") - .hairColor("Blond") - .height("172") - .mass("77") - .homeworld("https://swapi.dev/api/planets/1/") - .films(List.of("https://swapi.dev/api/films/1/", "https://swapi.dev/api/films/2/")) - .species(List.of()) - .starships(List.of("https://swapi.dev/api/starships/12/")) - .vehicles(List.of("https://swapi.dev/api/vehicles/14/")) - .created("2014-12-09T13:50:51.644000Z") - .edited("2014-12-20T21:17:56.891000Z") - .url("https://swapi.dev/api/people/1/") - .build(); - - this.searchResultDTO = SearchResultDTO.builder() - .count(82) - .previous(null) - .results(List.of(personDTO)) - .build(); - } - - - @Test - public void ShouldReturnPersonByIdWhenStatusIsOK() { - - when(personService.getPersonById(1)).thenReturn(Mono.just(personDTO)); - - webTestClient.get().uri(PATH+"/1") - .exchange() - .expectStatus().isOk() - .expectBody(PersonDTO.class) - .isEqualTo(personDTO); - - verify(personService).getPersonById(1); - - } - - @Test - public void ShouldTReturn404ErrorWhenEmptyPerson() { - - when(personService.getPersonById(100)) - .thenReturn(Mono - .error(WebClientResponseException - .create(404,"", null, null, null))); - - webTestClient.get().uri(PATH+"/100") - .exchange() - .expectStatus().isNotFound(); - - verify(personService).getPersonById(100); - - } - - @Test - public void ShouldReturnAllPersonWhenPageIdNotEmpty() { - - shouldReturnAllPeopleWhenStatusOK(1); - - } - - @Test - public void ShouldReturnAllPersonWhenEmptyPageId() { - - shouldReturnAllPeopleWhenStatusOK(null); - - } - - /** - * Test to retrieve all people when status is OK with a pageId param. - * If pageId is null, then default page number is 1 - * @param pageId Page number for the pagination - */ - private void shouldReturnAllPeopleWhenStatusOK(Integer pageId) { - - when(personService.getPeople(1)).thenReturn(Mono.just(searchResultDTO)); - - webTestClient.get() - .uri(uriBuilder -> { - var builder = uriBuilder.path(PATH); - if (pageId != null) { - builder.queryParam("page", pageId); - } - return builder.build(); - }) - .exchange() - .expectStatus().isOk() - .expectBody(new ParameterizedTypeReference>() {}) - .isEqualTo(searchResultDTO); - - verify(personService).getPeople(1); - } - - - @Test - public void ShouldReturn404ErrorWhenSearchResultisNotFound() { - - when(personService.getPeople(1)).thenReturn(Mono - .error(WebClientResponseException - .create(404,"", null, null, null))); - - webTestClient.get().uri(uriBuilder -> uriBuilder - .path(PATH) - .queryParam("page", 1) - .build()) - .exchange() - .expectStatus().isNotFound(); - - verify(personService).getPeople(1); - - } - - @Test - public void ShouldReturnPersonByNameWhenStatusIsOk() { - - when(personService.getPersonByName(anyString())).thenReturn(Mono.just(searchResultDTO)); - String name = "Luke SkyWalker"; - webTestClient.get() - .uri(uriBuilder -> uriBuilder.path(PATH+"/search") - .queryParam("name", name) - .build()) - .exchange() - .expectStatus().isOk() - .expectBody(new ParameterizedTypeReference>() {}) - .isEqualTo(searchResultDTO); - - verify(personService).getPersonByName(name); - - } - - @Test - public void ShouldReturn404ErrorWhenPersonByNameIsNotFound() { - - String name = "Luke SkyWalker"; - - when(personService.getPersonByName(name)) - .thenReturn(Mono - .error(WebClientResponseException - .create(404,"", null, null, null))); - - webTestClient.get() - .uri(uriBuilder -> uriBuilder.path(PATH+"/search") - .queryParam("name", name) - .build()) - .exchange() - .expectStatus().isNotFound(); - - verify(personService).getPersonByName(name); - - } -} \ No newline at end of file + private static final String PATH = "/api/v1/people"; + private PersonDTO personDTO; + + private SearchResultDTO searchResultDTO; + + @Autowired private WebTestClient webTestClient; + + @MockBean private PersonService personService; + + @BeforeEach + public void setup() { + this.personDTO = + PersonDTO.builder() + .name("Luke Skywalker") + .birthYear("19BBY") + .eyeColor("Blue") + .gender("Male") + .hairColor("Blond") + .height("172") + .mass("77") + .homeworld("https://swapi.dev/api/planets/1/") + .films(List.of("https://swapi.dev/api/films/1/", "https://swapi.dev/api/films/2/")) + .species(List.of()) + .starships(List.of("https://swapi.dev/api/starships/12/")) + .vehicles(List.of("https://swapi.dev/api/vehicles/14/")) + .created("2014-12-09T13:50:51.644000Z") + .edited("2014-12-20T21:17:56.891000Z") + .url("https://swapi.dev/api/people/1/") + .build(); + + this.searchResultDTO = + SearchResultDTO.builder() + .count(82) + .previous(null) + .results(List.of(personDTO)) + .build(); + } + + @Test + public void ShouldReturnPersonByIdWhenStatusIsOK() { + + when(personService.getPersonById(1)).thenReturn(Mono.just(personDTO)); + + webTestClient + .get() + .uri(PATH + "/1") + .exchange() + .expectStatus() + .isOk() + .expectBody(PersonDTO.class) + .isEqualTo(personDTO); + + verify(personService).getPersonById(1); + } + + @Test + public void ShouldTReturn404ErrorWhenEmptyPerson() { + + when(personService.getPersonById(100)) + .thenReturn(Mono.error(WebClientResponseException.create(404, "", null, null, null))); + + webTestClient.get().uri(PATH + "/100").exchange().expectStatus().isNotFound(); + + verify(personService).getPersonById(100); + } + + @Test + public void ShouldReturnAllPersonWhenPageIdNotEmpty() { + + shouldReturnAllPeopleWhenStatusOK(1); + } + + @Test + public void ShouldReturnAllPersonWhenEmptyPageId() { + + shouldReturnAllPeopleWhenStatusOK(null); + } + + /** + * Test to retrieve all people when status is OK with a pageId param. If pageId is null, then + * default page number is 1 + * + * @param pageId Page number for the pagination + */ + private void shouldReturnAllPeopleWhenStatusOK(Integer pageId) { + + when(personService.getPeople(1)).thenReturn(Mono.just(searchResultDTO)); + + webTestClient + .get() + .uri( + uriBuilder -> { + var builder = uriBuilder.path(PATH); + if (pageId != null) { + builder.queryParam("page", pageId); + } + return builder.build(); + }) + .exchange() + .expectStatus() + .isOk() + .expectBody(new ParameterizedTypeReference>() {}) + .isEqualTo(searchResultDTO); + + verify(personService).getPeople(1); + } + + @Test + public void ShouldReturn404ErrorWhenSearchResultisNotFound() { + + when(personService.getPeople(1)) + .thenReturn(Mono.error(WebClientResponseException.create(404, "", null, null, null))); + + webTestClient + .get() + .uri(uriBuilder -> uriBuilder.path(PATH).queryParam("page", 1).build()) + .exchange() + .expectStatus() + .isNotFound(); + + verify(personService).getPeople(1); + } + + @Test + public void ShouldReturnPersonByNameWhenStatusIsOk() { + + when(personService.getPersonByName(anyString())).thenReturn(Mono.just(searchResultDTO)); + String name = "Luke SkyWalker"; + webTestClient + .get() + .uri(uriBuilder -> uriBuilder.path(PATH + "/search").queryParam("name", name).build()) + .exchange() + .expectStatus() + .isOk() + .expectBody(new ParameterizedTypeReference>() {}) + .isEqualTo(searchResultDTO); + + verify(personService).getPersonByName(name); + } + + @Test + public void ShouldReturn404ErrorWhenPersonByNameIsNotFound() { + + String name = "Luke SkyWalker"; + + when(personService.getPersonByName(name)) + .thenReturn(Mono.error(WebClientResponseException.create(404, "", null, null, null))); + + webTestClient + .get() + .uri(uriBuilder -> uriBuilder.path(PATH + "/search").queryParam("name", name).build()) + .exchange() + .expectStatus() + .isNotFound(); + + verify(personService).getPersonByName(name); + } +} diff --git a/src/test/java/fr/kevpdev/swapi/controllers/PlanetControllerTest.java b/src/test/java/fr/kevpdev/swapi/controllers/PlanetControllerTest.java index 50ff546..b30ed3a 100644 --- a/src/test/java/fr/kevpdev/swapi/controllers/PlanetControllerTest.java +++ b/src/test/java/fr/kevpdev/swapi/controllers/PlanetControllerTest.java @@ -1,8 +1,12 @@ package fr.kevpdev.swapi.controllers; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import fr.kevpdev.swapi.dtos.PlanetDTO; import fr.kevpdev.swapi.dtos.SearchResultDTO; import fr.kevpdev.swapi.services.PlanetService; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -12,78 +16,70 @@ import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; -import java.util.List; - -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @WebFluxTest(PlanetController.class) class PlanetControllerTest { - private static final String PATH = "/api/v1/planets"; - private PlanetDTO planetDTO; - - private SearchResultDTO searchResultDTO; - - @Autowired - private WebTestClient webTestClient; - - @MockBean - private PlanetService planetService; - - @BeforeEach - void setUp() { - - this.planetDTO = PlanetDTO.builder() - .name("Yavin IV") - .rotationPeriod("24") - .orbitalPeriod("4818") - .diameter("10200") - .climate("temperate, tropical") - .gravity("1 standard") - .terrain("jungle, rainforests") - .surfaceWater("8") - .population("1000") - .residents(List.of()) // Liste vide - .films(List.of("https://swapi.dev/api/films/1/")) - .created("2014-12-10T11:37:19.144000Z") - .edited("2014-12-20T20:58:18.421000Z") - .url("https://swapi.dev/api/planets/3/") - .build(); - - } - - @Test - public void shouldReturnFilmByIdWhenFilmIdValid() { - - when(planetService.getPlanetById(1)).thenReturn(Mono.just(planetDTO)); - - webTestClient.get().uri(PATH+"/1") - .exchange() - .expectStatus().isOk() - .expectBody(PlanetDTO.class) - .isEqualTo(planetDTO); - - verify(planetService).getPlanetById(1); - } - - @Test - public void shouldReturn404ErrorFilmByIdWhenFilmNotFound() { - - when(planetService.getPlanetById(1)).thenReturn( - Mono.error( - WebClientResponseException - .create(404, "Film not Found", null, null, null))); - - webTestClient.get().uri(PATH+"/1") - .exchange() - .expectStatus().isNotFound(); - - verify(planetService).getPlanetById(1); - - } - - /* - TODO... - */ -} \ No newline at end of file + private static final String PATH = "/api/v1/planets"; + private PlanetDTO planetDTO; + + private SearchResultDTO searchResultDTO; + + @Autowired private WebTestClient webTestClient; + + @MockBean private PlanetService planetService; + + @BeforeEach + void setUp() { + + this.planetDTO = + PlanetDTO.builder() + .name("Yavin IV") + .rotationPeriod("24") + .orbitalPeriod("4818") + .diameter("10200") + .climate("temperate, tropical") + .gravity("1 standard") + .terrain("jungle, rainforests") + .surfaceWater("8") + .population("1000") + .residents(List.of()) // Liste vide + .films(List.of("https://swapi.dev/api/films/1/")) + .created("2014-12-10T11:37:19.144000Z") + .edited("2014-12-20T20:58:18.421000Z") + .url("https://swapi.dev/api/planets/3/") + .build(); + } + + @Test + public void shouldReturnFilmByIdWhenFilmIdValid() { + + when(planetService.getPlanetById(1)).thenReturn(Mono.just(planetDTO)); + + webTestClient + .get() + .uri(PATH + "/1") + .exchange() + .expectStatus() + .isOk() + .expectBody(PlanetDTO.class) + .isEqualTo(planetDTO); + + verify(planetService).getPlanetById(1); + } + + @Test + public void shouldReturn404ErrorFilmByIdWhenFilmNotFound() { + + when(planetService.getPlanetById(1)) + .thenReturn( + Mono.error(WebClientResponseException.create(404, "Film not Found", null, null, null))); + + webTestClient.get().uri(PATH + "/1").exchange().expectStatus().isNotFound(); + + verify(planetService).getPlanetById(1); + } + + /* + TODO... + */ +} From 64535a24acfa3ec35c1ce351fc2205aecf7cc4fd Mon Sep 17 00:00:00 2001 From: kevpdev Date: Thu, 8 Oct 2026 10:27:52 +0200 Subject: [PATCH 3/5] refactor(lint): fix the findings of the new static checks Explicit imports, private SearchResultDTO fields behind the Lombok accessors, customOpenApi bean method name, javadoc tag order and TODO comment format. --- src/main/java/fr/kevpdev/swapi/SwaggerConfiguration.java | 2 +- .../java/fr/kevpdev/swapi/controllers/FilmController.java | 6 +++++- .../fr/kevpdev/swapi/controllers/PersonController.java | 6 +++++- .../fr/kevpdev/swapi/controllers/PlanetController.java | 6 +++++- src/main/java/fr/kevpdev/swapi/dtos/SearchResultDTO.java | 8 ++++---- .../fr/kevpdev/swapi/services/impl/PersonServiceImpl.java | 8 ++++++-- .../fr/kevpdev/swapi/controllers/FilmControllerTest.java | 4 +--- .../kevpdev/swapi/controllers/PlanetControllerTest.java | 4 +--- 8 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/main/java/fr/kevpdev/swapi/SwaggerConfiguration.java b/src/main/java/fr/kevpdev/swapi/SwaggerConfiguration.java index be529aa..4185477 100644 --- a/src/main/java/fr/kevpdev/swapi/SwaggerConfiguration.java +++ b/src/main/java/fr/kevpdev/swapi/SwaggerConfiguration.java @@ -9,7 +9,7 @@ public class SwaggerConfiguration { @Bean - public OpenAPI customOpenAPI() { + public OpenAPI customOpenApi() { return new OpenAPI() .info( new Info() diff --git a/src/main/java/fr/kevpdev/swapi/controllers/FilmController.java b/src/main/java/fr/kevpdev/swapi/controllers/FilmController.java index 7b98281..5731fbd 100644 --- a/src/main/java/fr/kevpdev/swapi/controllers/FilmController.java +++ b/src/main/java/fr/kevpdev/swapi/controllers/FilmController.java @@ -4,7 +4,11 @@ import fr.kevpdev.swapi.dtos.SearchResultDTO; import fr.kevpdev.swapi.services.FilmService; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; @RestController diff --git a/src/main/java/fr/kevpdev/swapi/controllers/PersonController.java b/src/main/java/fr/kevpdev/swapi/controllers/PersonController.java index 9cdee3e..83a128d 100644 --- a/src/main/java/fr/kevpdev/swapi/controllers/PersonController.java +++ b/src/main/java/fr/kevpdev/swapi/controllers/PersonController.java @@ -4,7 +4,11 @@ import fr.kevpdev.swapi.dtos.SearchResultDTO; import fr.kevpdev.swapi.services.PersonService; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; @RestController diff --git a/src/main/java/fr/kevpdev/swapi/controllers/PlanetController.java b/src/main/java/fr/kevpdev/swapi/controllers/PlanetController.java index fc12302..51fc131 100644 --- a/src/main/java/fr/kevpdev/swapi/controllers/PlanetController.java +++ b/src/main/java/fr/kevpdev/swapi/controllers/PlanetController.java @@ -4,7 +4,11 @@ import fr.kevpdev.swapi.dtos.SearchResultDTO; import fr.kevpdev.swapi.services.PlanetService; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; @RestController diff --git a/src/main/java/fr/kevpdev/swapi/dtos/SearchResultDTO.java b/src/main/java/fr/kevpdev/swapi/dtos/SearchResultDTO.java index dc8decc..2eb2a5f 100644 --- a/src/main/java/fr/kevpdev/swapi/dtos/SearchResultDTO.java +++ b/src/main/java/fr/kevpdev/swapi/dtos/SearchResultDTO.java @@ -8,8 +8,8 @@ @Builder public class SearchResultDTO { - public Integer count; - public String next; - public String previous; - public List results; + private Integer count; + private String next; + private String previous; + private List results; } diff --git a/src/main/java/fr/kevpdev/swapi/services/impl/PersonServiceImpl.java b/src/main/java/fr/kevpdev/swapi/services/impl/PersonServiceImpl.java index 45c385f..40ec33b 100644 --- a/src/main/java/fr/kevpdev/swapi/services/impl/PersonServiceImpl.java +++ b/src/main/java/fr/kevpdev/swapi/services/impl/PersonServiceImpl.java @@ -1,6 +1,10 @@ package fr.kevpdev.swapi.services.impl; -import fr.kevpdev.swapi.dtos.*; +import fr.kevpdev.swapi.dtos.FilmDTO; +import fr.kevpdev.swapi.dtos.PersonDTO; +import fr.kevpdev.swapi.dtos.SearchResultDTO; +import fr.kevpdev.swapi.dtos.StarshipDTO; +import fr.kevpdev.swapi.dtos.VehicleDTO; import fr.kevpdev.swapi.services.PersonService; import java.net.URI; import java.util.List; @@ -95,10 +99,10 @@ public Mono> getPersonByName(String name) { /** * Retrieves DTO object list by hypermedia link * + * @param DTO type * @param urls hypermedialink list * @param responseType DTO type * @return a Mono containing the Generic DTO object list if found or empty if not - * @param */ public Mono> getResourcesByUrls(List urls, Class responseType) { return Flux.fromIterable(urls) diff --git a/src/test/java/fr/kevpdev/swapi/controllers/FilmControllerTest.java b/src/test/java/fr/kevpdev/swapi/controllers/FilmControllerTest.java index 7456328..89fcd3f 100644 --- a/src/test/java/fr/kevpdev/swapi/controllers/FilmControllerTest.java +++ b/src/test/java/fr/kevpdev/swapi/controllers/FilmControllerTest.java @@ -100,8 +100,6 @@ public void shouldReturn404ErrorFilmByIdWhenFilmNotFound() { verify(filmService).getFilmById(1); } - /* - TODO... - */ + // TODO: complete these tests } diff --git a/src/test/java/fr/kevpdev/swapi/controllers/PlanetControllerTest.java b/src/test/java/fr/kevpdev/swapi/controllers/PlanetControllerTest.java index b30ed3a..545298b 100644 --- a/src/test/java/fr/kevpdev/swapi/controllers/PlanetControllerTest.java +++ b/src/test/java/fr/kevpdev/swapi/controllers/PlanetControllerTest.java @@ -79,7 +79,5 @@ public void shouldReturn404ErrorFilmByIdWhenFilmNotFound() { verify(planetService).getPlanetById(1); } - /* - TODO... - */ + // TODO: complete these tests } From 37fdd64ad84768894a4eed2ddac6d4a80d1a2a5c Mon Sep 17 00:00:00 2001 From: kevpdev Date: Thu, 8 Oct 2026 10:27:52 +0200 Subject: [PATCH 4/5] ci(check): add format, lint, static analysis and coverage to verify Spotless and Checkstyle (google_checks) run in validate, PMD and SpotBugs after compile, JaCoCo checks 40 percent of lines at verify. Measured coverage is 43 percent. --- config/checkstyle-suppressions.xml | 12 +++ pom.xml | 119 +++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 config/checkstyle-suppressions.xml diff --git a/config/checkstyle-suppressions.xml b/config/checkstyle-suppressions.xml new file mode 100644 index 0000000..b146a67 --- /dev/null +++ b/config/checkstyle-suppressions.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index d9394fd..c7847af 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,125 @@ + + com.diffplug.spotless + spotless-maven-plugin + 3.10.3 + + + + + + + + + spotless + validate + + check + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + + com.puppycrawl.tools + checkstyle + 14.3.0 + + + + google_checks.xml + config/checkstyle-suppressions.xml + warning + true + + + + checkstyle + validate + + check + + + + + + org.apache.maven.plugins + maven-pmd-plugin + 3.28.0 + + true + + + + pmd + process-classes + + check + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.10.4.1 + + + spotbugs + process-classes + + check + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.15 + + + jacoco-agent + + prepare-agent + + + + jacoco-report + verify + + report + + + + jacoco-check + verify + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + 0.40 + + + + + + + + org.springframework.boot spring-boot-maven-plugin From ebd6dbb780105e832f155feb95b89aa1054301d1 Mon Sep 17 00:00:00 2001 From: kevpdev Date: Thu, 8 Oct 2026 10:49:35 +0200 Subject: [PATCH 5/5] docs(readme): document the verify chain and the CI jobs --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d950d3..cb2a67e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Bienvenue dans **SWAPI**, une API Spring Boot conçue pour consommer et servir d - [Endpoints](#endpoints) - [Exemples d'utilisation](#exemples-dutilisation) - [Tests](#tests) +- [Qualité et CI](#qualité-et-ci) - [Swagger UI](#swagger-ui) - [Contributions](#contributions) @@ -121,9 +122,23 @@ Réponse : Le projet inclut des tests unitaires et d'intégration utilisant **Spring Boot Starter Test** et **Reactor Test** pour les flux réactifs. Pour exécuter les tests : ```bash -mvn test +sh ./mvnw test ``` +## Qualité et CI + +La commande `sh ./mvnw verify` est la barrière unique, en local comme en CI. Elle enchaîne, dans cet ordre : + +1. **Spotless** (`google-java-format`) : vérifie le format. `sh ./mvnw spotless:apply` le corrige. +2. **Checkstyle** (`google_checks.xml`) : lint. Les exceptions sont dans `config/checkstyle-suppressions.xml`. +3. **PMD** et **SpotBugs** : analyse statique. +4. **Tests** (Surefire, classes `*Test`). +5. **JaCoCo** : rapport dans `target/site/jacoco`, échec sous **40 %** de lignes couvertes. + +Le workflow `.github/workflows/ci.yml` lance cette commande dans le job `check`. Trois autres contrôles tournent sur les pull requests : `security` (Trivy, dépendances), `codeql` (code Java et workflows GitHub Actions) et Dependabot (`github-actions` et `maven`, chaque semaine). La branche `main` exige `check` et `security`. + +Pour des tests d'intégration `*IT`, il faut déclarer `maven-failsafe-plugin` dans le `pom.xml` : sans lui, Maven ignore ces classes sans erreur. + ## Swagger UI Pour visualiser et interagir avec la documentation de l'API, accédez à **Swagger UI** via l'URL suivante :