reviewed error handling and tests
continuous-integration/drone/push Build is passing Details

dev
Override-6 2 years ago
parent c8247bd998
commit 8aa3c0a39a

@ -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) {}

@ -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) {}

@ -8,13 +8,9 @@ import io.getquill.*
import io.getquill.context.qzio.ZioJdbcContext import io.getquill.context.qzio.ZioJdbcContext
import io.getquill.context.sql.idiom.SqlIdiom import io.getquill.context.sql.idiom.SqlIdiom
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.tbasket.InternalBasketServerException
import org.tbasket.auth.Authenticator.* import org.tbasket.auth.Authenticator.*
import org.tbasket.data.{DatabaseContext, User} import org.tbasket.data.{DatabaseContext, User}
import org.tbasket.error.AuthException.* import org.tbasket.error.*
import org.tbasket.error.ExceptionEnum
import org.tbasket.error.JwtException.*
import org.tbasket.error.RegularException.*
import pdi.jwt.algorithms.JwtAsymmetricAlgorithm import pdi.jwt.algorithms.JwtAsymmetricAlgorithm
import pdi.jwt.{JwtClaim, JwtZIOJson} import pdi.jwt.{JwtClaim, JwtZIOJson}
import zio.* import zio.*
@ -31,10 +27,9 @@ import javax.sql.DataSource
import scala.collection.immutable.HashMap 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 ValidPasswordPattern = ".{6,}".r private final val ValidPasswordPattern = ".{6,}".r
case class JwtContent(uuid: UUID) case class JwtContent(uuid: UUID)
@ -55,12 +50,12 @@ class Authenticator(url: URL, key: PublicKey, algorithm: JwtAsymmetricAlgorithm)
def requestNewJwt(user: User) = { def requestNewJwt(user: User) = {
Client.request(mkRequest(user)) Client.request(mkRequest(user))
.flatMap { .flatMap {
case Response(Ok, _, body, _, _) => case Response(Ok, _, body, _, _) =>
body.asString body.asString
case Response(InternalServerError, _, _, _, _) => case Response(InternalServerError, _, body, _, _) =>
ZIO.fail(new InternalBasketServerException("internal server error")) ZIO.fail(EmitterInternalError(_)).flatMap(f => body.asString.map(f))
case _ => case r =>
ZIO.fail(new InternalBasketServerException("Received unknown response from emitter")) 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 => def loginUser(mail: String, password: String) = ZIO.serviceWithZIO[DatabaseContext] { ctx =>
import ctx.v.* import ctx.v.*
findByMail(mail) findByMail(mail)
.someOrFail(UserNotFound) // await one second if password fails to reduce bruteforce //FIXME this wont actually reduce bruteforce .someOrFail(UserNotFound(s"user not found for email address $mail"))
.filterOrElse(_.passwordHash == hashPassword(password))(Clock.sleep(1.seconds) *> ZIO.fail(InvalidPassword)) // 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 { private def insert(user: User) = quote {
@ -99,11 +95,11 @@ class Authenticator(url: URL, key: PublicKey, algorithm: JwtAsymmetricAlgorithm)
User(uuid, name, forename, hash, mail) User(uuid, name, forename, hash, mail)
} }
if (!ValidMailPattern.matches(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)) else if (!ValidPasswordPattern.matches(password))
ZIO.fail(InvalidPassword) ZIO.fail(InvalidPassword(s"password did not satisfy regex pattern $ValidPasswordPattern"))
else for 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 _ <- run(insert(user)).fork
yield user yield user
} }

@ -4,11 +4,13 @@ import io.getquill.context.qzio.{ZioContext, ZioJdbcContext}
import io.getquill.context.sql.idiom.SqlIdiom import io.getquill.context.sql.idiom.SqlIdiom
import io.getquill.idiom.Idiom import io.getquill.idiom.Idiom
import io.getquill.{Literal, NamingStrategy, SqliteDialect} 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.auth.Authenticator
import org.tbasket.data.DatabaseContext import org.tbasket.data.DatabaseContext
import org.tbasket.endpoint.Endpoint.LOG import org.tbasket.endpoint.Endpoint.{Log, app}
import org.tbasket.handler.LoginPageHandler import org.tbasket.error.*
import org.tbasket.handler.HandlerUtils.errorBody
import org.tbasket.handler.{HandlerUtils, LoginPageHandler}
import zio.* import zio.*
import zio.http.* import zio.http.*
import zio.http.ServerConfig.LeakDetectionLevel import zio.http.ServerConfig.LeakDetectionLevel
@ -17,74 +19,105 @@ import zio.http.model.Status
import zio.http.model.Status.InternalServerError import zio.http.model.Status.InternalServerError
import zio.http.netty.client.ConnectionPool import zio.http.netty.client.ConnectionPool
import java.sql.Timestamp
import javax.sql.DataSource import javax.sql.DataSource
import scala.collection.mutable import scala.collection.mutable
class Endpoint(port: Int): class Endpoint(port: Int):
// set generic required headers val run =
private def applyGenerics(response: Response): Response = val config = ServerConfig.default
response.withAccessControlAllowOrigin("*") .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" => case r@POST -> _ / "login" =>
LoginPageHandler.post(r) LoginPageHandler.post(r)
case r@method -> path => case r@method -> path =>
val ipInsights = r.remoteAddress val ipInsights = r.remoteAddress
.map(ip => s": request received from $ip.") .map(ip => s": request received from $ip.")
.getOrElse("") .getOrElse("")
LOG.error( Log.error(
s"Was unable to find a handler for request '$path' with method $method ${ipInsights}" s"Was unable to find a handler for request '$path' with method $method ${ipInsights}"
) )
ZIO.succeed(Response(Status.NotFound)) ZIO.succeed(Response(Status.NotFound))
}.catchAllCause(handleUnexpectedError)
def handle(r: Request) = tryHandle(r)
.catchSome(respondToRegulars)
.catchAllCause(handleUnexpectedError)
.map(applyGenerics) .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) = 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(s"Received unhandled $kind cause ${if value == null then "" else ": " + value}")
LOG.error(trace) Log.error(trace)
cause match cause match
case Cause.Empty => report("empty") case e: Throwable => report(e.getMessage, e)
case Cause.Fail(e, trace) => report("failure", e, trace) case Cause.Empty => report("empty")
case Cause.Die(e, trace) => report("die", e, trace) case Cause.Fail(e: Throwable, trace) => report("failure", e, trace)
case Cause.Interrupt(fiberId, e) => report(s"interruption of $fiberId", null, e) case Cause.Fail(_, trace) => report("failure", null, trace)
case Cause.Stackless(cause, _) => case Cause.Die(e, trace) => report("die", e, trace)
LOG.error("stackless error :") case Cause.Interrupt(fiberId, e) => report(s"interruption of $fiberId", null, e)
case Cause.Stackless(cause, _) =>
Log.error("stackless error :")
handleUnexpectedError(cause) handleUnexpectedError(cause)
case Cause.Then(left, right) => case Cause.Then(left, right) =>
handleUnexpectedError(left) handleUnexpectedError(left)
LOG.error("**THEN this error occurred : **") Log.error("**THEN this error occurred : **")
handleUnexpectedError(right) handleUnexpectedError(right)
case Cause.Both(left, right) => case Cause.Both(left, right) =>
handleUnexpectedError(left) handleUnexpectedError(left)
LOG.error("**AND this error also occurred (async) : **") Log.error("**AND this error also occurred (async) : **")
handleUnexpectedError(right) 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")

