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

@ -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('.'))

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

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

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

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

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

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

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

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

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

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

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

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