added further tests to register page
continuous-integration/drone/push Build is passing Details

dev
Override-6 2 years ago
parent f3782f492d
commit bb14aef019

@ -30,6 +30,8 @@ import scala.collection.immutable.HashMap
object Authenticator: object Authenticator:
private final val LOG = LogManager.getLogger("Authentification") private final val LOG = LogManager.getLogger("Authentification")
private final val ValidMailPattern = "^\\w+([.-]?\\w+)*@\\w+([.-]?\\w+)*(\\.\\w{2,3})+$".r private final val ValidMailPattern = "^\\w+([.-]?\\w+)*@\\w+([.-]?\\w+)*(\\.\\w{2,3})+$".r
private final val ValidForenamePattern = "^[a-zA-Z\\u00C0-\\u017F-']+$".r
private final val ValidNamePattern = ValidForenamePattern
private final val ValidPasswordPattern = ".{6,}".r private final val ValidPasswordPattern = ".{6,}".r
case class JwtContent(uuid: UUID) case class JwtContent(uuid: UUID)
@ -85,10 +87,15 @@ class Authenticator(url: URL, key: PublicKey, algorithm: JwtAsymmetricAlgorithm)
val hash = hashPassword(password) val hash = hashPassword(password)
User(uuid, name, forename, hash, mail) User(uuid, name, forename, hash, mail)
} }
if (!ValidMailPattern.matches(mail)) if (!ValidMailPattern.matches(mail))
ZIO.fail(InvalidEmail(s"email address did not satisfy regex pattern $ValidMailPattern")) ZIO.fail(InvalidEmail(s"email address did not satisfy regex pattern"))
else if (!ValidPasswordPattern.matches(password)) else if (!ValidPasswordPattern.matches(password))
ZIO.fail(InvalidPassword(s"password did not satisfy regex pattern $ValidPasswordPattern")) ZIO.fail(InvalidPassword(s"password did not satisfy regex pattern"))
else if (!ValidForenamePattern.matches(forename))
ZIO.fail(InvalidForename(s"email address did not satisfy regex pattern"))
else if (!ValidNamePattern.matches(name))
ZIO.fail(InvalidName(s"password did not satisfy regex pattern"))
else for else for
_ <- findByMail(mail).none.orElse(ZIO.fail(UserAlreadyRegistered(s"an email address already exists for '$mail'"))) _ <- findByMail(mail).none.orElse(ZIO.fail(UserAlreadyRegistered(s"an email address already exists for '$mail'")))
_ <- run(quote { _ <- run(quote {

@ -8,4 +8,10 @@ case class InvalidEmail(msg: String) extends AuthException(msg)
case class UserNotFound(msg: String) extends AuthException(msg) case class UserNotFound(msg: String) extends AuthException(msg)
case class UserAlreadyRegistered(msg: String) extends AuthException(msg) case class UserAlreadyRegistered(msg: String) extends AuthException(msg)
case class InvalidName(msg: String) extends AuthException(msg)
case class InvalidForename(msg: String) extends AuthException(msg)

@ -14,13 +14,7 @@ import scala.jdk.CollectionConverters.*
import scala.language.reflectiveCalls import scala.language.reflectiveCalls
object HandlerUtils { object HandlerUtils {
def parseAttribute[V, T <: Json {def value: V}](json: Json, name: String, cursor: JsonCursor[Json, T]): Task[V] =
ZIO.fromEither(json.get[T](cursor).map(_.value))
.mapError(InvalidRequest(s"Missing or invalid field $name.", _))
def parseAttributeOpt[V, T <: Json {def value: V}](json: Json, name: String, cursor: JsonCursor[Json, T]) =
ZIO.fromEither(json.get[T](cursor).map(_.value)).option
def errorBody(errorType: String, msg: String) = Body.fromString(s"""{"error": "$errorType","msg": "$msg"}""") def errorBody(errorType: String, msg: String) = Body.fromString(s"""{"error": "$errorType","msg": "$msg"}""")

@ -4,7 +4,7 @@ import org.apache.logging.log4j.{LogManager, Logger}
import org.tbasket.auth.Authenticator import org.tbasket.auth.Authenticator
import org.tbasket.data.User import org.tbasket.data.User
import org.tbasket.error.* import org.tbasket.error.*
import org.tbasket.handler.HandlerUtils.{errorBody, parseAttribute, parseRequestForm, search} import org.tbasket.handler.HandlerUtils.{errorBody, parseRequestForm, search}
import zio.ZIO import zio.ZIO
import zio.http.model.{Cookie, Headers, Status} import zio.http.model.{Cookie, Headers, Status}
import zio.http.{Body, Request, Response, model} import zio.http.{Body, Request, Response, model}
@ -38,10 +38,18 @@ object RegisterPageHandler extends PageHandler {
status = Status.ExpectationFailed, status = Status.ExpectationFailed,
body = errorBody("invalid email", msg) body = errorBody("invalid email", msg)
)) ))
case InvalidPassword(msg) => ZIO.attempt(Response( case InvalidPassword(msg) => ZIO.attempt(Response(
status = Status.ExpectationFailed, status = Status.ExpectationFailed,
body = errorBody("invalid password", msg) body = errorBody("invalid password", msg)
)) ))
case InvalidName(msg) => ZIO.attempt(Response(
status = Status.ExpectationFailed,
body = errorBody("invalid name", msg)
))
case InvalidForename(msg) => ZIO.attempt(Response(
status = Status.ExpectationFailed,
body = errorBody("invalid forename", msg)
))
case UserAlreadyRegistered(msg) => case UserAlreadyRegistered(msg) =>
ZIO.attempt(Response( ZIO.attempt(Response(
status = Status.NotAcceptable, status = Status.NotAcceptable,

@ -1,14 +1,23 @@
package org.tbasket.test package org.tbasket.test
import io.netty.handler.codec.http.QueryStringEncoder import io.netty.handler.codec.http.QueryStringEncoder
import org.tbasket.error.InvalidRequest
import zio.* import zio.*
import zio.http.{Body, Response} import zio.http.{Body, Response}
import zio.json.* import zio.json.*
import zio.json.ast.Json import zio.json.ast.{Json, JsonCursor}
import scala.language.implicitConversions import scala.language.{implicitConversions, reflectiveCalls}
object TestUtils { object TestUtils {
def parseAttribute[V, T <: Json {def value: V}](json: Json, cursor: JsonCursor[Json, T]): Task[V] =
ZIO.fromEither(json.get[T](cursor).map(_.value))
.mapError(InvalidRequest(s"Missing or invalid field $cursor.", _))
def parseAttributeOpt[V, T <: Json {def value: V}](json: Json, cursor: JsonCursor[Json, T]) =
ZIO.fromEither(json.get[T](cursor).map(_.value)).option
def getJsonBody(r: Response): Task[Json] = { def getJsonBody(r: Response): Task[Json] = {
for for
body <- r.body.asString body <- r.body.asString

@ -7,9 +7,8 @@ import org.tbasket.data.{Database, DatabaseContext}
import org.tbasket.endpoint.Endpoint import org.tbasket.endpoint.Endpoint
import org.tbasket.endpoint.Endpoint.handle import org.tbasket.endpoint.Endpoint.handle
import org.tbasket.error.* import org.tbasket.error.*
import org.tbasket.handler.HandlerUtils.parseAttribute
import org.tbasket.handler.LoginPageHandler import org.tbasket.handler.LoginPageHandler
import org.tbasket.test.TestUtils.{getJsonBody, makeFormBody} import org.tbasket.test.TestUtils.*
import org.tbasket.test.{TestLayers, TestUtils} import org.tbasket.test.{TestLayers, TestUtils}
import zio.* import zio.*
import zio.http.* import zio.http.*
@ -23,65 +22,65 @@ import zio.test.Assertion.*
object LoginPageHandlerTests extends TBasketPageSpec("/login") { object LoginPageHandlerTests extends TBasketPageSpec("/login") {
private def requestsSpec = suite("bad request tests")(
ZIO.attempt(Map(
"empty packet" -> Body.empty,
"with no mail attribute" -> makeFormBody("password" -> "bouhours"),
"with no password attribute" -> makeFormBody("email" -> "valid.email@not.very"),
"with invalid form data" -> Body.fromString("""this is a corrupted form data""")
)).map(_.map { case (name, body) =>
test(name) {
for
response <- handle(Request.post(body, url))
json <- getJsonBody(response)
errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString)
yield
assert(response)(hasField("status", _.status, equalTo(Status.BadRequest)))
&& assertTrue(errorType == "invalid request")
}
})
)
private def loginSpec = { private def requestsSpec = suite("bad request tests")(
suite("login situation tests")( ZIO.attempt(Map(
test("login with unknown account") { "empty packet" -> Body.empty,
for "with no mail attribute" -> makeFormBody("password" -> "bouhours"),
response <- handle(Request.post(makeFormBody("password" -> "bouhours", "email" -> "unknownaccount@gmail.com"), url)) "with no password attribute" -> makeFormBody("email" -> "valid.email@not.very"),
json <- getJsonBody(response) "with invalid form data" -> Body.fromString("""this is a corrupted form data""")
errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString) )).map(_.map { case (name, body) =>
yield test(name) {
//assert that the response error is of type unauthorized and headers are Location: /register for
assert(response)(hasField("status", _.status, equalTo(Status.Found))) response <- handle(Request.post(body, url))
&& assert(errorType)(equalTo("unauthorized")) json <- getJsonBody(response)
&& assert(response)(hasField("headers", _.headers, contains(Headers.location("/register")))) errorType <- parseAttribute(json, JsonCursor.field("error").isString)
}, yield
assert(response)(hasField("status", _.status, equalTo(Status.BadRequest)))
&& assertTrue(errorType == "invalid request")
}
})
)
private def loginSpec = {
suite("login situation tests")(
test("login with unknown account") {
for
response <- handle(Request.post(makeFormBody("password" -> "bouhours", "email" -> "unknownaccount@gmail.com"), url))
json <- getJsonBody(response)
errorType <- parseAttribute(json, JsonCursor.field("error").isString)
yield
//assert that the response error is of type unauthorized and headers are Location: /register
assert(response)(hasField("status", _.status, equalTo(Status.Found)))
&& assert(errorType)(equalTo("unauthorized"))
&& assert(response)(hasField("headers", _.headers, contains(Headers.location("/register"))))
},
test("login with known account") { test("login with known account") {
for for
response <- handle(Request.post(makeFormBody("password" -> "123456", "email" -> "maximebatista18@gmail.com"), url)) response <- handle(Request.post(makeFormBody("password" -> "123456", "email" -> "maximebatista18@gmail.com"), url))
yield yield
assert(response)(hasField("status", _.status, equalTo(Status.Found))) assert(response)(hasField("status", _.status, equalTo(Status.Found)))
&& assert(response)(hasField("body", _.body, equalTo(Body.empty))) //TODO assert that the cookie name is JWT && assert(response)(hasField("body", _.body, equalTo(Body.empty))) //TODO assert that the cookie name is JWT
&& assert(response)(hasField("headers", _.headers, exists(hasField("key", _.key, equalTo(HeaderNames.setCookie))))) && assert(response)(hasField("headers", _.headers, exists(hasField("key", _.key, equalTo(HeaderNames.setCookie)))))
}, },
test("login with known account wrong password") { test("login with known account wrong password") {
for for
fiber <- handle(Request.post(makeFormBody("password" -> "wrong", "email" -> "maximebatista18@gmail.com"), url)).fork fiber <- handle(Request.post(makeFormBody("password" -> "wrong", "email" -> "maximebatista18@gmail.com"), url)).fork
_ <- TestClock.adjust(1.seconds) _ <- TestClock.adjust(1.seconds)
response <- ZIO.fromFiber(fiber) response <- ZIO.fromFiber(fiber)
json <- getJsonBody(response) json <- getJsonBody(response)
errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString) errorType <- parseAttribute(json, JsonCursor.field("error").isString)
yield yield
assert(response)(hasField("status", _.status, equalTo(Status.Unauthorized))) assert(response)(hasField("status", _.status, equalTo(Status.Unauthorized)))
&& assert(errorType)(equalTo("unauthorized")) && assert(errorType)(equalTo("unauthorized"))
} }
)
}
override def tspec = suite("/login page handler")(
requestsSpec,
loginSpec
) )
}
override def tspec = suite("/login page handler")(
requestsSpec,
loginSpec
)
} }

@ -1,7 +1,6 @@
package org.tbasket.test.pages package org.tbasket.test.pages
import org.tbasket.endpoint.Endpoint.handle import org.tbasket.endpoint.Endpoint.handle
import org.tbasket.handler.HandlerUtils.{parseAttribute, parseAttributeOpt}
import org.tbasket.test.TestUtils import org.tbasket.test.TestUtils
import org.tbasket.test.TestUtils.* import org.tbasket.test.TestUtils.*
import org.tbasket.test.pages.RegisterPageHandlerTests.test import org.tbasket.test.pages.RegisterPageHandlerTests.test
@ -9,8 +8,8 @@ import zio.*
import zio.http.* import zio.http.*
import zio.http.model.{HeaderNames, Headers, Status} import zio.http.model.{HeaderNames, Headers, Status}
import zio.json.ast.JsonCursor import zio.json.ast.JsonCursor
import zio.test.*
import zio.test.Assertion.* import zio.test.Assertion.*
import zio.test.{assert, *}
object RegisterPageHandlerTests extends TBasketPageSpec("/register") { object RegisterPageHandlerTests extends TBasketPageSpec("/register") {
@ -26,7 +25,7 @@ object RegisterPageHandlerTests extends TBasketPageSpec("/register") {
for for
response <- handle(Request.post(body, url)) response <- handle(Request.post(body, url))
json <- getJsonBody(response) json <- getJsonBody(response)
errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString) errorType <- parseAttribute(json, JsonCursor.field("error").isString)
yield yield
assert(response)(hasField("status", _.status, equalTo(Status.BadRequest))) assert(response)(hasField("status", _.status, equalTo(Status.BadRequest)))
&& assertTrue(errorType.startsWith("invalid")) && assertTrue(errorType.startsWith("invalid"))
@ -46,15 +45,51 @@ object RegisterPageHandlerTests extends TBasketPageSpec("/register") {
) )
*> *>
(for (for
resp <- handle(Request.post(makeFormBody("name" -> "tuaillon", "forename" -> "leo", "email" -> "leo.tuaillon@etu.uca.fr", "password" -> "bouhours"), url)) resp <- handle(Request.post(makeFormBody("name" -> "tuaillon", "forename" -> "leo", "email" -> "leo.tuaillon@etu.uca.fr", "password" -> "bouhours"), url))
json <- getJsonBody(resp)
errorType <- parseAttributeOpt(json, JsonCursor.field("error").isString)
yield
assert(resp)(hasField("status", _.status, equalTo(Status.NotAcceptable)))
&& assert(errorType)(isSome(equalTo("already registered")))
&& assert(resp)(hasField("headers", _.headers, contains(Headers.location("/login")))))
},
test("register bad email") {
for
resp <- handle(Request.post(makeFormBody("name" -> "tuaillon", "forename" -> "leo", "email" -> "leo.tuaillonbadmail", "password" -> "bouhours"), url))
json <- getJsonBody(resp)
errorType <- parseAttributeOpt(json, JsonCursor.field("error").isString)
yield
assert(resp)(hasField("status", _.status, equalTo(Status.ExpectationFailed)))
&& assert(errorType)(isSome(equalTo("invalid email")))
},
test("register bad password") {
for
resp <- handle(Request.post(makeFormBody("name" -> "tuaillon", "forename" -> "leo", "email" -> "leo.tuaillon@etu.uca.fr", "password" -> "1234"), url))
json <- getJsonBody(resp) json <- getJsonBody(resp)
errorType <- parseAttributeOpt(json, "error", JsonCursor.field("error").isString) errorType <- parseAttributeOpt(json, JsonCursor.field("error").isString)
yield yield
assert(resp)(hasField("status", _.status, equalTo(Status.NotAcceptable))) assert(resp)(hasField("status", _.status, equalTo(Status.ExpectationFailed)))
&& assert(errorType)(isSome(equalTo("already registered"))) && assert(errorType)(isSome(equalTo("invalid password")))
&& assert(resp)(hasField("headers", _.headers, contains(Headers.location("/login")))))
}, },
test("register bad name") {
for
resp <- handle(Request.post(makeFormBody("name" -> "", "forename" -> "leo", "email" -> "leo.tuaillon@etu.uca.fr", "password" -> "bouhours"), url))
json <- getJsonBody(resp)
errorType <- parseAttributeOpt(json, JsonCursor.field("error").isString)
yield
assert(resp)(hasField("status", _.status, equalTo(Status.ExpectationFailed)))
&& assert(errorType)(isSome(equalTo("invalid name")))
},
test("register bad forename") {
for
resp <- handle(Request.post(makeFormBody("name" -> "tuaillon", "forename" -> "", "email" -> "leo.tuaillon@etu.uca.fr", "password" -> "bouhours"), url))
json <- getJsonBody(resp)
errorType <- parseAttributeOpt(json, JsonCursor.field("error").isString)
yield
assert(resp)(hasField("status", _.status, equalTo(Status.ExpectationFailed)))
&& assert(errorType)(isSome(equalTo("invalid forename")))
}
) )
override def tspec = suite("/register page handler")( override def tspec = suite("/register page handler")(