diff --git a/src/main/kotlin/ch/schulealtendorf/sporttagpsa/business/clazz/ClassManager.kt b/src/main/kotlin/ch/schulealtendorf/sporttagpsa/business/clazz/ClassManager.kt index 9bfe3b83..19949b9d 100644 --- a/src/main/kotlin/ch/schulealtendorf/sporttagpsa/business/clazz/ClassManager.kt +++ b/src/main/kotlin/ch/schulealtendorf/sporttagpsa/business/clazz/ClassManager.kt @@ -70,4 +70,12 @@ interface ClassManager { * @param clazz the class to save */ fun saveClass(clazz: Clazz) + + /** + * A pending participation means, if any participant of the given {@code clazz} + * as no sport. + * + * @return true if the class has pending participation, otherwise false + */ + fun hasPendingParticipation(clazz: Clazz): Boolean } diff --git a/src/main/kotlin/ch/schulealtendorf/sporttagpsa/business/clazz/DefaultClassManager.kt b/src/main/kotlin/ch/schulealtendorf/sporttagpsa/business/clazz/DefaultClassManager.kt index f1f8fe0a..a9ee0e4d 100644 --- a/src/main/kotlin/ch/schulealtendorf/sporttagpsa/business/clazz/DefaultClassManager.kt +++ b/src/main/kotlin/ch/schulealtendorf/sporttagpsa/business/clazz/DefaultClassManager.kt @@ -90,12 +90,17 @@ class DefaultClassManager( TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } - private fun ClazzEntity.pendingParticipation() = competitorRepository.findByClazzName(this.name).any { it.sport == null } + /** + * A pending participation means, if any participant of the given {@code clazz} + * as no sport. + * + * @return true if the class has pending participation, otherwise false + */ + override fun hasPendingParticipation(clazz: Clazz) = competitorRepository.findByClazzName(clazz.name).any { it.sport == null } private fun ClazzEntity.map(): Clazz { return Clazz( name, - Coach(coach.id!!, coach.name), - pendingParticipation()) + Coach(coach.id!!, coach.name)) } } \ No newline at end of file diff --git a/src/main/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/RestModels.kt b/src/main/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/RestModels.kt new file mode 100644 index 00000000..15761bd4 --- /dev/null +++ b/src/main/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/RestModels.kt @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2018 by Nicolas Märchy + * + * This file is part of Sporttag PSA. + * + * Sporttag PSA is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Sporttag PSA is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Sporttag PSA. If not, see . + * + * Diese Datei ist Teil von Sporttag PSA. + * + * Sporttag PSA ist Freie Software: Sie können es unter den Bedingungen + * der GNU General Public License, wie von der Free Software Foundation, + * Version 3 der Lizenz oder (nach Ihrer Wahl) jeder späteren + * veröffentlichten Version, weiterverbreiten und/oder modifizieren. + * + * Sporttag PSA wird in der Hoffnung, dass es nützlich sein wird, aber + * OHNE JEDE GEWÄHRLEISTUNG, bereitgestellt; sogar ohne die implizite + * Gewährleistung der MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK. + * Siehe die GNU General Public License für weitere Details. + * + * Sie sollten eine Kopie der GNU General Public License zusammen mit diesem + * Programm erhalten haben. Wenn nicht, siehe . + * + * + */ + +package ch.schulealtendorf.sporttagpsa.controller.rest + +import javax.validation.constraints.NotNull + +/* + * Because POJOs for Spring Rest Controller needs to have a default parameter + * and we can validate our POJOs with Spring Annotations, we make every property nullable + * and initialize it with null (like in Java where null-safe does not exist). + * + * If a property is required we annotate it with @NotNull, in order to validate it with Spring. + */ + +/** + * @author nmaerchy + * @since 2.0.0 + */ +data class RestClass( + + @NotNull + var name: String? = null, + + @NotNull + var coach: String? = null, + + @NotNull + var pendingParticipation: Boolean? = null +) + +/** + * @author nmaerchy + * @since 2.0.0 + */ +data class RestTown( + + @NotNull + var id: Int? = null, + + @NotNull + var zip: String? = null, + + @NotNull + var name: String? = null +) + +/** + * @author nmaerchy + * @since 2.0.0 + */ +data class RestParticipant( + + @NotNull + var id: Int? = null, + + @NotNull + var surname: String? = null, + + @NotNull + var prename: String? = null, + + @NotNull + var gender: Boolean? = null, + + @NotNull + var birthday: Long? = null, + + @NotNull + var absent: Boolean? = null, + + @NotNull + var address: String? = null, + + @NotNull + var town: RestTown? = null, + + @NotNull + var clazz: RestClass? = null, + + @NotNull + var sport: String? = null +) + +/** + * @author nmaerchy + * @since 2.0.0 + */ +data class RestPutParticipant( + + @NotNull + var surname: String? = null, + + @NotNull + var prename: String? = null, + + @NotNull + var gender: Boolean? = null, + + @NotNull + var birthday: Long? = null, + + @NotNull + var address: String? = null, + + @NotNull + var absent: Boolean? = null +) + +/** + * @author nmaerchy + * @since 2.0.0 + */ +data class RestPatchParticipant( + var town: RestTown? = null, + var clazz: RestClass? = null, + var sport: String? = null +) \ No newline at end of file diff --git a/src/main/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/clazz/ClassController.kt b/src/main/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/clazz/ClassController.kt index b53a4027..e61f4d95 100644 --- a/src/main/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/clazz/ClassController.kt +++ b/src/main/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/clazz/ClassController.kt @@ -38,6 +38,7 @@ package ch.schulealtendorf.sporttagpsa.controller.rest.clazz import ch.schulealtendorf.sporttagpsa.business.clazz.ClassManager import ch.schulealtendorf.sporttagpsa.controller.rest.BadRequestException +import ch.schulealtendorf.sporttagpsa.controller.rest.RestClass import ch.schulealtendorf.sporttagpsa.model.Clazz import org.springframework.http.MediaType import org.springframework.web.bind.annotation.GetMapping @@ -56,12 +57,24 @@ class ClassController( ) { @GetMapping("/classes", produces = [MediaType.APPLICATION_JSON_VALUE]) - fun getAllClasses() = classManager.getAllClasses() + fun getAllClasses(): List { + return classManager.getAllClasses() + .map { it.map() } + } @GetMapping("/class/{class_id}", produces = [MediaType.APPLICATION_JSON_VALUE]) - fun getClass(@PathVariable("class_id") classId: String): Clazz { + fun getClass(@PathVariable("class_id") classId: String): RestClass { + + val clazz = classManager.getClass(classId).orElseThrow { BadRequestException("Could not find class with id '$classId'") } + + return clazz.map() + } - val clazz = classManager.getClass(classId) - return clazz.orElseThrow { BadRequestException("Could not find class with id '$classId'") } + private fun Clazz.map(): RestClass { + return RestClass( + name, + coach.name, + classManager.hasPendingParticipation(this) + ) } } diff --git a/src/main/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/participant/ParticipantController.kt b/src/main/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/participant/ParticipantController.kt index da0bf853..b6af559d 100644 --- a/src/main/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/participant/ParticipantController.kt +++ b/src/main/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/participant/ParticipantController.kt @@ -36,16 +36,154 @@ package ch.schulealtendorf.sporttagpsa.controller.rest.participant -import org.springframework.web.bind.annotation.RestController +import ch.schulealtendorf.sporttagpsa.business.clazz.ClassManager +import ch.schulealtendorf.sporttagpsa.business.participant.ParticipantManager +import ch.schulealtendorf.sporttagpsa.controller.rest.* +import ch.schulealtendorf.sporttagpsa.model.* +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.* +import javax.validation.Valid /** + * Rest controller for the participants. + * * @author nmaerchy * @since 2.0.0 */ @RestController -class ParticipantController() { +class ParticipantController( + private val participantManager: ParticipantManager, + private val classManager: ClassManager +) { + + @GetMapping("/participants", produces = [MediaType.APPLICATION_JSON_VALUE]) + fun getAllParticipants(@RequestParam("class", required = false) clazzName: String?): List { + + if (clazzName == null) { + return participantManager.getAllParticipants().map { it.map() } + } + + val clazz = classManager.getClass(clazzName).orElseThrow { BadRequestException("Could not find class with name '$clazzName'") } + + return participantManager.getAllParticipants(clazz).map { it.map() } + } + + @PostMapping("/participant") + fun addParticipant() { + TODO("This request is not supported yet") + } + + @GetMapping("/participant/{participant_id}") + fun getParticipant(@PathVariable("participant_id") participantId: Int): RestParticipant { + + return participantManager.getParticipant(participantId) + .map { it.map() } + .orElseThrow { BadRequestException("Could not find participant with id '$participantId'") } + } + + @PutMapping("/participant/{participant_id}") + fun updateParticipant(@PathVariable("participant_id") participantId: Int, @Valid @RequestBody restParticipant: RestPutParticipant) { + + val participant = participantManager.getParticipant(participantId) + .orElseThrow { BadRequestException("Could not find participant with id '$participantId'") } + + val updatedParticipant = participant.copy( + surname = restParticipant.surname!!, + prename = restParticipant.prename!!, + gender = Gender(restParticipant.absent!!), + birthday = Birthday(restParticipant.birthday!!), + address = restParticipant.address!!, + absent = restParticipant.absent!! + ) + + participantManager.saveParticipant(updatedParticipant) + } + + @PatchMapping("/participant/{participant_id}") + fun updateParticipant(@PathVariable("participant_id") participantId: Int, @Valid @RequestBody restParticipant: RestPatchParticipant) { + + if (restParticipant.clazz == null && restParticipant.town == null && restParticipant.sport == null) { + throw BadRequestException("Missing either 'clazz', 'town' or 'sport' in request body.") + } + + val participant = participantManager.getParticipant(participantId) + .orElseThrow { BadRequestException("Could not find participant with id '$participantId'") } + + if (restParticipant.clazz != null) { + val clazz = classManager.getClass(restParticipant.clazz!!.name!!) + .orElseThrow { BadRequestException("Could not find class with name''${restParticipant.clazz!!.name}") } + participant.update(clazz) + } + + if (restParticipant.sport != null) { + participant.update(restParticipant.sport!!) + } + + if (restParticipant.town != null) { + participant.update(restParticipant.town!!) + } + } + + private fun Participant.update(sport: String) { + participantManager.saveParticipant( + this.copy( + sport = java.util.Optional.of(sport) + ) + ) + } + + private fun Participant.update(town: RestTown) { + participantManager.saveParticipant( + this.copy( + town = town.map() + ) + ) + } + + private fun Participant.update(clazz: Clazz) { + participantManager.saveParticipant( + this.copy( + clazz = clazz + ) + ) + } + + private fun Participant.map(): RestParticipant { + return RestParticipant( + id, + surname, + prename, + gender.value, + birthday.milliseconds, + absent, + address, + town.map(), + clazz.map(), + sport.orElse(null) + ) + } + + private fun Clazz.map(): RestClass { + return RestClass( + name, + coach.name, + classManager.hasPendingParticipation(this) + ) + } + + private fun Town.map(): RestTown { + return RestTown( + id, + zip, + name + ) + } - fun getAllParticipants(): List { - TODO("not implemented yet") + private fun RestTown.map(): Town { + return Town( + id!!, + zip!!, + name!! + ) } } \ No newline at end of file diff --git a/src/main/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/participant/RestModels.kt b/src/main/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/participant/RestModels.kt deleted file mode 100644 index 6cb6907a..00000000 --- a/src/main/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/participant/RestModels.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2018 by Nicolas Märchy - * - * This file is part of Sporttag PSA. - * - * Sporttag PSA is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Sporttag PSA is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Sporttag PSA. If not, see . - * - * Diese Datei ist Teil von Sporttag PSA. - * - * Sporttag PSA ist Freie Software: Sie können es unter den Bedingungen - * der GNU General Public License, wie von der Free Software Foundation, - * Version 3 der Lizenz oder (nach Ihrer Wahl) jeder späteren - * veröffentlichten Version, weiterverbreiten und/oder modifizieren. - * - * Sporttag PSA wird in der Hoffnung, dass es nützlich sein wird, aber - * OHNE JEDE GEWÄHRLEISTUNG, bereitgestellt; sogar ohne die implizite - * Gewährleistung der MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK. - * Siehe die GNU General Public License für weitere Details. - * - * Sie sollten eine Kopie der GNU General Public License zusammen mit diesem - * Programm erhalten haben. Wenn nicht, siehe . - * - * - */ - -package ch.schulealtendorf.sporttagpsa.controller.rest.participant - -import ch.schulealtendorf.sporttagpsa.model.Clazz -import ch.schulealtendorf.sporttagpsa.model.Town - -/** - * Data class representing a participant used in rest requests. - * - * @author nmaerchy - * @since 2.0.0 - */ -data class RestParticipant( - val id: Int, - val surname: String, - val prename: String, - val gender: Boolean, - val birthday: Long, - val absent: Boolean, - val address: String, - val town: Town, - val clazz: Clazz, - val sport: String -) diff --git a/src/main/kotlin/ch/schulealtendorf/sporttagpsa/model/Clazz.kt b/src/main/kotlin/ch/schulealtendorf/sporttagpsa/model/Clazz.kt index a37a1713..cc5c983b 100644 --- a/src/main/kotlin/ch/schulealtendorf/sporttagpsa/model/Clazz.kt +++ b/src/main/kotlin/ch/schulealtendorf/sporttagpsa/model/Clazz.kt @@ -42,8 +42,7 @@ package ch.schulealtendorf.sporttagpsa.model * @author nmaerchy * @since 2.0.0 */ -data class Clazz @JvmOverloads constructor( +data class Clazz( val name: String, - val coach: Coach, - val pendingParticipation: Boolean = false + val coach: Coach ) diff --git a/src/test/kotlin/ch/schulealtendorf/sporttagpsa/business/clazz/DefaultClassManagerSpec.kt b/src/test/kotlin/ch/schulealtendorf/sporttagpsa/business/clazz/DefaultClassManagerSpec.kt index 176da9f1..5c02ee6e 100644 --- a/src/test/kotlin/ch/schulealtendorf/sporttagpsa/business/clazz/DefaultClassManagerSpec.kt +++ b/src/test/kotlin/ch/schulealtendorf/sporttagpsa/business/clazz/DefaultClassManagerSpec.kt @@ -36,7 +36,8 @@ package ch.schulealtendorf.sporttagpsa.business.clazz -import ch.schulealtendorf.sporttagpsa.entity.* +import ch.schulealtendorf.sporttagpsa.entity.CompetitorEntity +import ch.schulealtendorf.sporttagpsa.entity.SportEntity import ch.schulealtendorf.sporttagpsa.model.Clazz import ch.schulealtendorf.sporttagpsa.model.Coach import ch.schulealtendorf.sporttagpsa.repository.ClazzRepository @@ -46,8 +47,12 @@ import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.reset import com.nhaarman.mockito_kotlin.whenever import org.jetbrains.spek.api.Spek -import org.jetbrains.spek.api.dsl.* -import kotlin.test.assertEquals +import org.jetbrains.spek.api.dsl.context +import org.jetbrains.spek.api.dsl.describe +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import kotlin.test.assertFalse +import kotlin.test.assertTrue /** * @author nmaerchy @@ -65,17 +70,12 @@ object DefaultClassManagerSpec: Spek({ val running = SportEntity("Running") - val classes: List = listOf( - ClazzEntity("2a", CoachEntity(1, "Max Muster")), - ClazzEntity("2b", CoachEntity(2, "Max Master")), - ClazzEntity("2c", CoachEntity(3, "Max Mister")) - ) beforeEachTest { reset(mockClassRepository, mockClassRepository) } - context("get all classes") { + context("has pending participation") { on("no pending participation") { @@ -86,21 +86,15 @@ object DefaultClassManagerSpec: Spek({ CompetitorEntity().apply { sport = running } ) - whenever(mockClassRepository.findAll()).thenReturn(classes) whenever(mockCompetitorRepository.findByClazzName(any())).thenReturn(competitors) - val result = classManager.getAllClasses() + val clazz = Clazz("2a", Coach(1, "Müller")) + val result = classManager.hasPendingParticipation(clazz) - it("should return a list of classes with no pending participation") { - - val expected: List = listOf( - Clazz("2a", Coach(1, "Max Muster")), - Clazz("2b", Coach(2, "Max Master")), - Clazz("2c", Coach(3, "Max Mister")) - ) - assertEquals(expected, result) + it("should return false") { + assertFalse(result) } } @@ -113,21 +107,15 @@ object DefaultClassManagerSpec: Spek({ CompetitorEntity() // one competitor has no sport so the class has pending participation ) - whenever(mockClassRepository.findAll()).thenReturn(classes) whenever(mockCompetitorRepository.findByClazzName(any())).thenReturn(competitors) - val result = classManager.getAllClasses() + val clazz = Clazz("2a", Coach(1, "Müller")) + val result = classManager.hasPendingParticipation(clazz) it("should return a list of classes with pending participation") { - - val expected: List = listOf( - Clazz("2a", Coach(1, "Max Muster"), true), - Clazz("2b", Coach(2,"Max Master"), true), - Clazz("2c", Coach(3, "Max Mister"), true) - ) - assertEquals(expected, result) + assertTrue(result) } } } diff --git a/src/test/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/participant/ParticipantControllerSpec.kt b/src/test/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/participant/ParticipantControllerSpec.kt new file mode 100644 index 00000000..206ef133 --- /dev/null +++ b/src/test/kotlin/ch/schulealtendorf/sporttagpsa/controller/rest/participant/ParticipantControllerSpec.kt @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2018 by Nicolas Märchy + * + * This file is part of Sporttag PSA. + * + * Sporttag PSA is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Sporttag PSA is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Sporttag PSA. If not, see . + * + * Diese Datei ist Teil von Sporttag PSA. + * + * Sporttag PSA ist Freie Software: Sie können es unter den Bedingungen + * der GNU General Public License, wie von der Free Software Foundation, + * Version 3 der Lizenz oder (nach Ihrer Wahl) jeder späteren + * veröffentlichten Version, weiterverbreiten und/oder modifizieren. + * + * Sporttag PSA wird in der Hoffnung, dass es nützlich sein wird, aber + * OHNE JEDE GEWÄHRLEISTUNG, bereitgestellt; sogar ohne die implizite + * Gewährleistung der MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK. + * Siehe die GNU General Public License für weitere Details. + * + * Sie sollten eine Kopie der GNU General Public License zusammen mit diesem + * Programm erhalten haben. Wenn nicht, siehe . + * + * + */ + +package ch.schulealtendorf.sporttagpsa.controller.rest.participant + +import ch.schulealtendorf.sporttagpsa.business.clazz.ClassManager +import ch.schulealtendorf.sporttagpsa.business.participant.ParticipantManager +import ch.schulealtendorf.sporttagpsa.controller.rest.BadRequestException +import ch.schulealtendorf.sporttagpsa.controller.rest.RestClass +import ch.schulealtendorf.sporttagpsa.controller.rest.RestPatchParticipant +import ch.schulealtendorf.sporttagpsa.controller.rest.RestTown +import ch.schulealtendorf.sporttagpsa.model.* +import com.nhaarman.mockito_kotlin.* +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.context +import org.jetbrains.spek.api.dsl.describe +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +/** + * @author nmaerchy + * @since 2.0.0 + */ +object ParticipantControllerSpec: Spek({ + + describe("a participant rest controller") { + + val mockParticipantManager: ParticipantManager = mock() + val mockClassManager: ClassManager = mock() + + val controller = ParticipantController(mockParticipantManager, mockClassManager) + + + val participant = Participant( + 1, + "Muster", + "Max", + Gender.male(), + Birthday(0), + false, + "", + Town(1, "3000", "Bern"), + Clazz("2a", Coach(1, "Müller")) + ) + + beforeEachTest { + reset(mockParticipantManager, mockClassManager) + } + + context("patch a participant") { + + on("given town to update") { + + whenever(mockParticipantManager.getParticipant(any())).thenReturn(Optional.of(participant)) + + + val town = RestTown(2, "8000", "Zürich") + val patchParticipant = RestPatchParticipant(town) + controller.updateParticipant(1, patchParticipant) + + + it("should update the town of the participant") { + val expected = participant.copy( + town = Town(2, "8000", "Zürich") + ) + verify(mockParticipantManager, times(1)).saveParticipant(expected) + } + } + + on("given class to update") { + + whenever(mockParticipantManager.getParticipant(any())).thenReturn(Optional.of(participant)) + + val clazz = Clazz("3a", Coach(2, "Muster")) + whenever(mockClassManager.getClass(any())).thenReturn(Optional.of(clazz)) + + + val restClass = RestClass("3a", "Muster", false) + val patchParticipant = RestPatchParticipant(clazz = restClass) + controller.updateParticipant(1, patchParticipant) + + + it("should update the class of the participant") { + val expected = participant.copy( + clazz = clazz + ) + verify(mockParticipantManager, times(1)).saveParticipant(expected) + } + } + + on("given sport to update") { + + whenever(mockParticipantManager.getParticipant(any())).thenReturn(Optional.of(participant)) + + val patchParticipant = RestPatchParticipant(sport = "Running") + controller.updateParticipant(1, patchParticipant) + + + it("should update the sport of the participant") { + val expected = participant.copy( + sport = Optional.of("Running") + ) + verify(mockParticipantManager, times(1)).saveParticipant(expected) + } + } + + on("nothing is given") { + + it("should throw a bad request exception") { + val exception = assertFailsWith { + controller.updateParticipant(1, RestPatchParticipant()) + } + assertEquals("Missing either 'clazz', 'town' or 'sport' in request body.", exception.message) + } + } + } + } +})