fixing bugs
continuous-integration/drone/push Build is failing Details

dev
Override-6 2 years ago
parent b22a1230c7
commit 7b3be31271

@ -4,7 +4,7 @@ import org.apache.logging.log4j.LogManager
import org.tbasket.auth.Authenticator import org.tbasket.auth.Authenticator
import org.tbasket.config.{FileServerConfig, ServerConfig} import org.tbasket.config.{FileServerConfig, ServerConfig}
import org.tbasket.data.Database import org.tbasket.data.Database
import org.tbasket.dispatch.PageDispatcher import org.tbasket.dispatch.ResourceDispatcher
import org.tbasket.endpoint.Endpoint import org.tbasket.endpoint.Endpoint
import pdi.jwt.algorithms.JwtAsymmetricAlgorithm import pdi.jwt.algorithms.JwtAsymmetricAlgorithm
import zio.* import zio.*
@ -33,7 +33,7 @@ object Main extends ZIOAppDefault:
} }
private def setupPageDispatcher(config: ServerConfig) = ZIO.attempt { private def setupPageDispatcher(config: ServerConfig) = ZIO.attempt {
val dispatcher = new PageDispatcher(config.pagesLocation) val dispatcher = new ResourceDispatcher(config.pagesLocation)
ZLayer.succeed(dispatcher) ZLayer.succeed(dispatcher)
} }

@ -9,7 +9,7 @@ import zio.stream.ZStream
import java.nio.file.{Files, Path} import java.nio.file.{Files, Path}
import scala.collection.mutable import scala.collection.mutable
class PageDispatcher(pagesLocation: Path) { class ResourceDispatcher(pagesLocation: Path) {
private val resources = resolveResources private val resources = resolveResources
@ -25,7 +25,6 @@ class PageDispatcher(pagesLocation: Path) {
ZIO.attempt(Response.status(status)) ZIO.attempt(Response.status(status))
} }
private def resolveResources: Map[String, Body] = { private def resolveResources: Map[String, Body] = {
val map = mutable.HashMap.empty[String, Body] val map = mutable.HashMap.empty[String, Body]
@ -35,7 +34,7 @@ class PageDispatcher(pagesLocation: Path) {
.forEach { .forEach {
case d if Files.isDirectory(d) => resolveAll(d) case d if Files.isDirectory(d) => resolveAll(d)
case f => case f =>
val body = Body.fromStream(ZStream.fromPath(f)) val body = Body.fromFile(f.toFile)
val fileName = f.toString val fileName = f.toString
map.put(fileName, body) map.put(fileName, body)
val extension = fileName.drop(fileName.indexOf('.')) val extension = fileName.drop(fileName.indexOf('.'))

@ -8,7 +8,7 @@ import io.netty.handler.codec.http.HttpMethod
import org.apache.logging.log4j.{LogManager, Logger} 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.dispatch.PageDispatcher import org.tbasket.dispatch.ResourceDispatcher
import org.tbasket.endpoint.Endpoint.{Log, app} import org.tbasket.endpoint.Endpoint.{Log, app}
import org.tbasket.error.* import org.tbasket.error.*
import org.tbasket.handler.HandlerUtils.errorBody import org.tbasket.handler.HandlerUtils.errorBody
@ -37,7 +37,7 @@ class Endpoint(port: Int):
Server.install(app).flatMap { port => Server.install(app).flatMap { port =>
Log.info(s"Listening API entries on $port") Log.info(s"Listening API entries on $port")
ZIO.never ZIO.never
}.provideSome[DatabaseContext & Authenticator & DataSource & PageDispatcher]( }.provideSome[DatabaseContext & Authenticator & DataSource & ResourceDispatcher](
Scope.default, Scope.default,
serverConfigLayer, serverConfigLayer,
ConnectionPool.fixed(4), ConnectionPool.fixed(4),
@ -57,7 +57,7 @@ object Endpoint:
RegisterPageHandler.post(r) RegisterPageHandler.post(r)
case r@GET -> _ => case r@GET -> _ =>
ZIO.serviceWithZIO[PageDispatcher](_.send(r)) ZIO.serviceWithZIO[ResourceDispatcher](_.send(r))
case r@method -> path => case r@method -> path =>
val ipInsights = r.remoteAddress val ipInsights = r.remoteAddress

@ -1,7 +1,6 @@
package org.tbasket.error package org.tbasket.error
import org.apache.logging.log4j.{LogManager, Logger} import org.apache.logging.log4j.{LogManager, Logger}
import org.tbasket.handler.HandlerUtils.errorBody
import zio.http.Response import zio.http.Response
import zio.http.model.Status import zio.http.model.Status
import zio.{Task, ZIO} 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 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

@ -1,21 +1,37 @@
package org.tbasket.handler package org.tbasket.handler
import io.netty.handler.codec.http.QueryStringDecoder
import org.tbasket.error.* import org.tbasket.error.*
import zio.{Task, ZIO}
import zio.http.Body import zio.http.Body
import zio.http.api.openapi.OpenAPI.Parameter.QueryStyle.Form
import zio.json.* import zio.json.*
import zio.json.ast.{Json, JsonCursor} 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 import scala.language.reflectiveCalls
object HandlerUtils { object HandlerUtils {
def parseAttribute[V, T <: Json {def value: V}](json: Json, name: String, cursor: JsonCursor[Json, T]): Task[V] = 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)) ZIO.fromEither(json.get[T](cursor).map(_.value))
.mapError(InvalidRequest(s"Missing or invalid field $name.", _)) .mapError(InvalidRequest(s"Missing or invalid field $name.", _))
def parseAttributeOpt[V, T <: Json {def value: V}](json: Json, name: String, cursor: JsonCursor[Json, T]) =
ZIO.fromEither(json.get[T](cursor).map(_.value)).option
def errorBody(errorType: String, msg: String) = Body.fromString(s"""{"error": "$errorType","msg": "$msg"}""") def errorBody(errorType: String, msg: String) = Body.fromString(s"""{"error": "$errorType","msg": "$msg"}""")
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."))
} }

@ -8,7 +8,7 @@ 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.* import org.tbasket.error.*
import org.tbasket.handler.HandlerUtils.errorBody import org.tbasket.handler.HandlerUtils.*
import zio.* import zio.*
import zio.http.* import zio.http.*
import zio.http.model.{Cookie, Header, Headers, Status} 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") 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 => ZIO.serviceWithZIO[Authenticator] { auth =>
for val zio = for
mail <- HandlerUtils.parseAttribute(json, "email", JsonCursor.field("email").isString) mail <- search(params)("email")
password <- HandlerUtils.parseAttribute(json, "password", JsonCursor.field("password").isString) password <- search(params)("password")
user <- auth.loginUser(mail, password) user <- auth.loginUser(mail, password)
yield user yield user
zio
} }
private def tryPost(request: Request) = private def tryPost(request: Request) =
for for
body <- request params <- parseRequestForm(request.body)
.body user <- getUser(params)
.asString
.mapError(s =>
InvalidRequest("Invalid request body", s.getMessage)
)
json <- ZIO.fromEither(body.fromJson[Json])
.mapError(InvalidRequest("Invalid JSON body", _))
user <- getUser(json)
jwt <- ZIO.serviceWithZIO[Authenticator](_.requestNewJwt(user)) jwt <- ZIO.serviceWithZIO[Authenticator](_.requestNewJwt(user))
yield Response( yield Response(
status = Status.Found, status = Status.Found,

@ -4,7 +4,7 @@ import org.apache.logging.log4j.{LogManager, Logger}
import org.tbasket.auth.Authenticator import org.tbasket.auth.Authenticator
import org.tbasket.data.User import org.tbasket.data.User
import org.tbasket.error.* import org.tbasket.error.*
import org.tbasket.handler.HandlerUtils.{errorBody, parseAttribute} import org.tbasket.handler.HandlerUtils.{errorBody, parseAttribute, parseRequestForm, search}
import zio.ZIO import zio.ZIO
import zio.http.model.{Cookie, Headers, Status} import zio.http.model.{Cookie, Headers, Status}
import zio.http.{Body, Request, Response, model} import zio.http.{Body, Request, Response, model}
@ -17,16 +17,12 @@ object RegisterPageHandler extends PageHandler {
private def tryPost(request: Request) = private def tryPost(request: Request) =
for for
body <- request.body.asString params <- parseRequestForm(request.body)
.mapError(e => InvalidRequest("Invalid request body", e.getMessage))
json <- ZIO.fromEither(body.fromJson[Json]) name <- search(params)("name")
.mapError(InvalidRequest("Invalid JSON body", _)) forename <- search(params)("forename")
mail <- search(params)("email")
name <- parseAttribute(json, "name", JsonCursor.field("name").isString) password <- search(params)("password")
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)) user <- ZIO.serviceWithZIO[Authenticator](_.registerUser(name, forename, mail, password))
jwt <- ZIO.serviceWithZIO[Authenticator](_.requestNewJwt(user)) jwt <- ZIO.serviceWithZIO[Authenticator](_.requestNewJwt(user))
@ -46,11 +42,12 @@ object RegisterPageHandler extends PageHandler {
status = Status.ExpectationFailed, status = Status.ExpectationFailed,
body = errorBody("invalid password", msg) body = errorBody("invalid password", msg)
)) ))
case UserAlreadyRegistered(msg) => ZIO.attempt(Response( case UserAlreadyRegistered(msg) =>
status = Status.NotAcceptable, ZIO.attempt(Response(
body = errorBody("already registered", msg), status = Status.NotAcceptable,
headers = Headers.location("/login") //the account already exists so we move the user to login page body = errorBody("already registered", msg),
)) headers = Headers.location("/login") //the account already exists so we move the user to login page
))
} }
} }

@ -57,6 +57,7 @@ object Main extends ZIOAppDefault:
private def onStart(port: Int) = private def onStart(port: Int) =
Console.printLine(s"JWT AppToken open on port $port") *> ZIO.attempt { Console.printLine(s"JWT AppToken open on port $port") *> ZIO.attempt {
Files.deleteIfExists(EmitterPresenceHook)
Files.createFile(EmitterPresenceHook) Files.createFile(EmitterPresenceHook)
} }

@ -3,6 +3,7 @@ package org.tbasket.test
import org.tbasket.auth.Authenticator import org.tbasket.auth.Authenticator
import org.tbasket.config.ServerConfig import org.tbasket.config.ServerConfig
import org.tbasket.data.Database import org.tbasket.data.Database
import org.tbasket.dispatch.ResourceDispatcher
import zio.{Task, ZLayer} import zio.{Task, ZLayer}
import java.nio.file.{Files, Path} import java.nio.file.{Files, Path}
@ -21,6 +22,8 @@ object TestLayers {
ZLayer.succeed(auth) ZLayer.succeed(auth)
} }
val resp = ZLayer.succeed(new ResourceDispatcher(TestServerConfig.pagesLocation))
val db = { val db = {
//ensure that the test table is always new in order to make tests on the same dataset all the time. //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")) Files.deleteIfExists(Path.of("/tmp/test-database.sqlite"))

@ -29,5 +29,5 @@ object TestServerConfig extends ServerConfig {
override def databaseConfigName: String = "test-database" override def databaseConfigName: String = "test-database"
override def pagesLocation: io.Path = Path.of("www") override def pagesLocation: Path = Path.of("www")
} }

@ -1,15 +1,25 @@
package org.tbasket.test package org.tbasket.test
import io.netty.handler.codec.http.QueryStringEncoder
import zio.* import zio.*
import zio.http.{Body, Response} import zio.http.{Body, Response}
import zio.json.* import zio.json.*
import zio.json.ast.Json import zio.json.ast.Json
import scala.language.implicitConversions
object TestUtils { object TestUtils {
def getJsonBody(r: Response): Task[Json] = { def getJsonBody(r: Response): Task[Json] = {
for for
body <- r.body.asString 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 yield json
} }
implicit def makeFormBody(params: (String, String)*): Body = {
val qse = new QueryStringEncoder("")
params.foreach(qse.addParam(_, _))
Body.fromString(qse.toString)
}
} }

@ -9,7 +9,7 @@ import org.tbasket.endpoint.Endpoint.handle
import org.tbasket.error.* 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.test.TestUtils.getJsonBody import org.tbasket.test.TestUtils.{getJsonBody, makeFormBody}
import org.tbasket.test.{TestLayers, TestUtils} import org.tbasket.test.{TestLayers, TestUtils}
import zio.* import zio.*
import zio.http.* import zio.http.*
@ -27,9 +27,9 @@ object LoginPageHandlerTests extends TBasketPageSpec("/login") {
private def requestsSpec = suite("bad request 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" -> makeFormBody("password" -> "bouhours"),
"with no password attribute" -> Body.fromString("""{"email":"valid.mail@x.y"}"""), "with no password attribute" -> makeFormBody("email" -> "valid.email@not.very"),
"with invalid json" -> Body.fromString("""this is a corrupted json""") "with invalid form data" -> Body.fromString("""this is a corrupted form data""")
)).map(_.map { case (name, body) => )).map(_.map { case (name, body) =>
test(name) { test(name) {
for for
@ -47,7 +47,7 @@ object LoginPageHandlerTests extends TBasketPageSpec("/login") {
suite("login situation tests")( suite("login situation tests")(
test("login with unknown account") { test("login with unknown account") {
for 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) json <- getJsonBody(response)
errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString) errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString)
yield yield
@ -59,7 +59,7 @@ object LoginPageHandlerTests extends TBasketPageSpec("/login") {
test("login with known account") { test("login with known account") {
for 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 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
@ -67,9 +67,8 @@ object LoginPageHandlerTests extends TBasketPageSpec("/login") {
}, },
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"}"""
for 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) _ <- TestClock.adjust(1.seconds)
response <- ZIO.fromFiber(fiber) response <- ZIO.fromFiber(fiber)
json <- getJsonBody(response) json <- getJsonBody(response)

@ -1,26 +1,26 @@
package org.tbasket.test.pages package org.tbasket.test.pages
import org.tbasket.endpoint.Endpoint.handle import org.tbasket.endpoint.Endpoint.handle
import org.tbasket.handler.HandlerUtils.parseAttribute import org.tbasket.handler.HandlerUtils.{parseAttribute, parseAttributeOpt}
import org.tbasket.test.TestUtils import org.tbasket.test.TestUtils
import org.tbasket.test.TestUtils.getJsonBody import org.tbasket.test.TestUtils.*
import org.tbasket.test.pages.RegisterPageHandlerTests.test import org.tbasket.test.pages.RegisterPageHandlerTests.test
import zio.* import zio.*
import zio.http.* import zio.http.*
import zio.http.model.{HeaderNames, Headers, Status} import zio.http.model.{HeaderNames, Headers, Status}
import zio.json.ast.JsonCursor import zio.json.ast.JsonCursor
import zio.test.Assertion.*
import zio.test.* import zio.test.*
import zio.test.Assertion.*
object RegisterPageHandlerTests extends TBasketPageSpec("/register") { object RegisterPageHandlerTests extends TBasketPageSpec("/register") {
private def requestsSpec = suite("bad request tests")( private def requestsSpec = suite("bad request tests")(
ZIO.attempt(Map( ZIO.attempt(Map( //TODO test all wrong combinations
"empty packet" -> Body.empty, "empty packet" -> Body.empty,
"with no mail attribute" -> Body.fromString("""{"password":"1234"}"""), "with no mail attribute" -> makeFormBody("password" -> "123445678"),
"with no password attribute" -> Body.fromString("""{"email":"valid.mail@x.y"}"""), "with no password attribute" -> makeFormBody("email" -> "valid.mail@x.y"),
"with invalid json" -> Body.fromString("""this is a corrupted json""") "with invalid form data" -> Body.fromString("""this is a corrupted form data""")
)).map(_.map { case (name, body) => )).map(_.map { case (name, body) =>
test(name) { test(name) {
for for
@ -35,30 +35,29 @@ object RegisterPageHandlerTests extends TBasketPageSpec("/register") {
) )
private def registerSpec = suite("register tests")( private def registerSpec = suite("register tests")(
test("normal register") { test("register then try register again") {
for (for
resp <- handle(Request.post(Body.fromString(s"""{"name":"tuaillon","forename":"leo","email":"leo.tuaillon@etu.uca.fr","password":"bouhours"}"""), url)) resp <- handle(Request.post(makeFormBody("name" -> "tuaillon", "forename" -> "leo", "email" -> "leo.tuaillon@etu.uca.fr", "password" -> "bouhours"), url))
yield yield
assert(resp)(hasField("status", _.status, equalTo(Status.Found))) 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("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, exists(hasField("key", _.key, equalTo(HeaderNames.setCookie)))))
&& assert(resp)(hasField("headers", _.headers, contains(Headers.location("/")))) && assert(resp)(hasField("headers", _.headers, contains(Headers.location("/"))))
}, )
test("register again with same credentials") { *>
for (for
resp <- handle(Request.post(Body.fromString(s"""{"name":"tuaillon","forename":"leo","email":"leo.tuaillon@etu.uca.fr","password":"bouhours"}"""), url)) resp <- handle(Request.post(makeFormBody("name" -> "tuaillon", "forename" -> "leo", "email" -> "leo.tuaillon@etu.uca.fr", "password" -> "bouhours"), url))
json <- getJsonBody(resp) json <- getJsonBody(resp)
errorType <- parseAttribute(json, "error", JsonCursor.field("error").isString) errorType <- parseAttributeOpt(json, "error", JsonCursor.field("error").isString)
yield yield
assert(resp)(hasField("status", _.status, equalTo(Status.NotAcceptable))) assert(resp)(hasField("status", _.status, equalTo(Status.NotAcceptable)))
&& assert(errorType)(equalTo("already registered")) && assert(errorType)(isSome(equalTo("already registered")))
&& assert(resp)(hasField("headers", _.headers, contains(Headers.location("/login")))) && assert(resp)(hasField("headers", _.headers, contains(Headers.location("/login")))))
} },
) )
override def tspec = suite("/login page handler")( override def tspec = suite("/register page handler")(
requestsSpec, requestsSpec,
registerSpec registerSpec
) )

@ -5,6 +5,7 @@ import io.getquill.context.qzio.ZioJdbcContext
import io.getquill.context.sql.idiom.SqlIdiom import io.getquill.context.sql.idiom.SqlIdiom
import org.tbasket.auth.Authenticator import org.tbasket.auth.Authenticator
import org.tbasket.data.DatabaseContext import org.tbasket.data.DatabaseContext
import org.tbasket.dispatch.ResourceDispatcher
import zio.test.{Spec, TestEnvironment, ZIOSpecDefault} import zio.test.{Spec, TestEnvironment, ZIOSpecDefault}
import org.tbasket.handler.LoginPageHandler.post import org.tbasket.handler.LoginPageHandler.post
import zio.* import zio.*
@ -20,12 +21,13 @@ abstract class TBasketPageSpec(location: String) extends ZIOSpecDefault {
case Left(value) => throw value case Left(value) => throw value
case Right(url) => url 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( final override def spec = tspec.provide(
db.datasourceLayer, db.datasourceLayer,
db.contextLayer, db.contextLayer,
auth, auth,
resp,
ConnectionPool.fixed(1), ConnectionPool.fixed(1),
Scope.default, Scope.default,
ClientConfig.default, ClientConfig.default,