diff --git a/app/src/main/scala/Executor.scala b/app/src/main/scala/Executor.scala index 2f94540..4a42be3 100644 --- a/app/src/main/scala/Executor.scala +++ b/app/src/main/scala/Executor.scala @@ -60,9 +60,9 @@ object Executor: ref.flatModify: coll => coll get workId match case None => - coll -> notFound(workId, data.key) - case Some(move) if move.isAcquiredBy(data.key) => - data.move.uci match + coll -> notFound(workId, data.fishnet.apikey) + case Some(move) if move.isAcquiredBy(data.fishnet.apikey) => + data.move.bestmove.uci match case Some(uci) => coll - move.id -> (success(move) >> client.send(Lila.Move( move.request.id, @@ -71,9 +71,9 @@ object Executor: ))) case _ => updateOrGiveUp(coll, move.invalid) -> - failure(move, data.key, new Exception("Missing move")) + failure(move, data.fishnet.apikey, new Exception("Missing move")) case Some(move) => - coll -> notAcquired(move, data.key) + coll -> notAcquired(move, data.fishnet.apikey) def clean(since: Instant): IO[Unit] = ref.update: coll => diff --git a/app/src/main/scala/http/FishnetRoutes.scala b/app/src/main/scala/http/FishnetRoutes.scala index b309a3e..7e257e7 100644 --- a/app/src/main/scala/http/FishnetRoutes.scala +++ b/app/src/main/scala/http/FishnetRoutes.scala @@ -30,7 +30,7 @@ final class FishnetRoutes(executor: Executor) extends Http4sDsl[IO]: req .decode[Fishnet.PostMove]: move => executor.move(id, move) - >> executor.acquire(move.key) + >> executor.acquire(move.fishnet.apikey) .map(_.map(_.toResponse)) .flatMap(_.fold(NoContent())(Ok(_))) .recoverWith: diff --git a/app/src/main/scala/http/HttpApi.scala b/app/src/main/scala/http/HttpApi.scala index 19f2473..0c821f4 100644 --- a/app/src/main/scala/http/HttpApi.scala +++ b/app/src/main/scala/http/HttpApi.scala @@ -24,7 +24,7 @@ final class HttpApi(executor: Executor, healthCheck: HealthCheck): private val middleware = autoSlash andThen timeout private val loggers: HttpApp[IO] => HttpApp[IO] = - RequestLogger.httpApp[IO](true, true) andThen - ResponseLogger.httpApp[IO, Request[IO]](true, true) + RequestLogger.httpApp[IO](false, true) andThen + ResponseLogger.httpApp[IO, Request[IO]](false, true) val httpApp: HttpApp[IO] = loggers(middleware(routes).orNotFound) diff --git a/app/src/main/scala/model.scala b/app/src/main/scala/model.scala index 9018fd8..8dba98f 100644 --- a/app/src/main/scala/model.scala +++ b/app/src/main/scala/model.scala @@ -47,7 +47,8 @@ object Fishnet: case class Acquire(fishnet: Fishnet) derives Codec.AsObject case class Fishnet(version: String, apikey: ClientKey) derives Codec.AsObject - case class PostMove(key: ClientKey, move: BestMove) derives Codec.AsObject + case class PostMove(fishnet: Fishnet, move: Move) derives Codec.AsObject + case class Move(bestmove: BestMove) case class Work(id: WorkId, level: Int, clock: Option[Lila.Clock], `type`: String = "move") derives Encoder.AsObject diff --git a/app/src/test/scala/ExecutorTest.scala b/app/src/test/scala/ExecutorTest.scala index bf0b1d4..2de599b 100644 --- a/app/src/test/scala/ExecutorTest.scala +++ b/app/src/test/scala/ExecutorTest.scala @@ -1,12 +1,11 @@ package lila.fishnet import weaver.* -import weaver.scalacheck.Checkers import cats.effect.IO import cats.effect.kernel.Ref import java.time.Instant -object ExecutorTest extends SimpleIOSuite with Checkers: +object ExecutorTest extends SimpleIOSuite: val request: Lila.Request = Lila.Request( id = GameId("id"), @@ -19,8 +18,8 @@ object ExecutorTest extends SimpleIOSuite with Checkers: val key = ClientKey("key") - val validMove = Fishnet.PostMove(key, BestMove("e2e4")) - val invalidMove = Fishnet.PostMove(key, BestMove("ee4")) + val validMove = Fishnet.PostMove(Fishnet.Fishnet("v1", key), Fishnet.Move(BestMove("e2e4"))) + val invalidMove = Fishnet.PostMove(Fishnet.Fishnet("v1", key), Fishnet.Move(BestMove("2e4"))) test("acquire when there is no work should return none"): for diff --git a/app/src/test/scala/http/FishnetRoutesTest.scala b/app/src/test/scala/http/FishnetRoutesTest.scala new file mode 100644 index 0000000..9d88233 --- /dev/null +++ b/app/src/test/scala/http/FishnetRoutesTest.scala @@ -0,0 +1,93 @@ +package lila.fishnet +package http + +import cats.effect.IO +import cats.syntax.all.* +import io.circe.* +import io.circe.literal.* +import java.time.Instant +import org.http4s.* +import org.http4s.implicits.* +import org.http4s.circe.* +import weaver.* + +object FishnetRoutesTest extends SimpleIOSuite: + + val acqurieRequestBody = json"""{ + "fishnet": { + "version": "1.0.0", + "apikey": "apikey" + } + }""" + + val postMoveRequestBody = json"""{ + "fishnet": { + "version": "1.0.0", + "apikey": "apikey" + }, + "move": { + "bestmove": "e2e4" + } + }""" + + val workResponse: Json = json"""{ + "work": { + "id": "workid", + "level": 1, + "clock": { + "wtime": 600, + "btime": 600, + "inc": 0 + }, + "type": "move" + }, + "game_id": "gameid", + "position": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR", + "moves": "", + "variant": "Standard" + }""" + + val requestWithId = Work.RequestWithId( + id = WorkId("workid"), + request = Lila.Request( + id = GameId("gameid"), + initialFen = chess.format.Fen.Epd("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"), + moves = "", + variant = chess.variant.Standard, + level = 1, + clock = Some(Lila.Clock(wtime = 600, btime = 600, inc = 0)), + ), + ) + + test("POST /fishnet/acquire should return work response"): + val executor = createExecutor() + val routes = createRoutes(executor) + val req = Request[IO](Method.POST, uri"/fishnet/acquire").withEntity(acqurieRequestBody) + exepectHttpBodyAndStatus(routes, req)(expectedBody = workResponse, expectedStatus = Status.Ok) + + test("POST /fishnet/move should also return work response"): + val executor = createExecutor() + val routes = createRoutes(executor) + val req = Request[IO](Method.POST, uri"/fishnet/move/workid").withEntity(postMoveRequestBody) + exepectHttpBodyAndStatus(routes, req)(expectedBody = workResponse, expectedStatus = Status.Ok) + + def exepectHttpBodyAndStatus(routes: HttpRoutes[IO], req: Request[IO])( + expectedBody: Json, + expectedStatus: Status, + ) = + routes.run(req).value.flatMap: + case Some(resp) => resp.asJson.map: + expect.same(_, expectedBody) `and` expect.same(resp.status, expectedStatus) + case _ => IO.pure(failure("expected response but not found")) + + def createRoutes(executor: Executor): HttpRoutes[IO] = + FishnetRoutes(executor).routes + + def createExecutor(): Executor = + new Executor: + def acquire(key: ClientKey) = IO.pure(requestWithId.some) + def move(id: WorkId, move: Fishnet.PostMove): IO[Unit] = + if id == requestWithId.id then IO.unit + else IO.raiseError(new Exception("invalid work id")) + def add(request: Lila.Request): IO[Unit] = IO.unit + def clean(before: Instant) = IO.unit diff --git a/build.sbt b/build.sbt index 93ef2c2..c8b6b16 100644 --- a/build.sbt +++ b/build.sbt @@ -41,6 +41,8 @@ lazy val app = project munitScalacheck, weaver, weaverScalaCheck, + http4sClient, + circeLiteral, ), ) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 68faeba..ec3473d 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -29,6 +29,7 @@ object Dependencies { val circeCore = circe("core") val circeParser = circe("parser") val circeGeneric = circe("generic") + val circeLiteral = circe("literal") % Test val cirisCore = "is.cir" %% "ciris" % V.ciris val cirisHtt4s = "is.cir" %% "ciris-http4s" % V.ciris @@ -40,7 +41,7 @@ object Dependencies { val http4sDsl = http4s("dsl") val http4sServer = http4s("ember-server") - val http4sClient = http4s("ember-client") + val http4sClient = http4s("ember-client") % Test val http4sCirce = http4s("circe") val log4Cats = "org.typelevel" %% "log4cats-slf4j" % "2.6.0"