From 7b3be312717949fd4ea9a24e19a2658377bfb08c Mon Sep 17 00:00:00 2001 From: Override-6 Date: Fri, 3 Feb 2023 23:20:08 +0100 Subject: [PATCH] fixing bugs --- Core/src/org/tbasket/Main.scala | 4 +- ...patcher.scala => ResourceDispatcher.scala} | 5 +-- Core/src/org/tbasket/endpoint/Endpoint.scala | 6 +-- .../org/tbasket/error/OtherExceptions.scala | 3 +- .../org/tbasket/handler/HandlerUtils.scala | 20 +++++++++- .../tbasket/handler/LoginPageHandler.scala | 23 ++++------- .../tbasket/handler/RegisterPageHandler.scala | 27 ++++++------- JWTEmitter/src/org/tbasket/jwt/Main.scala | 1 + tests/src/org/tbasket/test/TestLayers.scala | 3 ++ .../org/tbasket/test/TestServerConfig.scala | 2 +- tests/src/org/tbasket/test/TestUtils.scala | 12 +++++- .../test/pages/LoginPageHandlerTests.scala | 15 ++++--- .../test/pages/RegisterPageHandlerTests.scala | 39 +++++++++---------- .../tbasket/test/pages/TBasketPageSpec.scala | 4 +- 14 files changed, 91 insertions(+), 73 deletions(-) rename Core/src/org/tbasket/dispatch/{PageDispatcher.scala => ResourceDispatcher.scala} (92%) diff --git a/Core/src/org/tbasket/Main.scala b/Core/src/org/tbasket/Main.scala index 778b55d..7313895 100644 --- a/Core/src/org/tbasket/Main.scala +++ b/Core/src/org/tbasket/Main.scala @@ -4,7 +4,7 @@ import org.apache.logging.log4j.LogManager import org.tbasket.auth.Authenticator import org.tbasket.config.{FileServerConfig, ServerConfig} import org.tbasket.data.Database -import org.tbasket.dispatch.PageDispatcher +import org.tbasket.dispatch.ResourceDispatcher import org.tbasket.endpoint.Endpoint import pdi.jwt.algorithms.JwtAsymmetricAlgorithm import zio.* @@ -33,7 +33,7 @@ object Main extends ZIOAppDefault: } private def setupPageDispatcher(config: ServerConfig) = ZIO.attempt { - val dispatcher = new PageDispatcher(config.pagesLocation) + val dispatcher = new ResourceDispatcher(config.pagesLocation) ZLayer.succeed(dispatcher) } diff --git a/Core/src/org/tbasket/dispatch/PageDispatcher.scala b/Core/src/org/tbasket/dispatch/ResourceDispatcher.scala similarity index 92% rename from Core/src/org/tbasket/dispatch/PageDispatcher.scala rename to Core/src/org/tbasket/dispatch/ResourceDispatcher.scala index ec3abf8..bc7324f 100644 --- a/Core/src/org/tbasket/dispatch/PageDispatcher.scala +++ b/Core/src/org/tbasket/dispatch/ResourceDispatcher.scala @@ -9,7 +9,7 @@ import zio.stream.ZStream import java.nio.file.{Files, Path} import scala.collection.mutable -class PageDispatcher(pagesLocation: Path) { +class ResourceDispatcher(pagesLocation: Path) { private val resources = resolveResources @@ -25,7 +25,6 @@ class PageDispatcher(pagesLocation: Path) { ZIO.attempt(Response.status(status)) } - private def resolveResources: Map[String, Body] = { val map = mutable.HashMap.empty[String, Body] @@ -35,7 +34,7 @@ class PageDispatcher(pagesLocation: Path) { .forEach { case d if Files.isDirectory(d) => resolveAll(d) case f => - val body = Body.fromStream(ZStream.fromPath(f)) + val body = Body.fromFile(f.toFile) val fileName = f.toString map.put(fileName, body) val extension = fileName.drop(fileName.indexOf('.')) diff --git a/Core/src/org/tbasket/endpoint/Endpoint.scala b/Core/src/org/tbasket/endpoint/Endpoint.scala index be1c9aa..b44c0f0 100644 --- a/Core/src/org/tbasket/endpoint/Endpoint.scala +++ b/Core/src/org/tbasket/endpoint/Endpoint.scala @@ -8,7 +8,7 @@ import io.netty.handler.codec.http.HttpMethod import org.apache.logging.log4j.{LogManager, Logger} import org.tbasket.auth.Authenticator import org.tbasket.data.DatabaseContext -import org.tbasket.dispatch.PageDispatcher +import org.tbasket.dispatch.ResourceDispatcher import org.tbasket.endpoint.Endpoint.{Log, app} import org.tbasket.error.* import org.tbasket.handler.HandlerUtils.errorBody @@ -37,7 +37,7 @@ class Endpoint(port: Int): Server.install(app).flatMap { port => Log.info(s"Listening API entries on $port") ZIO.never - }.provideSome[DatabaseContext & Authenticator & DataSource & PageDispatcher]( + }.provideSome[DatabaseContext & Authenticator & DataSource & ResourceDispatcher]( Scope.default, serverConfigLayer, ConnectionPool.fixed(4), @@ -57,7 +57,7 @@ object Endpoint: RegisterPageHandler.post(r) case r@GET -> _ => - ZIO.serviceWithZIO[PageDispatcher](_.send(r)) + ZIO.serviceWithZIO[ResourceDispatcher](_.send(r)) case r@method -> path => val ipInsights = r.remoteAddress diff --git a/Core/src/org/tbasket/error/OtherExceptions.scala b/Core/src/org/tbasket/error/OtherExceptions.scala index 11b931f..a8d7a39 100644 --- a/Core/src/org/tbasket/error/OtherExceptions.scala +++ b/Core/src/org/tbasket/error/OtherExceptions.scala @@ -1,7 +1,6 @@ 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} @@ -13,6 +12,6 @@ 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 +case class InvalidRequest(msg: String, cause: String = "") extends Exception(msg + (if cause.nonEmpty then ":" + cause else "")) with UserException diff --git a/Core/src/org/tbasket/handler/HandlerUtils.scala b/Core/src/org/tbasket/handler/HandlerUtils.scala index 76a62a4..42ea329 100644 --- a/Core/src/org/tbasket/handler/HandlerUtils.scala +++ b/Core/src/org/tbasket/handler/HandlerUtils.scala @@ -1,21 +1,37 @@ package org.tbasket.handler +import io.netty.handler.codec.http.QueryStringDecoder import org.tbasket.error.* -import zio.{Task, ZIO} import zio.http.Body +import zio.http.api.openapi.OpenAPI.Parameter.QueryStyle.Form import zio.json.* import zio.json.ast.{Json, JsonCursor} +import zio.{Task, ZIO} +import java.net.URLDecoder +import java.nio.charset.StandardCharsets +import scala.jdk.CollectionConverters.* import scala.language.reflectiveCalls - 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 parseRequestForm(body: Body) = + (for + decoded <- body.asString.map(URLDecoder.decode(_, StandardCharsets.UTF_8.name())) + params = decoded.split('&').collect { case s"$k=$v" => (k, v)}.toMap + yield params) + .mapError(s => InvalidRequest("Invalid request body", s.getMessage)) + def search(map: Map[String, String])(name: String) = + ZIO.attempt(map.get(name)).someOrFail(InvalidRequest(s"Missing or invalid field $name.")) } diff --git a/Core/src/org/tbasket/handler/LoginPageHandler.scala b/Core/src/org/tbasket/handler/LoginPageHandler.scala index 74804f3..d08a7ba 100644 --- a/Core/src/org/tbasket/handler/LoginPageHandler.scala +++ b/Core/src/org/tbasket/handler/LoginPageHandler.scala @@ -8,7 +8,7 @@ import org.apache.logging.log4j.{LogManager, Logger} import org.tbasket.auth.Authenticator import org.tbasket.data.{DatabaseContext, User} import org.tbasket.error.* -import org.tbasket.handler.HandlerUtils.errorBody +import org.tbasket.handler.HandlerUtils.* import zio.* import zio.http.* import zio.http.model.{Cookie, Header, Headers, Status} @@ -24,28 +24,21 @@ object LoginPageHandler extends PageHandler: implicit private final val Log: Logger = LogManager.getLogger("/login") - private def getUser(json: Json) = + private def getUser(params: Map[String, String]) = ZIO.serviceWithZIO[Authenticator] { auth => - for - mail <- HandlerUtils.parseAttribute(json, "email", JsonCursor.field("email").isString) - password <- HandlerUtils.parseAttribute(json, "password", JsonCursor.field("password").isString) + val zio = for + mail <- search(params)("email") + password <- search(params)("password") user <- auth.loginUser(mail, password) yield user + zio } private def tryPost(request: Request) = for - body <- request - .body - .asString - .mapError(s => - InvalidRequest("Invalid request body", s.getMessage) - ) + params <- parseRequestForm(request.body) + user <- getUser(params) - json <- ZIO.fromEither(body.fromJson[Json]) - .mapError(InvalidRequest("Invalid JSON body", _)) - - user <- getUser(json) jwt <- ZIO.serviceWithZIO[Authenticator](_.requestNewJwt(user)) yield Response( status = Status.Found, diff --git a/Core/src/org/tbasket/handler/RegisterPageHandler.scala b/Core/src/org/tbasket/handler/RegisterPageHandler.scala index f1f6c88..934418a 100644 --- a/Core/src/org/tbasket/handler/RegisterPageHandler.scala +++ b/Core/src/org/tbasket/handler/RegisterPageHandler.scala @@ -4,7 +4,7 @@ import org.apache.logging.log4j.{LogManager, Logger} import org.tbasket.auth.Authenticator import org.tbasket.data.User import org.tbasket.error.* -import org.tbasket.handler.HandlerUtils.{errorBody, parseAttribute} +import org.tbasket.handler.HandlerUtils.{errorBody, parseAttribute, parseRequestForm, search} import zio.ZIO import zio.http.model.{Cookie, Headers, Status} import zio.http.{Body, Request, Response, model} @@ -17,16 +17,12 @@ object RegisterPageHandler extends PageHandler { private def tryPost(request: Request) = for - body <- request.body.asString - .mapError(e => InvalidRequest("Invalid request body", e.getMessage)) + params <- parseRequestForm(request.body) - 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) + name <- search(params)("name") + forename <- search(params)("forename") + mail <- search(params)("email") + password <- search(params)("password") user <- ZIO.serviceWithZIO[Authenticator](_.registerUser(name, forename, mail, password)) jwt <- ZIO.serviceWithZIO[Authenticator](_.requestNewJwt(user)) @@ -46,11 +42,12 @@ object RegisterPageHandler extends PageHandler { 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 - )) + 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/JWTEmitter/src/org/tbasket/jwt/Main.scala b/JWTEmitter/src/org/tbasket/jwt/Main.scala index eaa1426..e2b0e7e 100644 --- a/JWTEmitter/src/org/tbasket/jwt/Main.scala +++ b/JWTEmitter/src/org/tbasket/jwt/Main.scala @@ -57,6 +57,7 @@ object Main extends ZIOAppDefault: private def onStart(port: Int) = Console.printLine(s"JWT AppToken open on port $port") *> ZIO.attempt { + Files.deleteIfExists(EmitterPresenceHook) Files.createFile(EmitterPresenceHook) } diff --git a/tests/src/org/tbasket/test/TestLayers.scala b/tests/src/org/tbasket/test/TestLayers.scala index bffe1e0..d943c8c 100644 --- a/tests/src/org/tbasket/test/TestLayers.scala +++ b/tests/src/org/tbasket/test/TestLayers.scala @@ -3,6 +3,7 @@ package org.tbasket.test import org.tbasket.auth.Authenticator import org.tbasket.config.ServerConfig import org.tbasket.data.Database +import org.tbasket.dispatch.ResourceDispatcher import zio.{Task, ZLayer} import java.nio.file.{Files, Path} @@ -21,6 +22,8 @@ object TestLayers { ZLayer.succeed(auth) } + val resp = ZLayer.succeed(new ResourceDispatcher(TestServerConfig.pagesLocation)) + val db = { //ensure that the test table is always new in order to make tests on the same dataset all the time. Files.deleteIfExists(Path.of("/tmp/test-database.sqlite")) diff --git a/tests/src/org/tbasket/test/TestServerConfig.scala b/tests/src/org/tbasket/test/TestServerConfig.scala index f3f3f36..7502647 100644 --- a/tests/src/org/tbasket/test/TestServerConfig.scala +++ b/tests/src/org/tbasket/test/TestServerConfig.scala @@ -29,5 +29,5 @@ object TestServerConfig extends ServerConfig { override def databaseConfigName: String = "test-database" - override def pagesLocation: io.Path = Path.of("www") + override def pagesLocation: Path = Path.of("www") } diff --git a/tests/src/org/tbasket/test/TestUtils.scala b/tests/src/org/tbasket/test/TestUtils.scala index ba69402..bf1ee54 100644 --- a/tests/src/org/tbasket/test/TestUtils.scala +++ b/tests/src/org/tbasket/test/TestUtils.scala @@ -1,15 +1,25 @@ package org.tbasket.test +import io.netty.handler.codec.http.QueryStringEncoder import zio.* import zio.http.{Body, Response} import zio.json.* import zio.json.ast.Json +import scala.language.implicitConversions + 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) + json <- ZIO.fromEither(body.fromJson[Json]).orElseSucceed(Json.Obj()) yield json } + + implicit def makeFormBody(params: (String, String)*): Body = { + val qse = new QueryStringEncoder("") + params.foreach(qse.addParam(_, _)) + Body.fromString(qse.toString) + } + } diff --git a/tests/src/org/tbasket/test/pages/LoginPageHandlerTests.scala b/tests/src/org/tbasket/test/pages/LoginPageHandlerTests.scala index e53be97..69d4bed 100644 --- a/tests/src/org/tbasket/test/pages/LoginPageHandlerTests.scala +++ b/tests/src/org/tbasket/test/pages/LoginPageHandlerTests.scala @@ -9,7 +9,7 @@ import org.tbasket.endpoint.Endpoint.handle import org.tbasket.error.* import org.tbasket.handler.HandlerUtils.parseAttribute import org.tbasket.handler.LoginPageHandler -import org.tbasket.test.TestUtils.getJsonBody +import org.tbasket.test.TestUtils.{getJsonBody, makeFormBody} import org.tbasket.test.{TestLayers, TestUtils} import zio.* import zio.http.* @@ -27,9 +27,9 @@ object LoginPageHandlerTests extends TBasketPageSpec("/login") { 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""") + "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 @@ -47,7 +47,7 @@ object LoginPageHandlerTests extends TBasketPageSpec("/login") { suite("login situation tests")( test("login with unknown account") { for - response <- handle(Request.post(Body.fromString("""{"password":"123456","email":"unknownaccount@gmail.com"}"""), url)) + response <- handle(Request.post(makeFormBody("password" -> "bouhours", "email" -> "unknownaccount@gmail.com"), url)) json <- getJsonBody(response) errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString) yield @@ -59,7 +59,7 @@ object LoginPageHandlerTests extends TBasketPageSpec("/login") { test("login with known account") { for - response <- handle(Request.post(Body.fromString("""{"password":"123456","email":"maximebatista18@gmail.com"}"""), url)) + response <- handle(Request.post(makeFormBody("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 @@ -67,9 +67,8 @@ object LoginPageHandlerTests extends TBasketPageSpec("/login") { }, 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 + fiber <- handle(Request.post(makeFormBody("password" -> "wrong", "email" -> "maximebatista18@gmail.com"), url)).fork _ <- TestClock.adjust(1.seconds) response <- ZIO.fromFiber(fiber) json <- getJsonBody(response) diff --git a/tests/src/org/tbasket/test/pages/RegisterPageHandlerTests.scala b/tests/src/org/tbasket/test/pages/RegisterPageHandlerTests.scala index 26e707b..b803795 100644 --- a/tests/src/org/tbasket/test/pages/RegisterPageHandlerTests.scala +++ b/tests/src/org/tbasket/test/pages/RegisterPageHandlerTests.scala @@ -1,26 +1,26 @@ package org.tbasket.test.pages import org.tbasket.endpoint.Endpoint.handle -import org.tbasket.handler.HandlerUtils.parseAttribute +import org.tbasket.handler.HandlerUtils.{parseAttribute, parseAttributeOpt} import org.tbasket.test.TestUtils -import org.tbasket.test.TestUtils.getJsonBody +import org.tbasket.test.TestUtils.* import org.tbasket.test.pages.RegisterPageHandlerTests.test import zio.* import zio.http.* import zio.http.model.{HeaderNames, Headers, Status} import zio.json.ast.JsonCursor -import zio.test.Assertion.* import zio.test.* +import zio.test.Assertion.* object RegisterPageHandlerTests extends TBasketPageSpec("/register") { private def requestsSpec = suite("bad request tests")( - ZIO.attempt(Map( + ZIO.attempt(Map( //TODO test all wrong combinations "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""") + "with no mail attribute" -> makeFormBody("password" -> "123445678"), + "with no password attribute" -> makeFormBody("email" -> "valid.mail@x.y"), + "with invalid form data" -> Body.fromString("""this is a corrupted form data""") )).map(_.map { case (name, body) => test(name) { for @@ -35,30 +35,29 @@ object RegisterPageHandlerTests extends TBasketPageSpec("/register") { ) private def registerSpec = suite("register tests")( - test("normal register") { - for - resp <- handle(Request.post(Body.fromString(s"""{"name":"tuaillon","forename":"leo","email":"leo.tuaillon@etu.uca.fr","password":"bouhours"}"""), url)) - + test("register then try register again") { + (for + resp <- handle(Request.post(makeFormBody("name" -> "tuaillon", "forename" -> "leo", "email" -> "leo.tuaillon@etu.uca.fr", "password" -> "bouhours"), url)) yield assert(resp)(hasField("status", _.status, equalTo(Status.Found))) && assert(resp)(hasField("body", _.body, equalTo(Body.empty))) //TODO assert that the cookie name is JWT && assert(resp)(hasField("headers", _.headers, exists(hasField("key", _.key, equalTo(HeaderNames.setCookie))))) && assert(resp)(hasField("headers", _.headers, contains(Headers.location("/")))) - }, - test("register again with same credentials") { - for - resp <- handle(Request.post(Body.fromString(s"""{"name":"tuaillon","forename":"leo","email":"leo.tuaillon@etu.uca.fr","password":"bouhours"}"""), url)) + ) + *> + (for + resp <- handle(Request.post(makeFormBody("name" -> "tuaillon", "forename" -> "leo", "email" -> "leo.tuaillon@etu.uca.fr", "password" -> "bouhours"), url)) json <- getJsonBody(resp) - errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString) + errorType <- parseAttributeOpt(json, "error", JsonCursor.field("error").isString) yield assert(resp)(hasField("status", _.status, equalTo(Status.NotAcceptable))) - && assert(errorType)(equalTo("already registered")) - && assert(resp)(hasField("headers", _.headers, contains(Headers.location("/login")))) - } + && assert(errorType)(isSome(equalTo("already registered"))) + && assert(resp)(hasField("headers", _.headers, contains(Headers.location("/login"))))) + }, ) - override def tspec = suite("/login page handler")( + override def tspec = suite("/register page handler")( requestsSpec, registerSpec ) diff --git a/tests/src/org/tbasket/test/pages/TBasketPageSpec.scala b/tests/src/org/tbasket/test/pages/TBasketPageSpec.scala index b86e4cd..5438d0e 100644 --- a/tests/src/org/tbasket/test/pages/TBasketPageSpec.scala +++ b/tests/src/org/tbasket/test/pages/TBasketPageSpec.scala @@ -5,6 +5,7 @@ import io.getquill.context.qzio.ZioJdbcContext import io.getquill.context.sql.idiom.SqlIdiom import org.tbasket.auth.Authenticator import org.tbasket.data.DatabaseContext +import org.tbasket.dispatch.ResourceDispatcher import zio.test.{Spec, TestEnvironment, ZIOSpecDefault} import org.tbasket.handler.LoginPageHandler.post import zio.* @@ -20,12 +21,13 @@ abstract class TBasketPageSpec(location: String) extends ZIOSpecDefault { case Left(value) => throw value case Right(url) => url - protected def tspec: Spec[DataSource & ClientConfig & Authenticator & ConnectionPool & Scope & DatabaseContext & Client, Any] + protected def tspec: Spec[DataSource & ClientConfig & Authenticator & ConnectionPool & Scope & DatabaseContext & Client & ResourceDispatcher, Any] final override def spec = tspec.provide( db.datasourceLayer, db.contextLayer, auth, + resp, ConnectionPool.fixed(1), Scope.default, ClientConfig.default,