Nantes Université

Skip to content
Extraits de code Groupes Projets
Non vérifiée Valider eb9bfdcf rédigé par Thibault Duperron's avatar Thibault Duperron Validation de GitHub
Parcourir les fichiers

Td2 (#1)

* init td2

* init 20

* clean

* TD2 + init TD3

* revert
parent c6d02c0f
Aucune branche associée trouvée
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
Affichage de
avec 679 ajouts et 1 suppression
== TD2
== Exo 20
Ajouter une validation pour que l'appel au endpoint GET /api/v1/pets/{petId}
retourne un code 400 si petId est inférieur ou égal à 0
La classe de test Exo20 de PetControllerTest doit passer sans y apporter de modifications
== Exo 21
Ajouter une validation pour que l'appel au endpoint POST /api/v1/pets
retourne un code 400 si le nom est vide ou si l'âge est inférieur à 0
La classe de test Exo21 de PetControllerTest doit passer sans y apporter de modifications
== Exo 22
Créer une meta annotation pour que les appels GET et PUT partagent la même règle de validation du petId en dans l'url
Les classes de test Exo20 et Exo22 de PetControllerTest doivent passer sans y apporter de modifications
== Exo 23
Les "requests params" peuvent être groupés dans un objet.
Il ne faut pas lui ajouter d'annotation (même pas @RequestParam) sans lui fournir d'annotation.
Créer une classe AgeRange pour remplacer les paramètres de GET /api/v1/pets
La classe de test Exo23 de PetControllerTest doit passer sans y apporter de modifications
== Exo 24
Créer une annotation @ValidAgeRange pour valider que l'âge minimum est inférieur à l'âge maximum s'ils sont fournis
Les classes de test Exo23 et Exo24 de PetControllerTest doivent passer sans y apporter de modifications
== Exo 25
A l'aide d'un "controller advice", renvoyer une erreur 418 en cas d'exception ImATeapotException
La classe de test Exo25 de PetControllerTest doit passer sans y apporter de modifications
== Exo 26
Dans le controller advice ErrorHandler, qui étend la gestion des erreurs de spring,
remplacer le retour en cas d'erreur de validation par une réponse sous la forme:
[source,json]
----
{
"status": 400,
"message": "message de l'exception"
}
----
La classe de test Exo26 de PetControllerTest doit passer sans y apporter de modifications
== Exo 27
Ajouter le fichier src/main/resources/application.yml
[source,yml]
----
custom:
app-name: "mon application"
app-version: "1.0.1"
git:
branch: "main"
commit: "123456"
----
Ajouter un fichier banner.txt dans src/main/resources avec le contenu
[source]
----
${custom.app-name} ${custom.app-version} ${custom.git.branch} ${custom.git.commit}
----
== Exo 28
En utilisant des @Value, créer un endpoint GET /api/v1/info qui renvoie la réponse suivante en json
[source,json]
----
{
"app-name": "mon application",
"app-version": "1.0",
"git": {
"branch": "master",
"commit": "123456"
}
}
----
La classe de test Exo28 de InfoTest doit passer sans y apporter de modifications
== Exo 29
Remplacer l'utilisation des @Value par un l'approche avec un @ConfigurationProperties
La classe de test Exo28 de InfoTest doit toujours passer sans y apporter de modifications
== Exo 30
Ajouter le fichier src/main/resources/application-dev.yml
[source,yml]
----
custom:
app-version: "1.0.2-SNAPSHOT"
git:
branch: "feature/TICKET"
commit: "654321"
----
La classe de test Exo30 de InfoTest doit passer sans y apporter de modifications
== Exo 31
Ajouter un paramètre variable "maxRange" dans l'annotation @ValidAgeRange.
Modifier la validation pour garantir que l'écart entre l'âge minimum et l'âge maximum est inférieur à ce max.
== Exo 32
Spring s'occupe de la création du validator,
on peut y injecter le max-range depuis le fichier de configuration (custom.api.pets.max-range=100)
Utiliser un @Value pour le faire.
/!\ Dans ce contexte on ne peut pas utiliser lateinit.
Il faut fournir une valeur par défaut directement sur le champ.
\ No newline at end of file
......@@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.http.RequestEntity.post
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post
......
plugins {
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
id("org.springframework.boot") version "3.3.5"
id("io.spring.dependency-management") version "1.1.6"
kotlin("plugin.jpa") version "1.9.25"
}
group = "iut.nantes"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
runtimeOnly("com.h2database:h2")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.28.1")
testImplementation("io.mockk:mockk:1.13.12")
testImplementation("com.ninja-squad:springmockk:4.0.2")
}
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
package iut.nantes.exo20
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class Exo20Application
fun main(args: Array<String>) {
runApplication<Exo20Application>(*args)
}
package iut.nantes.exo20.config
import jakarta.validation.ConstraintViolationException
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatusCode
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
@ControllerAdvice
class ErrorHandler: ResponseEntityExceptionHandler() {
@ExceptionHandler(ConstraintViolationException::class)
fun handleConstraintViolation(e: ConstraintViolationException) = ResponseEntity.badRequest().body("Failure: ${e.message}")
@ExceptionHandler(Exception::class)
fun fallback(e: Exception) = ResponseEntity.internalServerError().body("Failure: ${e.message}")
}
\ No newline at end of file
package iut.nantes.exo20.controller
import iut.nantes.exo20.errors.ImATeapotException
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
class PetController(val database : MutableMap<Int, PetDto> = mutableMapOf()){
@GetMapping("/api/v1/pets/{petId}")
fun getPet(@PathVariable petId: Int) = database[petId]?.let {
ResponseEntity.ok(it)
} ?: ResponseEntity.notFound().build()
@PostMapping("/api/v1/pets")
fun createPet(@RequestBody pet: PetDto): ResponseEntity<PetDto> {
val next = (database.keys.maxOrNull() ?: 0) + 1
val withId = pet.copy(id = next)
database[next] = withId
return ResponseEntity.status(HttpStatus.CREATED).body(withId)
}
@PutMapping("/api/v1/pets/{petId}")
fun updatePet(@RequestBody pet: PetDto, @PathVariable petId: Int): ResponseEntity<PetDto> {
if (pet.id != petId) {
throw IllegalArgumentException("Pet ID in path and body do not match")
}
val previous = database[petId]
if (previous == null) {
throw ImATeapotException()
} else {
database[petId] = pet
}
return ResponseEntity.ok(pet)
}
@GetMapping("/api/v1/pets")
fun getPets(@RequestParam minAge: Int?, @RequestParam maxAge: Int?): ResponseEntity<List<PetDto>> {
var result: List<PetDto> = database.values.toList()
if (minAge != null) result = result.filter { it.age >= minAge }
if (maxAge != null) result = result.filter { it.age <= maxAge }
return ResponseEntity.ok(result)
}
}
\ No newline at end of file
package iut.nantes.exo20.controller
data class PetDto(val id: Int?, val name: String, val age: Int, val kind: PetKind) {
}
enum class PetKind {
CAT, DOG, FISH, OCTOPUS
}
\ No newline at end of file
package iut.nantes.exo20.errors
class ImATeapotException : RuntimeException() {
}
spring.application.name: exo20
spring:
r2dbc:
url: jdbc:h2:mem:///test;MODE=PostgreSQL
flyway:
enabled: true
url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
h2:
console:
enabled: true
package iut.nantes.exo20.controller
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.context.annotation.Profile
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
class InfoTest{
@Nested
@WebMvcTest
inner class Exo28 {
@Autowired
lateinit var mockMvc: MockMvc
@Test
fun `happy path`() {
mockMvc.get("/api/v1/info")
.andExpect {
status { isOk() }
content { contentType("application/json") }
jsonPath("$.appName") { value("mon application") }
jsonPath("$.appVersion") { value("1.0.1") }
jsonPath("$.git.branch") { value("main") }
jsonPath("$.git.commit") { value("123456") }
}
}
}
@Nested
@ActiveProfiles("dev")
@WebMvcTest
inner class Exo30 {
@Autowired
lateinit var mockMvc: MockMvc
@Test
fun `happy path`() {
mockMvc.get("/api/v1/info")
.andExpect {
status { isOk() }
content { contentType("application/json") }
jsonPath("$.appName") { value("mon application") }
jsonPath("$.appVersion") { value("1.0.2-SNAPSHOT") }
jsonPath("$.git.branch") { value("feature/TICKET") }
jsonPath("$.git.commit") { value("654321") }
}
}
}
}
\ No newline at end of file
package iut.nantes.exo20.controller
import org.hamcrest.Matchers.startsWith
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post
import org.springframework.test.web.servlet.put
import kotlin.test.Test
@WebMvcTest
class PetControllerTest {
@Autowired
lateinit var mockMvc: MockMvc
@Autowired
lateinit var petController: PetController
@BeforeEach
fun setup() {
val database = petController.database // in real production code, database should be private
database.clear()
database[1] = PetDto(1, "Kraken", 2008, PetKind.OCTOPUS)
database[2] = PetDto(2, "Maurice", 1, PetKind.FISH)
}
@Nested
inner class Exo20 {
@Test
fun `happy path`() {
mockMvc.get("/api/v1/pets/1")
.andExpect {
status { isOk() }
content { contentType("application/json") }
jsonPath("$.name") { value("Kraken") }
}
}
@Test
fun `error case`() {
mockMvc.get("/api/v1/pets/0")
.andExpect {
status { isBadRequest() }
}
}
}
@Nested
inner class Exo21 {
@Test
fun `happy path`() {
mockMvc.post("/api/v1/pets") {
contentType = APPLICATION_JSON
content = """
{
"name": "Kraken",
"age": 2008,
"kind": "OCTOPUS"
}
""".trimIndent()
}
.andExpect {
status { isCreated() }
content { contentType("application/json") }
jsonPath("$.name") { value("Kraken") }
}
}
@Test
fun `error case invalid age`() {
mockMvc.post("/api/v1/pets") {
contentType = APPLICATION_JSON
content = """
{
"name": "Egg",
"age": -1,
"kind": "FISH"
}
""".trimIndent()
}
.andExpect {
status { isBadRequest() }
}
}
@Test
fun `error case invalid name`() {
mockMvc.post("/api/v1/pets") {
contentType = APPLICATION_JSON
content = """
{
"name": "",
"age": 1,
"kind": "FISH"
}
""".trimIndent()
}
.andExpect {
status { isBadRequest() }
}
}
}
@Nested
inner class Exo22 {
@Test
fun `happy path`() {
mockMvc.put("/api/v1/pets/1") {
contentType = APPLICATION_JSON
content = """
{
"id": 1,
"name": "Kraken",
"age": 3210,
"kind": "OCTOPUS"
}
""".trimIndent()
}
.andExpect {
status { isOk() }
content { contentType("application/json") }
jsonPath("$.name") { value("Kraken") }
jsonPath("$.age") { value("3210") }
}
}
@Test
fun `invalid path id`() {
mockMvc.put("/api/v1/pets/0") {
contentType = APPLICATION_JSON
content = """
{
"id": 1,
"name": "Kraken",
"age": 3210,
"kind": "OCTOPUS"
}
""".trimIndent()
}
.andExpect {
status { isBadRequest() }
}
}
}
@Nested
inner class Exo23 {
@Test
fun `happy path`() {
mockMvc.get("/api/v1/pets?minAge=2000&maxAge=2010")
.andExpect {
status { isOk() }
content { contentType("application/json") }
jsonPath("$[0].name") { value("Kraken") }
}
}
@Test
fun `max only`() {
mockMvc.get("/api/v1/pets?maxAge=2010")
.andExpect {
status { isOk() }
content { contentType("application/json") }
jsonPath("$[0].name") { value("Kraken") }
jsonPath("$[1].name") { value("Maurice") }
}
}
}
@Nested
inner class Exo24 {
@Test
fun `happy path`() {
mockMvc.get("/api/v1/pets?minAge=2011&maxAge=2010")
.andExpect {
status { isBadRequest() }
}
}
@Test
fun `min equals max`() {
mockMvc.get("/api/v1/pets?minAge=1&maxAge=1")
.andExpect {
status { isOk() }
content { contentType("application/json") }
jsonPath("$[0].name") { value("Maurice") }
}
}
}
@Nested
inner class Exo25 {
@Test
fun `happy path`() {
mockMvc.put("/api/v1/pets/8") {
contentType = APPLICATION_JSON
content = """
{
"id": 8,
"name": "Kraken",
"age": 3210,
"kind": "OCTOPUS"
}
""".trimIndent()
}
.andExpect {
status { isIAmATeapot() }
}
}
}
@Nested
inner class Exo26 {
@Test
fun `error case invalid age`() {
mockMvc.post("/api/v1/pets") {
contentType = APPLICATION_JSON
content = """
{
"name": "Pinkie Pie",
"age": 100,
"kind": "PONY"
}
""".trimIndent()
}.andDo { print() }
.andExpect {
status { isBadRequest() }
content { contentType("application/json") }
jsonPath("$.status") { value("400") }
jsonPath("$.message") { startsWith("JSON parse error") }
jsonPath("$.title") { doesNotExist() }
}
}
}
}
\ No newline at end of file
plugins {
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
id("org.springframework.boot") version "3.3.5"
id("io.spring.dependency-management") version "1.1.6"
kotlin("plugin.jpa") version "1.9.25"
}
group = "iut.nantes"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
runtimeOnly("com.h2database:h2")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.28.1")
testImplementation("io.mockk:mockk:1.13.12")
testImplementation("com.ninja-squad:springmockk:4.0.2")
}
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
package iut.nantes.exo33
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class Exo33Application
fun main(args: Array<String>) {
runApplication<Exo33Application>(*args)
}
package iut.nantes.exo33.config
import jakarta.validation.ConstraintViolationException
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
@ControllerAdvice
class ErrorHandler: ResponseEntityExceptionHandler() {
@ExceptionHandler(ConstraintViolationException::class)
fun handleConstraintViolation(e: ConstraintViolationException) = ResponseEntity.badRequest().body("Failure: ${e.message}")
@ExceptionHandler(Exception::class)
fun fallback(e: Exception) = ResponseEntity.internalServerError().body("Failure: ${e.message}")
}
\ No newline at end of file
spring.application.name: exo33
spring:
r2dbc:
url: jdbc:h2:mem:///test;MODE=PostgreSQL
flyway:
enabled: true
url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
h2:
console:
enabled: true
rootProject.name = "demo"
include("exo1")
include("exo10")
\ No newline at end of file
include("exo10")
include("exo20")
include("exo33")
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter