diff --git a/build.gradle.kts b/build.gradle.kts index 8dfc1cc..39ce424 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,7 +36,11 @@ val coverageExclusions = listOf( "**/core/src/main/scala/de/nowchess/chess/registry/GameEntry.scala", "**/core/src/main/scala/de/nowchess/chess/registry/GameRegistryImpl.scala", // GameResource — REST integration layer with @Inject var fields; mocking dependencies for unit tests is infeasible with Quarkus DI; integration tests would require @QuarkusTest which Scoverage doesn't instrument - "**/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala" + "**/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala", + // IoResource — same rationale as GameResource; @QuarkusTest not instrumented by Scoverage + "**/io/src/main/scala/de/nowchess/io/service/resource/IoResource.scala", + // JacksonConfig — Quarkus lifecycle hook, no testable logic beyond ObjectMapper registration + "**/io/src/main/scala/de/nowchess/io/service/config/JacksonConfig.scala", ) // Converts a Sonar-style glob to a scoverage regex (matched against full source path). diff --git a/lint b/lint old mode 100644 new mode 100755 diff --git a/modules/io/src/test/scala/de/nowchess/io/json/SquareKeyDeserializerTest.scala b/modules/io/src/test/scala/de/nowchess/io/json/SquareKeyDeserializerTest.scala new file mode 100644 index 0000000..bef9cb0 --- /dev/null +++ b/modules/io/src/test/scala/de/nowchess/io/json/SquareKeyDeserializerTest.scala @@ -0,0 +1,60 @@ +package de.nowchess.io.json + +import com.fasterxml.jackson.core.`type`.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.module.scala.DefaultScalaModule +import de.nowchess.api.board.{File, Rank, Square} +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class SquareKeyDeserializerTest extends AnyFunSuite with Matchers: + + private def mapper: ObjectMapper = + val m = new ObjectMapper() + val mod = new SimpleModule() + mod.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer()) + m.registerModule(DefaultScalaModule) + m.registerModule(mod) + m + + private def readMap(json: String): Map[Square, Int] = + mapper.readValue(json, new TypeReference[Map[Square, Int]] {}) + + test("deserializes valid algebraic key") { + val result = readMap("""{"e4":1}""") + result(Square(File.E, Rank.R4)) shouldBe 1 + } + + test("deserializes a1 corner") { + val result = readMap("""{"a1":1}""") + result(Square(File.A, Rank.R1)) shouldBe 1 + } + + test("deserializes h8 corner") { + val result = readMap("""{"h8":1}""") + result(Square(File.H, Rank.R8)) shouldBe 1 + } + + test("deserializes multiple squares") { + val result = readMap("""{"a1":1,"h8":2,"e4":3}""") + result(Square(File.A, Rank.R1)) shouldBe 1 + result(Square(File.H, Rank.R8)) shouldBe 2 + result(Square(File.E, Rank.R4)) shouldBe 3 + } + + test("deserializeKey returns null for invalid square") { + new SquareKeyDeserializer().deserializeKey("invalid", null) shouldBe null + } + + test("deserializeKey returns null for wrong-length key") { + new SquareKeyDeserializer().deserializeKey("e44", null) shouldBe null + } + + test("deserializeKey returns null for bad file") { + new SquareKeyDeserializer().deserializeKey("z4", null) shouldBe null + } + + test("deserializeKey returns null for bad rank") { + new SquareKeyDeserializer().deserializeKey("e9", null) shouldBe null + } diff --git a/modules/io/src/test/scala/de/nowchess/io/json/SquareKeySerializerTest.scala b/modules/io/src/test/scala/de/nowchess/io/json/SquareKeySerializerTest.scala new file mode 100644 index 0000000..bcc2f0f --- /dev/null +++ b/modules/io/src/test/scala/de/nowchess/io/json/SquareKeySerializerTest.scala @@ -0,0 +1,50 @@ +package de.nowchess.io.json + +import com.fasterxml.jackson.core.`type`.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.module.scala.DefaultScalaModule +import de.nowchess.api.board.{File, Rank, Square} +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class SquareKeySerializerTest extends AnyFunSuite with Matchers: + + private def mapper: ObjectMapper = + val m = new ObjectMapper() + val mod = new SimpleModule() + mod.addKeySerializer(classOf[Square], new SquareKeySerializer()) + m.registerModule(DefaultScalaModule) + m.registerModule(mod) + m + + test("serializes square as algebraic notation") { + val json = mapper.writeValueAsString(Map(Square(File.E, Rank.R4) -> 1)) + json should include("\"e4\"") + } + + test("serializes a1 corner") { + val json = mapper.writeValueAsString(Map(Square(File.A, Rank.R1) -> 1)) + json should include("\"a1\"") + } + + test("serializes h8 corner") { + val json = mapper.writeValueAsString(Map(Square(File.H, Rank.R8) -> 1)) + json should include("\"h8\"") + } + + test("round-trips with SquareKeyDeserializer") { + val rt = { + val m = new ObjectMapper() + val mod = new SimpleModule() + mod.addKeySerializer(classOf[Square], new SquareKeySerializer()) + mod.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer()) + m.registerModule(DefaultScalaModule) + m.registerModule(mod) + m + } + val original = Map(Square(File.D, Rank.R5) -> 99) + val json = rt.writeValueAsString(original) + val result = rt.readValue(json, new TypeReference[Map[Square, Int]] {}) + result shouldBe original + } diff --git a/modules/io/src/test/scala/de/nowchess/io/service/resource/IoResourceTest.scala b/modules/io/src/test/scala/de/nowchess/io/service/resource/IoResourceTest.scala new file mode 100644 index 0000000..67b43a2 --- /dev/null +++ b/modules/io/src/test/scala/de/nowchess/io/service/resource/IoResourceTest.scala @@ -0,0 +1,78 @@ +package de.nowchess.io.service.resource + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.module.scala.DefaultScalaModule +import de.nowchess.api.board.Square +import de.nowchess.api.game.GameContext +import de.nowchess.io.json.{SquareKeyDeserializer, SquareKeySerializer} +import io.quarkus.test.junit.QuarkusTest +import io.restassured.RestAssured +import io.restassured.http.ContentType +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +// scalafix:off +@QuarkusTest +class IoResourceTest: + + private lazy val testMapper: ObjectMapper = + val m = new ObjectMapper() + val mod = new SimpleModule() + mod.addKeySerializer(classOf[Square], new SquareKeySerializer()) + mod.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer()) + m.registerModule(new DefaultScalaModule()) + m.registerModule(mod) + m + + private def contextJson(ctx: GameContext): String = testMapper.writeValueAsString(ctx) + + @Test + def importFenReturns200(): Unit = + val resp = RestAssured.`given`() + .contentType(ContentType.JSON) + .body("""{"fen":"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"}""") + .post("/io/import/fen") + assertEquals(200, resp.statusCode()) + + @Test + def importFenInvalidReturns400(): Unit = + val resp = RestAssured.`given`() + .contentType(ContentType.JSON) + .body("""{"fen":"not-a-fen"}""") + .post("/io/import/fen") + assertEquals(400, resp.statusCode()) + + @Test + def importPgnReturns200(): Unit = + val resp = RestAssured.`given`() + .contentType(ContentType.JSON) + .body("""{"pgn":"1. e4 e5"}""") + .post("/io/import/pgn") + assertEquals(200, resp.statusCode()) + + @Test + def importPgnInvalidReturns400(): Unit = + val resp = RestAssured.`given`() + .contentType(ContentType.JSON) + .body("""{"pgn":"not valid pgn !!!###"}""") + .post("/io/import/pgn") + assertEquals(400, resp.statusCode()) + + @Test + def exportFenReturns200WithFen(): Unit = + val resp = RestAssured.`given`() + .contentType(ContentType.JSON) + .body(contextJson(GameContext.initial)) + .post("/io/export/fen") + assertEquals(200, resp.statusCode()) + assertTrue(resp.getBody.asString().contains("rnbqkbnr")) + + @Test + def exportPgnReturns200(): Unit = + val resp = RestAssured.`given`() + .contentType(ContentType.JSON) + .body(contextJson(GameContext.initial)) + .post("/io/export/pgn") + assertEquals(200, resp.statusCode()) +// scalafix:on