@ -1,10 +1,11 @@
package org.tbasket.error package org.tbasket.error
import org.tbasket.error.ExceptionEnum sealed class AuthException(msg: String) extends Exception(msg) with UserException
enum AuthException extends ExceptionEnum { case class InvalidPassword(msg: String) extends AuthException(msg)
case InvalidPassword
case InvalidEmail case class InvalidEmail(msg: String) extends AuthException(msg)
case UserNotFound
case UserAlreadyRegistered case class UserNotFound(msg: String) extends AuthException(msg)
}
case class UserAlreadyRegistered(msg: String) extends AuthException(msg)

@ -1,5 +0,0 @@
package org.tbasket.error
trait ExceptionEnum extends Exception {
}

@ -1,10 +1,12 @@
package org.tbasket.error package org.tbasket.error
enum JwtException extends ExceptionEnum { sealed class JwtException(msg: String) extends Exception(msg)
case InvalidJwt(cause: String)
case ExpiredJwt case class InvalidJwt(msg: String) extends JwtException(msg) with UserException
case class ExpiredJwt(msg: String) extends JwtException(msg) with UserException
case InvalidEmitterResponse
case EmitterInternalError
} case class UnrecognizedEmitterResponse(msg: String) extends JwtException(msg)
case class EmitterInternalError(msg: String) extends JwtException(msg)

