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

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

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

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

@ -1,10 +1,12 @@
package org.tbasket.error
enum JwtException extends ExceptionEnum {
case InvalidJwt(cause: String)
case ExpiredJwt
case InvalidEmitterResponse
case EmitterInternalError
}
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)

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

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

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

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

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