From 8aa3c0a39a7c56b8fbfd009af63d3bcde170e5fa Mon Sep 17 00:00:00 2001 From: Override-6 Date: Wed, 1 Feb 2023 23:22:54 +0100 Subject: [PATCH] reviewed error handling and tests --- .../ExternalBasketServerException.scala | 7 - .../InternalBasketServerException.scala | 7 - Core/src/org/tbasket/auth/Authenticator.scala | 34 ++-- Core/src/org/tbasket/endpoint/Endpoint.scala | 123 +++++++++----- .../src/org/tbasket/error/AuthException.scala | 17 +- .../src/org/tbasket/error/ExceptionEnum.scala | 5 - Core/src/org/tbasket/error/JwtException.scala | 18 +- .../org/tbasket/error/OtherExceptions.scala | 18 ++ .../org/tbasket/error/RegularException.scala | 9 - .../src/org/tbasket/error/UserException.scala | 10 ++ .../org/tbasket/handler/HandlerUtils.scala | 5 +- .../tbasket/handler/LoginPageHandler.scala | 42 +---- .../tbasket/handler/RegisterPageHandler.scala | 36 ++-- tests/src/org/tbasket/test/TestEmitter.scala | 2 +- tests/src/org/tbasket/test/TestUtils.scala | 15 ++ .../test/pages/LoginPageHandlerTests.scala | 154 +++++++++--------- 16 files changed, 270 insertions(+), 232 deletions(-) delete mode 100644 Core/src/org/tbasket/ExternalBasketServerException.scala delete mode 100644 Core/src/org/tbasket/InternalBasketServerException.scala delete mode 100644 Core/src/org/tbasket/error/ExceptionEnum.scala create mode 100644 Core/src/org/tbasket/error/OtherExceptions.scala delete mode 100644 Core/src/org/tbasket/error/RegularException.scala create mode 100644 Core/src/org/tbasket/error/UserException.scala create mode 100644 tests/src/org/tbasket/test/TestUtils.scala diff --git a/Core/src/org/tbasket/ExternalBasketServerException.scala b/Core/src/org/tbasket/ExternalBasketServerException.scala deleted file mode 100644 index fad0e41..0000000 --- a/Core/src/org/tbasket/ExternalBasketServerException.scala +++ /dev/null @@ -1,7 +0,0 @@ -package org.tbasket - -/** - * exception thrown when an error occurs due to an external fault from the server. - */ -class ExternalBasketServerException(msg: String, cause: Throwable = null) - extends Exception(msg, cause) {} diff --git a/Core/src/org/tbasket/InternalBasketServerException.scala b/Core/src/org/tbasket/InternalBasketServerException.scala deleted file mode 100644 index e14867d..0000000 --- a/Core/src/org/tbasket/InternalBasketServerException.scala +++ /dev/null @@ -1,7 +0,0 @@ -package org.tbasket - -/** - * exception thrown when an error occurs due to an internal fault of the server or its maintainers. - */ -class InternalBasketServerException(msg: String, cause: Throwable = null) - extends Exception(msg, cause) {} diff --git a/Core/src/org/tbasket/auth/Authenticator.scala b/Core/src/org/tbasket/auth/Authenticator.scala index 54b4641..0a6a0df 100644 --- a/Core/src/org/tbasket/auth/Authenticator.scala +++ b/Core/src/org/tbasket/auth/Authenticator.scala @@ -8,13 +8,9 @@ import io.getquill.* import io.getquill.context.qzio.ZioJdbcContext import io.getquill.context.sql.idiom.SqlIdiom import org.apache.logging.log4j.LogManager -import org.tbasket.InternalBasketServerException import org.tbasket.auth.Authenticator.* import org.tbasket.data.{DatabaseContext, User} -import org.tbasket.error.AuthException.* -import org.tbasket.error.ExceptionEnum -import org.tbasket.error.JwtException.* -import org.tbasket.error.RegularException.* +import org.tbasket.error.* import pdi.jwt.algorithms.JwtAsymmetricAlgorithm import pdi.jwt.{JwtClaim, JwtZIOJson} import zio.* @@ -31,10 +27,9 @@ import javax.sql.DataSource import scala.collection.immutable.HashMap - object Authenticator: - private final val LOG = LogManager.getLogger("Authentification") - private final val ValidMailPattern = "^\\w+([.-]?\\w+)*@\\w+([.-]?\\w+)*(\\.\\w{2,3})+$".r + private final val LOG = LogManager.getLogger("Authentification") + private final val ValidMailPattern = "^\\w+([.-]?\\w+)*@\\w+([.-]?\\w+)*(\\.\\w{2,3})+$".r private final val ValidPasswordPattern = ".{6,}".r case class JwtContent(uuid: UUID) @@ -55,12 +50,12 @@ class Authenticator(url: URL, key: PublicKey, algorithm: JwtAsymmetricAlgorithm) def requestNewJwt(user: User) = { Client.request(mkRequest(user)) .flatMap { - case Response(Ok, _, body, _, _) => + case Response(Ok, _, body, _, _) => body.asString - case Response(InternalServerError, _, _, _, _) => - ZIO.fail(new InternalBasketServerException("internal server error")) - case _ => - ZIO.fail(new InternalBasketServerException("Received unknown response from emitter")) + case Response(InternalServerError, _, body, _, _) => + ZIO.fail(EmitterInternalError(_)).flatMap(f => body.asString.map(f)) + case r => + ZIO.fail(UnrecognizedEmitterResponse(s"Received unknown response from emitter ${r}")) } } @@ -75,10 +70,11 @@ class Authenticator(url: URL, key: PublicKey, algorithm: JwtAsymmetricAlgorithm) def loginUser(mail: String, password: String) = ZIO.serviceWithZIO[DatabaseContext] { ctx => import ctx.v.* - + findByMail(mail) - .someOrFail(UserNotFound) // await one second if password fails to reduce bruteforce //FIXME this wont actually reduce bruteforce - .filterOrElse(_.passwordHash == hashPassword(password))(Clock.sleep(1.seconds) *> ZIO.fail(InvalidPassword)) + .someOrFail(UserNotFound(s"user not found for email address $mail")) + // await one second if password fails to reduce bruteforce //FIXME this wont actually reduce bruteforce + .filterOrElse(_.passwordHash == hashPassword(password))(Clock.sleep(1.seconds) *> ZIO.fail(InvalidPassword("invalid password"))) } private def insert(user: User) = quote { @@ -99,11 +95,11 @@ class Authenticator(url: URL, key: PublicKey, algorithm: JwtAsymmetricAlgorithm) User(uuid, name, forename, hash, mail) } if (!ValidMailPattern.matches(mail)) - ZIO.fail(InvalidEmail) + ZIO.fail(InvalidEmail(s"email address did not satisfy regex pattern $ValidMailPattern")) else if (!ValidPasswordPattern.matches(password)) - ZIO.fail(InvalidPassword) + ZIO.fail(InvalidPassword(s"password did not satisfy regex pattern $ValidPasswordPattern")) else for - _ <- findByMail(mail).none.orElse(ZIO.fail(UserAlreadyRegistered)) + _ <- findByMail(mail).none.orElse(ZIO.fail(UserAlreadyRegistered(s"an email address already exists for '$mail'"))) _ <- run(insert(user)).fork yield user } diff --git a/Core/src/org/tbasket/endpoint/Endpoint.scala b/Core/src/org/tbasket/endpoint/Endpoint.scala index 07c6f4b..3a4fe07 100644 --- a/Core/src/org/tbasket/endpoint/Endpoint.scala +++ b/Core/src/org/tbasket/endpoint/Endpoint.scala @@ -4,11 +4,13 @@ import io.getquill.context.qzio.{ZioContext, ZioJdbcContext} import io.getquill.context.sql.idiom.SqlIdiom import io.getquill.idiom.Idiom import io.getquill.{Literal, NamingStrategy, SqliteDialect} -import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.{LogManager, Logger} import org.tbasket.auth.Authenticator import org.tbasket.data.DatabaseContext -import org.tbasket.endpoint.Endpoint.LOG -import org.tbasket.handler.LoginPageHandler +import org.tbasket.endpoint.Endpoint.{Log, app} +import org.tbasket.error.* +import org.tbasket.handler.HandlerUtils.errorBody +import org.tbasket.handler.{HandlerUtils, LoginPageHandler} import zio.* import zio.http.* import zio.http.ServerConfig.LeakDetectionLevel @@ -17,74 +19,105 @@ import zio.http.model.Status import zio.http.model.Status.InternalServerError import zio.http.netty.client.ConnectionPool +import java.sql.Timestamp import javax.sql.DataSource import scala.collection.mutable class Endpoint(port: Int): - // set generic required headers - private def applyGenerics(response: Response): Response = - response.withAccessControlAllowOrigin("*") + val run = + val config = ServerConfig.default + .port(port) + .leakDetection(LeakDetectionLevel.PARANOID) + + val serverConfigLayer = ServerConfig.live(config) + Server.install(app).flatMap { port => + Log.info(s"Listening API entries on $port") + ZIO.never + }.provideSome[DatabaseContext & Authenticator & DataSource]( + Scope.default, + serverConfigLayer, + ConnectionPool.fixed(4), + ClientConfig.default, + Server.live, + Client.live + ) + +object Endpoint: + final val Log = LogManager.getLogger("API") - private val app = Http.collectZIO[Request] { + private def tryHandle(r: Request) = r match case r@POST -> _ / "login" => LoginPageHandler.post(r) - + case r@method -> path => val ipInsights = r.remoteAddress .map(ip => s": request received from $ip.") .getOrElse("") - LOG.error( + Log.error( s"Was unable to find a handler for request '$path' with method $method ${ipInsights}" ) ZIO.succeed(Response(Status.NotFound)) - }.catchAllCause(handleUnexpectedError) + + def handle(r: Request) = tryHandle(r) + .catchSome(respondToRegulars) + .catchAllCause(handleUnexpectedError) .map(applyGenerics) - private def handleUnexpectedError(cause: Cause[Throwable]): HttpApp[Any, Throwable] = { + val app = Http.collectZIO[Request] { r => + handle(r) + } + + // set generic required headers + private def applyGenerics(response: Response): Response = + response.withAccessControlAllowOrigin("*") + + + private def respondToRegulars: PartialFunction[Object, Task[Response]] = { + case InvalidRequest(msg, cause) => ZIO.attempt(Response( + status = Status.BadRequest, + body = errorBody("invalid request", s"$cause: $msg") + )) + + case InternalError(e) => + Log.error("Internal error : ") + Log.throwing(e) + ZIO.attempt(Response( + status = Status.InternalServerError, + body = errorBody("invalid request", s"internal error: please contact support, timestamp : $timestamp") + )) + } + + private def timestamp = new Timestamp(java.lang.System.currentTimeMillis()) + + private def handleUnexpectedError(cause: Object): Task[Response] = { def report(kind: String, value: Throwable = null, trace: StackTrace = StackTrace.none) = - LOG.error(s"Received unhandled $kind cause ${if value == null then "" else ": " + value}") - LOG.error(trace) + Log.error(s"Received unhandled $kind cause ${if value == null then "" else ": " + value}") + Log.error(trace) cause match - case Cause.Empty => report("empty") - case Cause.Fail(e, trace) => report("failure", e, trace) - case Cause.Die(e, trace) => report("die", e, trace) - case Cause.Interrupt(fiberId, e) => report(s"interruption of $fiberId", null, e) - case Cause.Stackless(cause, _) => - LOG.error("stackless error :") + case e: Throwable => report(e.getMessage, e) + case Cause.Empty => report("empty") + case Cause.Fail(e: Throwable, trace) => report("failure", e, trace) + case Cause.Fail(_, trace) => report("failure", null, trace) + case Cause.Die(e, trace) => report("die", e, trace) + case Cause.Interrupt(fiberId, e) => report(s"interruption of $fiberId", null, e) + case Cause.Stackless(cause, _) => + Log.error("stackless error :") handleUnexpectedError(cause) - case Cause.Then(left, right) => + case Cause.Then(left, right) => handleUnexpectedError(left) - LOG.error("**THEN this error occurred : **") + Log.error("**THEN this error occurred : **") handleUnexpectedError(right) - case Cause.Both(left, right) => + case Cause.Both(left, right) => handleUnexpectedError(left) - LOG.error("**AND this error also occurred (async) : **") + Log.error("**AND this error also occurred (async) : **") handleUnexpectedError(right) - Http.succeed(Response.status(InternalServerError)) + ZIO.attempt(Response( + status = Status.InternalServerError, + body = errorBody("internal", s"internal error, please contact support (timestamp: $timestamp") + )) } - - val run = - val config = ServerConfig.default - .port(port) - .leakDetection(LeakDetectionLevel.PARANOID) - - val serverConfigLayer = ServerConfig.live(config) - Server.install(app).flatMap { port => - LOG.info(s"Listening API entries on $port") - ZIO.never - }.provideSome[DatabaseContext & Authenticator & DataSource]( - Scope.default, - serverConfigLayer, - ConnectionPool.fixed(4), - ClientConfig.default, - Server.live, - Client.live - ) - -object Endpoint: - final val LOG = LogManager.getLogger("API") diff --git a/Core/src/org/tbasket/error/AuthException.scala b/Core/src/org/tbasket/error/AuthException.scala index 3092507..24ddf00 100644 --- a/Core/src/org/tbasket/error/AuthException.scala +++ b/Core/src/org/tbasket/error/AuthException.scala @@ -1,10 +1,11 @@ package org.tbasket.error -import org.tbasket.error.ExceptionEnum - -enum AuthException extends ExceptionEnum { - case InvalidPassword - case InvalidEmail - case UserNotFound - case UserAlreadyRegistered -} \ No newline at end of file +sealed class AuthException(msg: String) extends Exception(msg) with UserException + +case class InvalidPassword(msg: String) extends AuthException(msg) + +case class InvalidEmail(msg: String) extends AuthException(msg) + +case class UserNotFound(msg: String) extends AuthException(msg) + +case class UserAlreadyRegistered(msg: String) extends AuthException(msg) \ No newline at end of file diff --git a/Core/src/org/tbasket/error/ExceptionEnum.scala b/Core/src/org/tbasket/error/ExceptionEnum.scala deleted file mode 100644 index 2f08017..0000000 --- a/Core/src/org/tbasket/error/ExceptionEnum.scala +++ /dev/null @@ -1,5 +0,0 @@ -package org.tbasket.error - -trait ExceptionEnum extends Exception { - -} diff --git a/Core/src/org/tbasket/error/JwtException.scala b/Core/src/org/tbasket/error/JwtException.scala index df61cbf..918b1b1 100644 --- a/Core/src/org/tbasket/error/JwtException.scala +++ b/Core/src/org/tbasket/error/JwtException.scala @@ -1,10 +1,12 @@ package org.tbasket.error -enum JwtException extends ExceptionEnum { - case InvalidJwt(cause: String) - case ExpiredJwt - - - case InvalidEmitterResponse - case EmitterInternalError -} \ No newline at end of file +sealed class JwtException(msg: String) extends Exception(msg) + +case class InvalidJwt(msg: String) extends JwtException(msg) with UserException + +case class ExpiredJwt(msg: String) extends JwtException(msg) with UserException + + +case class UnrecognizedEmitterResponse(msg: String) extends JwtException(msg) + +case class EmitterInternalError(msg: String) extends JwtException(msg) diff --git a/Core/src/org/tbasket/error/OtherExceptions.scala b/Core/src/org/tbasket/error/OtherExceptions.scala new file mode 100644 index 0000000..11b931f --- /dev/null +++ b/Core/src/org/tbasket/error/OtherExceptions.scala @@ -0,0 +1,18 @@ +package org.tbasket.error + +import org.apache.logging.log4j.{LogManager, Logger} +import org.tbasket.handler.HandlerUtils.errorBody +import zio.http.Response +import zio.http.model.Status +import zio.{Task, ZIO} + +sealed class RegularException(msg: String, cause: Throwable = null) extends Exception(msg, cause) + +case class InternalError(cause: Throwable) extends Exception(cause) + +case class InvalidArgumentError(cause: String) extends Exception(cause) + + +case class InvalidRequest(msg: String, cause: String) extends Exception(msg + ":" +cause) with UserException + + diff --git a/Core/src/org/tbasket/error/RegularException.scala b/Core/src/org/tbasket/error/RegularException.scala deleted file mode 100644 index 5d31a3f..0000000 --- a/Core/src/org/tbasket/error/RegularException.scala +++ /dev/null @@ -1,9 +0,0 @@ -package org.tbasket.error - -enum RegularException extends ExceptionEnum { - case InternalError(cause: Throwable) - case InvalidArgumentError(cause: String) - - - case InvalidRequest(msg: String, cause: String) -} diff --git a/Core/src/org/tbasket/error/UserException.scala b/Core/src/org/tbasket/error/UserException.scala new file mode 100644 index 0000000..4306c6f --- /dev/null +++ b/Core/src/org/tbasket/error/UserException.scala @@ -0,0 +1,10 @@ +package org.tbasket.error + +/** + * User exceptions are a special type of exception where stack trace is not filled as those exceptions are not meant to be printed in the console + * because they are predictable error. + * + * */ +trait UserException extends Exception { + override def fillInStackTrace(): Throwable = this //do not fill stack trace +} diff --git a/Core/src/org/tbasket/handler/HandlerUtils.scala b/Core/src/org/tbasket/handler/HandlerUtils.scala index 4695df7..76a62a4 100644 --- a/Core/src/org/tbasket/handler/HandlerUtils.scala +++ b/Core/src/org/tbasket/handler/HandlerUtils.scala @@ -1,6 +1,6 @@ package org.tbasket.handler -import org.tbasket.error.RegularException.InvalidRequest +import org.tbasket.error.* import zio.{Task, ZIO} import zio.http.Body import zio.json.* @@ -15,4 +15,7 @@ object HandlerUtils { .mapError(InvalidRequest(s"Missing or invalid field $name.", _)) def errorBody(errorType: String, msg: String) = Body.fromString(s"""{"error": "$errorType","msg": "$msg"}""") + + + } diff --git a/Core/src/org/tbasket/handler/LoginPageHandler.scala b/Core/src/org/tbasket/handler/LoginPageHandler.scala index e670624..74804f3 100644 --- a/Core/src/org/tbasket/handler/LoginPageHandler.scala +++ b/Core/src/org/tbasket/handler/LoginPageHandler.scala @@ -4,20 +4,17 @@ import io.getquill.* import io.getquill.context.ZioJdbc.* import io.getquill.context.qzio.{ZioContext, ZioJdbcContext} import io.getquill.context.sql.idiom.SqlIdiom -import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.{LogManager, Logger} import org.tbasket.auth.Authenticator import org.tbasket.data.{DatabaseContext, User} -import org.tbasket.error.AuthException.* -import org.tbasket.error.ExceptionEnum -import org.tbasket.error.JwtException.* -import org.tbasket.error.RegularException.* +import org.tbasket.error.* import org.tbasket.handler.HandlerUtils.errorBody +import zio.* import zio.http.* import zio.http.model.{Cookie, Header, Headers, Status} import zio.json.* import zio.json.ast.Json.Str import zio.json.ast.{Json, JsonCursor} -import zio.* import java.sql.SQLException import java.util.UUID @@ -25,7 +22,7 @@ import java.util.UUID object LoginPageHandler extends PageHandler: - private val LOG = LogManager.getLogger("Login") + implicit private final val Log: Logger = LogManager.getLogger("/login") private def getUser(json: Json) = ZIO.serviceWithZIO[Authenticator] { auth => @@ -57,39 +54,18 @@ object LoginPageHandler extends PageHandler: ) def post(request: Request) = - tryPost(request).catchAll { - case UserNotFound => ZIO.attempt(Response( + tryPost(request).catchSome { + case UserNotFound(msg) => ZIO.attempt(Response( status = Status.Unauthorized, - body = errorBody("unauthorized", "unknown user email"), + body = errorBody("unauthorized", msg), headers = Headers( Headers.location("/register") ) // send back caller to register panel )) - case InvalidPassword => ZIO.attempt(Response( + case InvalidPassword(msg) => ZIO.attempt(Response( status = Status.Unauthorized, - body = errorBody("unauthorized", "invalid password") + body = errorBody("unauthorized", msg) )) - - case InvalidRequest(msg, cause) => ZIO.attempt(Response( - status = Status.Unauthorized, - body = errorBody("invalid request", s"$cause: $msg") - )) - - case InternalError(e) => - LOG.error("Internal error : ") - LOG.throwing(e) - ZIO.attempt(Response( - status = Status.InternalServerError, - body = errorBody("internal", "internal error, please contact support") - )) - - case other => - LOG.error("Unhandle exception : ") - LOG.throwing(other) - ZIO.attempt(Response( - status = Status.InternalServerError, - body = errorBody("internal", "internal error, please contact support") - )) } diff --git a/Core/src/org/tbasket/handler/RegisterPageHandler.scala b/Core/src/org/tbasket/handler/RegisterPageHandler.scala index 1adf3b9..f1f6c88 100644 --- a/Core/src/org/tbasket/handler/RegisterPageHandler.scala +++ b/Core/src/org/tbasket/handler/RegisterPageHandler.scala @@ -1,30 +1,33 @@ package org.tbasket.handler +import org.apache.logging.log4j.{LogManager, Logger} import org.tbasket.auth.Authenticator import org.tbasket.data.User -import org.tbasket.error.RegularException.InvalidRequest -import org.tbasket.handler.HandlerUtils.parseAttribute +import org.tbasket.error.* +import org.tbasket.handler.HandlerUtils.{errorBody, parseAttribute} import zio.ZIO import zio.http.model.{Cookie, Headers, Status} -import zio.http.{Request, Response, model} -import zio.json.ast.{Json, JsonCursor} +import zio.http.{Body, Request, Response, model} import zio.json.* +import zio.json.ast.{Json, JsonCursor} object RegisterPageHandler extends PageHandler { + implicit private final val Log: Logger = LogManager.getLogger("/register") + private def tryPost(request: Request) = for body <- request.body.asString .mapError(e => InvalidRequest("Invalid request body", e.getMessage)) - + json <- ZIO.fromEither(body.fromJson[Json]) .mapError(InvalidRequest("Invalid JSON body", _)) - + name <- parseAttribute(json, "name", JsonCursor.field("name").isString) forename <- parseAttribute(json, "forename", JsonCursor.field("forename").isString) mail <- parseAttribute(json, "email", JsonCursor.field("email").isString) password <- parseAttribute(json, "password", JsonCursor.field("password").isString) - + user <- ZIO.serviceWithZIO[Authenticator](_.registerUser(name, forename, mail, password)) jwt <- ZIO.serviceWithZIO[Authenticator](_.requestNewJwt(user)) yield Response( @@ -33,8 +36,21 @@ object RegisterPageHandler extends PageHandler { Headers.setCookie(Cookie("JWT", jwt)) ) - def post(request: Request) = tryPost(request).catchSome { - case x => ??? - } + def post(request: Request) = tryPost(request) + .catchSome { + case InvalidEmail(msg) => ZIO.attempt(Response( + status = Status.ExpectationFailed, + body = errorBody("invalid email", msg) + )) + case InvalidPassword(msg) => ZIO.attempt(Response( + status = Status.ExpectationFailed, + body = errorBody("invalid password", msg) + )) + case UserAlreadyRegistered(msg) => ZIO.attempt(Response( + status = Status.NotAcceptable, + body = errorBody("already registered", msg), + headers = Headers.location("/login") //the account already exists so we move the user to login page + )) + } } diff --git a/tests/src/org/tbasket/test/TestEmitter.scala b/tests/src/org/tbasket/test/TestEmitter.scala index a576968..4d3d515 100644 --- a/tests/src/org/tbasket/test/TestEmitter.scala +++ b/tests/src/org/tbasket/test/TestEmitter.scala @@ -16,7 +16,7 @@ object TestEmitter: val process = new ProcessBuilder( "bash", - "./mill", "JWTEmitter.run", + "./mill", "--disable-ticker", "JWTEmitter.run", "-k", "/tmp/keys/key.pcqks", "-p", TestServerConfig.emitterURL.port.get.toString ) diff --git a/tests/src/org/tbasket/test/TestUtils.scala b/tests/src/org/tbasket/test/TestUtils.scala new file mode 100644 index 0000000..ba69402 --- /dev/null +++ b/tests/src/org/tbasket/test/TestUtils.scala @@ -0,0 +1,15 @@ +package org.tbasket.test + +import zio.* +import zio.http.{Body, Response} +import zio.json.* +import zio.json.ast.Json + +object TestUtils { + def getJsonBody(r: Response): Task[Json] = { + for + body <- r.body.asString + json <- ZIO.fromEither(body.fromJson[Json]).mapError(new Exception(_)).orElseSucceed(Json.Null) + yield json + } +} diff --git a/tests/src/org/tbasket/test/pages/LoginPageHandlerTests.scala b/tests/src/org/tbasket/test/pages/LoginPageHandlerTests.scala index d61ca53..ed5f912 100644 --- a/tests/src/org/tbasket/test/pages/LoginPageHandlerTests.scala +++ b/tests/src/org/tbasket/test/pages/LoginPageHandlerTests.scala @@ -4,17 +4,18 @@ import io.getquill.jdbczio.Quill import io.getquill.{SnakeCase, SqliteZioJdbcContext} import org.tbasket.auth.Authenticator import org.tbasket.data.{Database, DatabaseContext} -import org.tbasket.error.RegularException.InvalidRequest +import org.tbasket.endpoint.Endpoint +import org.tbasket.endpoint.Endpoint.handle +import org.tbasket.error.* import org.tbasket.handler.HandlerUtils.parseAttribute import org.tbasket.handler.LoginPageHandler -import org.tbasket.handler.LoginPageHandler.post -import org.tbasket.test.TestLayers -import org.tbasket.test.pages.LoginPageHandlerTests.test +import org.tbasket.test.TestUtils.getJsonBody +import org.tbasket.test.{TestLayers, TestUtils} import zio.* -import zio.http.netty.client.ConnectionPool import zio.http.* -import zio.http.model.{HeaderNames, Headers, Status} import zio.http.model.Headers.{Header, empty} +import zio.http.model.{HeaderNames, Headers, Status} +import zio.http.netty.client.ConnectionPool import zio.json.* import zio.json.ast.{Json, JsonCursor} import zio.test.* @@ -25,82 +26,77 @@ object LoginPageHandlerTests extends ZIOSpecDefault { import LoginPageHandler.post import TestLayers.* - private def getJsonBody(r: Response): Task[Json] = { - for - body <- r.body.asString - json <- ZIO.fromEither(body.fromJson[Json]).mapError(new Exception(_)) - yield json - } + private final val url = URL.fromString("http://localhost/login") match + case Left(value) => throw value + case Right(url) => url + - private def requestsSpec = suite("erroned request body tests")( - ZIO.attempt(Map( - "empty packet" -> Body.empty, - "with no mail attribute" -> Body.fromString("""{"password":"1234"}"""), - "with no password attribute" -> Body.fromString("""{"email":"valid.mail@x.y"}"""), - "with invalid json" -> Body.fromString("""this is a corrupted json""") - )).map(_.map((name, body) => - test(name) { - for - response <- post(Request.post(body, URL.empty)) - json <- getJsonBody(response) - errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString) - yield - assert(response)(hasField("status", _.status, equalTo(Status.Unauthorized))) - && assertTrue(errorType == "invalid request") - } - )) - ) + private def requestsSpec = suite("bad request tests")( + ZIO.attempt(Map( + "empty packet" -> Body.empty, + "with no mail attribute" -> Body.fromString("""{"password":"1234"}"""), + "with no password attribute" -> Body.fromString("""{"email":"valid.mail@x.y"}"""), + "with invalid json" -> Body.fromString("""this is a corrupted json""") + )).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 = { - suite("login situation tests")( - test("login with unknown account") { - for - response <- post(Request.post(Body.fromString("""{"password":"123456","email":"unknownaccount@gmail.com"}"""), URL.empty)) - json <- getJsonBody(response) - errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString) - errorMsg <- parseAttribute(json, "msg", JsonCursor.field("msg").isString) - yield - //assert that the response error is of type unauthorized and headers are Location: /register - assert(response)(hasField("status", _.status, equalTo(Status.Unauthorized))) - && assert(errorType)(equalTo("unauthorized")) - && assert(errorMsg)(equalTo("unknown user email")) - && assert(response)(hasField("headers", _.headers, hasSameElements(Headers.location("/register")))) - }, + private def loginSpec = { + suite("login situation tests")( + test("login with unknown account") { + for + response <- handle(Request.post(Body.fromString("""{"password":"123456","email":"unknownaccount@gmail.com"}"""), url)) + json <- getJsonBody(response) + errorType <- parseAttribute(json, "error", 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.Unauthorized))) + && assert(errorType)(equalTo("unauthorized")) + && assert(response)(hasField("headers", _.headers, contains(Headers.location("/register")))) + }, - test("login with known account") { - for - response <- post(Request.post(Body.fromString("""{"password":"123456","email":"maximebatista18@gmail.com"}"""), URL.empty)) - yield - 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("headers", _.headers, exists(hasField("key", _.key, equalTo(HeaderNames.setCookie))))) - }, + test("login with known account") { + for + response <- handle(Request.post(Body.fromString("""{"password":"123456","email":"maximebatista18@gmail.com"}"""), url)) + yield + 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("headers", _.headers, exists(hasField("key", _.key, equalTo(HeaderNames.setCookie))))) + }, - test("login with known account wrong password") { - val requestJson = """{"password":"this is a wrong password","email":"maximebatista18@gmail.com"}""" - for - fiber <- post(Request.post(Body.fromString(requestJson), URL.empty)).fork - _ <- TestClock.adjust(1.seconds) - response <- ZIO.fromFiber(fiber) - json <- getJsonBody(response) - errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString) - errorMsg <- parseAttribute(json, "msg", JsonCursor.field("msg").isString) - yield - assert(errorType)(equalTo("unauthorized")) - && assert(errorMsg)(equalTo("invalid password")) - } - ) - } + test("login with known account wrong password") { + val requestJson = """{"password":"this is a wrong password","email":"maximebatista18@gmail.com"}""" + for + fiber <- handle(Request.post(Body.fromString(requestJson), url)).fork + _ <- TestClock.adjust(1.seconds) + response <- ZIO.fromFiber(fiber) + json <- getJsonBody(response) + errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString) + yield + assert(response)(hasField("status", _.status, equalTo(Status.Unauthorized))) + && assert(errorType)(equalTo("unauthorized")) + } + ) + } - override def spec = suite("/login page handler")( - requestsSpec, - loginSpec - ).provide( - db.datasourceLayer, - db.contextLayer, - auth, - ConnectionPool.fixed(1), - Scope.default, - ClientConfig.default, - Client.live) + override def spec = suite("/login page handler")( + requestsSpec, + loginSpec + ).provide( + db.datasourceLayer, + db.contextLayer, + auth, + ConnectionPool.fixed(1), + Scope.default, + ClientConfig.default, + Client.live) }