From e360489257b9702e4f21a28b5b69349b6749d1ca Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 15 Jun 2020 00:27:55 +0200 Subject: [PATCH 1/2] Add users validation --- api/pom.xml | 5 ++ api/src/routing/UserController.kt | 11 ++-- api/src/validation/ValidationExtensions.kt | 14 +++++ api/src/validation/user/UserValidation.kt | 18 ++++++ .../routing/UserControllerKtTest.kt | 7 ++- .../services/UserServiceTest.kt | 2 +- .../unit/validation/RegisterValidationTest.kt | 57 +++++++++++++++++++ api/test/utils/ValidationExtensions.kt | 6 ++ 8 files changed, 109 insertions(+), 11 deletions(-) create mode 100644 api/src/validation/ValidationExtensions.kt create mode 100644 api/src/validation/user/UserValidation.kt rename api/test/{ => integration}/routing/UserControllerKtTest.kt (95%) rename api/test/{ => integration}/services/UserServiceTest.kt (98%) create mode 100644 api/test/unit/validation/RegisterValidationTest.kt create mode 100644 api/test/utils/ValidationExtensions.kt diff --git a/api/pom.xml b/api/pom.xml index 40e5d0b..3df2ab1 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -160,6 +160,11 @@ hoplite-yaml ${hoplite_version} + + am.ik.yavi + yavi + 0.4.0 + com.github.javafaker javafaker diff --git a/api/src/routing/UserController.kt b/api/src/routing/UserController.kt index bbdbea9..4205f85 100644 --- a/api/src/routing/UserController.kt +++ b/api/src/routing/UserController.kt @@ -4,6 +4,8 @@ import be.vandewalleh.entities.User import be.vandewalleh.extensions.respondStatus import be.vandewalleh.extensions.userId import be.vandewalleh.services.UserService +import be.vandewalleh.validation.receiveValidated +import be.vandewalleh.validation.user.registerValidator import io.ktor.application.* import io.ktor.auth.* import io.ktor.http.* @@ -18,14 +20,9 @@ import java.time.LocalDateTime fun Routing.user(kodein: Kodein) { val userService by kodein.instance() - post("/user/test") { - val user = call.receive() - call.respond(user) - } - route("/user") { post { - val user = call.receive() + val user = call.receiveValidated(registerValidator) if (userService.userExists(user.username, user.email)) return@post call.respondStatus(HttpStatusCode.Conflict) @@ -39,7 +36,7 @@ fun Routing.user(kodein: Kodein) { authenticate { put { - val user = call.receive() + val user = call.receiveValidated(registerValidator) if (userService.userExists(user.username, user.email)) return@put call.respond(HttpStatusCode.Conflict) diff --git a/api/src/validation/ValidationExtensions.kt b/api/src/validation/ValidationExtensions.kt new file mode 100644 index 0000000..1d00401 --- /dev/null +++ b/api/src/validation/ValidationExtensions.kt @@ -0,0 +1,14 @@ +package be.vandewalleh.validation + +import am.ik.yavi.core.Validator +import am.ik.yavi.core.ViolationDetail +import io.ktor.application.* +import io.ktor.request.* + +suspend inline fun ApplicationCall.receiveValidated(validator: Validator): T { + val value: T = receive() + validator.validate(value).throwIfInvalid { ValidationException(it.details()) } + return value +} + +data class ValidationException(val details: List) : RuntimeException() diff --git a/api/src/validation/user/UserValidation.kt b/api/src/validation/user/UserValidation.kt new file mode 100644 index 0000000..6f2063e --- /dev/null +++ b/api/src/validation/user/UserValidation.kt @@ -0,0 +1,18 @@ +package be.vandewalleh.validation.user + +import am.ik.yavi.builder.ValidatorBuilder +import am.ik.yavi.builder.konstraint +import am.ik.yavi.core.Validator +import be.vandewalleh.entities.User + +val registerValidator: Validator = ValidatorBuilder.of() + .konstraint(User::username) { + notNull().lessThanOrEqual(50).greaterThanOrEqual(3) + } + .konstraint(User::email) { + notNull().notEmpty().lessThanOrEqual(255).email() + } + .konstraint(User::password) { + notNull().greaterThanOrEqual(6) + } + .build() diff --git a/api/test/routing/UserControllerKtTest.kt b/api/test/integration/routing/UserControllerKtTest.kt similarity index 95% rename from api/test/routing/UserControllerKtTest.kt rename to api/test/integration/routing/UserControllerKtTest.kt index 8e521fc..04fe9d1 100644 --- a/api/test/routing/UserControllerKtTest.kt +++ b/api/test/integration/routing/UserControllerKtTest.kt @@ -1,4 +1,4 @@ -package routing +package integration.routing import be.vandewalleh.auth.SimpleJWT import be.vandewalleh.entities.User @@ -70,7 +70,7 @@ class UserControllerKtTest { val res = testEngine.post("/user") { json { it["username"] = "new" - it["password"] = "test" + it["password"] = "test123abc" it["email"] = "new@test.com" } } @@ -84,7 +84,7 @@ class UserControllerKtTest { json { it["username"] = "existing" it["email"] = "existing@test.com" - it["password"] = "test" + it["password"] = "test123abc" } } res.status() `should be equal to` HttpStatusCode.Conflict @@ -129,6 +129,7 @@ class UserControllerKtTest { json { it["username"] = "ThisIsMyNewName" it["email"] = "ThisIsMyNewName@mail.com" + it["password"] = "ThisIsMyCurrentPassword" } } diff --git a/api/test/services/UserServiceTest.kt b/api/test/integration/services/UserServiceTest.kt similarity index 98% rename from api/test/services/UserServiceTest.kt rename to api/test/integration/services/UserServiceTest.kt index d9a6c57..4cb9b4c 100644 --- a/api/test/services/UserServiceTest.kt +++ b/api/test/integration/services/UserServiceTest.kt @@ -1,4 +1,4 @@ -package services +package integration.services import be.vandewalleh.mainModule import be.vandewalleh.migrations.Migration diff --git a/api/test/unit/validation/RegisterValidationTest.kt b/api/test/unit/validation/RegisterValidationTest.kt new file mode 100644 index 0000000..b2ea374 --- /dev/null +++ b/api/test/unit/validation/RegisterValidationTest.kt @@ -0,0 +1,57 @@ +package unit.validation + +import be.vandewalleh.entities.User +import be.vandewalleh.validation.user.registerValidator +import org.amshove.kluent.* +import org.junit.jupiter.api.* +import utils.firstInvalid + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class RegisterValidationTest { + + @Test + fun `valid register test`() { + val violations = registerValidator.validate(User { + username = "hubert" + password = "definitelyNotMyPassword" + email = "test@mail.com" + }) + + violations.isValid `should be equal to` true + } + + @Test + fun `invalid email test`() { + val violations = registerValidator.validate(User { + username = "hubert" + password = "definitelyNotMyPassword" + email = "teom" + }) + + violations.isValid `should be equal to` false + violations.firstInvalid `should be equal to` "email" + } + + @Test + fun `missing email test`() { + val violations = registerValidator.validate(User { + username = "hubert" + password = "definitelyNotMyPassword" + }) + + violations.isValid `should be equal to` false + violations.firstInvalid `should be equal to` "email" + } + + @Test + fun `username too long test`() { + val violations = registerValidator.validate(User { + username = "6X9iboWmEOWjVjkO328ReTJ1gGPTTmB/ZGgBLhB6EzAJoWkJht8" + password = "definitelyNotMyPassword" + email = "test@mail.com" + }) + + violations.isValid `should be equal to` false + violations.firstInvalid `should be equal to` "username" + } +} diff --git a/api/test/utils/ValidationExtensions.kt b/api/test/utils/ValidationExtensions.kt new file mode 100644 index 0000000..91e461a --- /dev/null +++ b/api/test/utils/ValidationExtensions.kt @@ -0,0 +1,6 @@ +package utils + +import am.ik.yavi.core.ConstraintViolations + +val ConstraintViolations.firstInvalid: Any? + get() = this.violations().firstOrNull()?.name() From 6fa41569826ce5e5b957ce371d4abda7b2e5741a Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Mon, 15 Jun 2020 00:34:29 +0200 Subject: [PATCH 2/2] Respond with validation details --- api/src/features/ErrorFeature.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/api/src/features/ErrorFeature.kt b/api/src/features/ErrorFeature.kt index c901e7e..ee59c87 100644 --- a/api/src/features/ErrorFeature.kt +++ b/api/src/features/ErrorFeature.kt @@ -1,5 +1,7 @@ package be.vandewalleh.features +import am.ik.yavi.core.ViolationDetail +import be.vandewalleh.validation.ValidationException import io.ktor.application.* import io.ktor.features.* import io.ktor.http.* @@ -11,5 +13,13 @@ fun Application.handleErrors() { exception { call.respond(HttpStatusCode.BadRequest) } + exception { + val error = ViolationError(it.details[0]) + call.respond(HttpStatusCode.BadRequest, error) + } } -} \ No newline at end of file +} + +class ViolationError(detail: ViolationDetail) { + val msg = detail.defaultMessage +}