@ -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

@ -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)
}

@ -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
}

@ -1,6 +1,6 @@
package org.tbasket.handler package org.tbasket.handler
import org.tbasket.error.RegularException.InvalidRequest import org.tbasket.error.*
import zio.{Task, ZIO} import zio.{Task, ZIO}
import zio.http.Body import zio.http.Body
import zio.json.* import zio.json.*
@ -15,4 +15,7 @@ object HandlerUtils {
.mapError(InvalidRequest(s"Missing or invalid field $name.", _)) .mapError(InvalidRequest(s"Missing or invalid field $name.", _))
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,20 +4,17 @@ import io.getquill.*
import io.getquill.context.ZioJdbc.* import io.getquill.context.ZioJdbc.*
import io.getquill.context.qzio.{ZioContext, ZioJdbcContext} import io.getquill.context.qzio.{ZioContext, ZioJdbcContext}
import io.getquill.context.sql.idiom.SqlIdiom 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.auth.Authenticator
import org.tbasket.data.{DatabaseContext, User} import org.tbasket.data.{DatabaseContext, User}
import org.tbasket.error.AuthException.* import org.tbasket.error.*
import org.tbasket.error.ExceptionEnum
import org.tbasket.error.JwtException.*
import org.tbasket.error.RegularException.*
import org.tbasket.handler.HandlerUtils.errorBody import org.tbasket.handler.HandlerUtils.errorBody
import zio.*
import zio.http.* import zio.http.*
import zio.http.model.{Cookie, Header, Headers, Status} import zio.http.model.{Cookie, Header, Headers, Status}
import zio.json.* import zio.json.*
import zio.json.ast.Json.Str import zio.json.ast.Json.Str
import zio.json.ast.{Json, JsonCursor} import zio.json.ast.{Json, JsonCursor}
import zio.*
import java.sql.SQLException import java.sql.SQLException
import java.util.UUID import java.util.UUID
@ -25,7 +22,7 @@ import java.util.UUID
object LoginPageHandler extends PageHandler: object LoginPageHandler extends PageHandler:
private val LOG = LogManager.getLogger("Login") implicit private final val Log: Logger = LogManager.getLogger("/login")
private def getUser(json: Json) = private def getUser(json: Json) =
ZIO.serviceWithZIO[Authenticator] { auth => ZIO.serviceWithZIO[Authenticator] { auth =>
@ -57,39 +54,18 @@ object LoginPageHandler extends PageHandler:
) )
def post(request: Request) = def post(request: Request) =
tryPost(request).catchAll { tryPost(request).catchSome {
case UserNotFound => ZIO.attempt(Response( case UserNotFound(msg) => ZIO.attempt(Response(
status = Status.Unauthorized, status = Status.Unauthorized,
body = errorBody("unauthorized", "unknown user email"), body = errorBody("unauthorized", msg),
headers = headers =
Headers( Headers(
Headers.location("/register") Headers.location("/register")
) // send back caller to register panel ) // send back caller to register panel
)) ))
case InvalidPassword => ZIO.attempt(Response( case InvalidPassword(msg) => ZIO.attempt(Response(
status = Status.Unauthorized, 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")
))
} }

@ -1,30 +1,33 @@
package org.tbasket.handler package org.tbasket.handler
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.RegularException.InvalidRequest import org.tbasket.error.*
import org.tbasket.handler.HandlerUtils.parseAttribute import org.tbasket.handler.HandlerUtils.{errorBody, parseAttribute}
import zio.ZIO import zio.ZIO
import zio.http.model.{Cookie, Headers, Status} import zio.http.model.{Cookie, Headers, Status}
import zio.http.{Request, Response, model} import zio.http.{Body, Request, Response, model}
import zio.json.ast.{Json, JsonCursor}
import zio.json.* import zio.json.*
import zio.json.ast.{Json, JsonCursor}
object RegisterPageHandler extends PageHandler { object RegisterPageHandler extends PageHandler {
implicit private final val Log: Logger = LogManager.getLogger("/register")
private def tryPost(request: Request) = private def tryPost(request: Request) =
for for
body <- request.body.asString body <- request.body.asString
.mapError(e => InvalidRequest("Invalid request body", e.getMessage)) .mapError(e => InvalidRequest("Invalid request body", e.getMessage))
json <- ZIO.fromEither(body.fromJson[Json]) json <- ZIO.fromEither(body.fromJson[Json])
.mapError(InvalidRequest("Invalid JSON body", _)) .mapError(InvalidRequest("Invalid JSON body", _))
name <- parseAttribute(json, "name", JsonCursor.field("name").isString) name <- parseAttribute(json, "name", JsonCursor.field("name").isString)
forename <- parseAttribute(json, "forename", JsonCursor.field("forename").isString) forename <- parseAttribute(json, "forename", JsonCursor.field("forename").isString)
mail <- parseAttribute(json, "email", JsonCursor.field("email").isString) mail <- parseAttribute(json, "email", JsonCursor.field("email").isString)
password <- parseAttribute(json, "password", JsonCursor.field("password").isString) password <- parseAttribute(json, "password", JsonCursor.field("password").isString)
user <- ZIO.serviceWithZIO[Authenticator](_.registerUser(name, forename, mail, password)) user <- ZIO.serviceWithZIO[Authenticator](_.registerUser(name, forename, mail, password))
jwt <- ZIO.serviceWithZIO[Authenticator](_.requestNewJwt(user)) jwt <- ZIO.serviceWithZIO[Authenticator](_.requestNewJwt(user))
yield Response( yield Response(
@ -33,8 +36,21 @@ object RegisterPageHandler extends PageHandler {
Headers.setCookie(Cookie("JWT", jwt)) Headers.setCookie(Cookie("JWT", jwt))
) )
def post(request: Request) = tryPost(request).catchSome { def post(request: Request) = tryPost(request)
case x => ??? .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
))
}
} }

@ -16,7 +16,7 @@ object TestEmitter:
val process = new ProcessBuilder( val process = new ProcessBuilder(
"bash", "bash",
"./mill", "JWTEmitter.run", "./mill", "--disable-ticker", "JWTEmitter.run",
"-k", "/tmp/keys/key.pcqks", "-k", "/tmp/keys/key.pcqks",
"-p", TestServerConfig.emitterURL.port.get.toString "-p", TestServerConfig.emitterURL.port.get.toString
) )

@ -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
}
}

@ -4,17 +4,18 @@ import io.getquill.jdbczio.Quill
import io.getquill.{SnakeCase, SqliteZioJdbcContext} import io.getquill.{SnakeCase, SqliteZioJdbcContext}
import org.tbasket.auth.Authenticator import org.tbasket.auth.Authenticator
import org.tbasket.data.{Database, DatabaseContext} 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.HandlerUtils.parseAttribute
import org.tbasket.handler.LoginPageHandler import org.tbasket.handler.LoginPageHandler
import org.tbasket.handler.LoginPageHandler.post import org.tbasket.test.TestUtils.getJsonBody
import org.tbasket.test.TestLayers import org.tbasket.test.{TestLayers, TestUtils}
import org.tbasket.test.pages.LoginPageHandlerTests.test
import zio.* import zio.*
import zio.http.netty.client.ConnectionPool
import zio.http.* import zio.http.*
import zio.http.model.{HeaderNames, Headers, Status}
import zio.http.model.Headers.{Header, empty} 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.*
import zio.json.ast.{Json, JsonCursor} import zio.json.ast.{Json, JsonCursor}
import zio.test.* import zio.test.*
@ -25,82 +26,77 @@ object LoginPageHandlerTests extends ZIOSpecDefault {
import LoginPageHandler.post import LoginPageHandler.post
import TestLayers.* import TestLayers.*
private def getJsonBody(r: Response): Task[Json] = { private final val url = URL.fromString("http://localhost/login") match
for case Left(value) => throw value
body <- r.body.asString case Right(url) => url
json <- ZIO.fromEither(body.fromJson[Json]).mapError(new Exception(_))
yield json
}
private def requestsSpec = suite("erroned request body tests")( private def requestsSpec = suite("bad request tests")(
ZIO.attempt(Map( ZIO.attempt(Map(
"empty packet" -> Body.empty, "empty packet" -> Body.empty,
"with no mail attribute" -> Body.fromString("""{"password":"1234"}"""), "with no mail attribute" -> Body.fromString("""{"password":"1234"}"""),
"with no password attribute" -> Body.fromString("""{"email":"valid.mail@x.y"}"""), "with no password attribute" -> Body.fromString("""{"email":"valid.mail@x.y"}"""),
"with invalid json" -> Body.fromString("""this is a corrupted json""") "with invalid json" -> Body.fromString("""this is a corrupted json""")
)).map(_.map((name, body) => )).map(_.map { case (name, body) =>
test(name) { test(name) {
for for
response <- post(Request.post(body, URL.empty)) response <- handle(Request.post(body, url))
json <- getJsonBody(response) json <- getJsonBody(response)
errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString) errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString)
yield yield
assert(response)(hasField("status", _.status, equalTo(Status.Unauthorized))) assert(response)(hasField("status", _.status, equalTo(Status.BadRequest)))
&& assertTrue(errorType == "invalid request") && assertTrue(errorType == "invalid request")
} }
)) })
) )
private def loginSpec = { private def loginSpec = {
suite("login situation tests")( suite("login situation tests")(
test("login with unknown account") { test("login with unknown account") {
for for
response <- post(Request.post(Body.fromString("""{"password":"123456","email":"unknownaccount@gmail.com"}"""), URL.empty)) response <- handle(Request.post(Body.fromString("""{"password":"123456","email":"unknownaccount@gmail.com"}"""), url))
json <- getJsonBody(response) json <- getJsonBody(response)
errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString) errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString)
errorMsg <- parseAttribute(json, "msg", JsonCursor.field("msg").isString) yield
yield //assert that the response error is of type unauthorized and headers are Location: /register
//assert that the response error is of type unauthorized and headers are Location: /register assert(response)(hasField("status", _.status, equalTo(Status.Unauthorized)))
assert(response)(hasField("status", _.status, equalTo(Status.Unauthorized))) && assert(errorType)(equalTo("unauthorized"))
&& assert(errorType)(equalTo("unauthorized")) && assert(response)(hasField("headers", _.headers, contains(Headers.location("/register"))))
&& assert(errorMsg)(equalTo("unknown user email")) },
&& assert(response)(hasField("headers", _.headers, hasSameElements(Headers.location("/register"))))
},
test("login with known account") { test("login with known account") {
for for
response <- post(Request.post(Body.fromString("""{"password":"123456","email":"maximebatista18@gmail.com"}"""), URL.empty)) response <- handle(Request.post(Body.fromString("""{"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") {
val requestJson = """{"password":"this is a wrong password","email":"maximebatista18@gmail.com"}""" val requestJson = """{"password":"this is a wrong password","email":"maximebatista18@gmail.com"}"""
for for
fiber <- post(Request.post(Body.fromString(requestJson), URL.empty)).fork fiber <- handle(Request.post(Body.fromString(requestJson), 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, "error", JsonCursor.field("error").isString)
errorMsg <- parseAttribute(json, "msg", JsonCursor.field("msg").isString) yield
yield assert(response)(hasField("status", _.status, equalTo(Status.Unauthorized)))
assert(errorType)(equalTo("unauthorized")) && assert(errorType)(equalTo("unauthorized"))
&& assert(errorMsg)(equalTo("invalid password")) }
} )
) }
}
override def spec = suite("/login page handler")( override def spec = suite("/login page handler")(
requestsSpec, requestsSpec,
loginSpec loginSpec
).provide( ).provide(
db.datasourceLayer, db.datasourceLayer,
db.contextLayer, db.contextLayer,
auth, auth,
ConnectionPool.fixed(1), ConnectionPool.fixed(1),
Scope.default, Scope.default,
ClientConfig.default, ClientConfig.default,
Client.live) Client.live)
} }