diff --git a/modules/backcore/src/main/scala/de/nowchess/backcore/resource/ImportResource.scala b/modules/backcore/src/main/scala/de/nowchess/backcore/resource/ImportResource.scala new file mode 100644 index 0000000..6291661 --- /dev/null +++ b/modules/backcore/src/main/scala/de/nowchess/backcore/resource/ImportResource.scala @@ -0,0 +1,29 @@ +package de.nowchess.backcore.resource + +import de.nowchess.backcore.dto.{ApiErrorResponse, ImportFenRequest, ImportPgnRequest} +import de.nowchess.backcore.game.{GameMapper, GameStore} +import jakarta.enterprise.context.ApplicationScoped +import jakarta.inject.Inject +import jakarta.ws.rs.* +import jakarta.ws.rs.core.{MediaType, Response} + +@Path("/api/board/game/import") +@Produces(Array(MediaType.APPLICATION_JSON)) +@Consumes(Array(MediaType.APPLICATION_JSON)) +@ApplicationScoped +class ImportResource @Inject() (store: GameStore): + + @POST + @Path("/fen") + def importFen(req: ImportFenRequest): Response = + store.importFen(Option(req).getOrElse(ImportFenRequest())) match + case Right(session) => Response.status(201).entity(GameMapper.toGameFull(session)).build() + case Left(err) => Response.status(400).entity(ApiErrorResponse("INVALID_FEN", err)).build() + + @POST + @Path("/pgn") + def importPgn(req: ImportPgnRequest): Response = + val body = Option(req).getOrElse(ImportPgnRequest()) + store.importPgn(body.pgn, None, None) match + case Right(session) => Response.status(201).entity(GameMapper.toGameFull(session)).build() + case Left(err) => Response.status(400).entity(ApiErrorResponse("INVALID_PGN", err)).build() diff --git a/modules/backcore/src/test/scala/de/nowchess/backcore/resource/ImportExportTest.scala b/modules/backcore/src/test/scala/de/nowchess/backcore/resource/ImportExportTest.scala new file mode 100644 index 0000000..a4a8eff --- /dev/null +++ b/modules/backcore/src/test/scala/de/nowchess/backcore/resource/ImportExportTest.scala @@ -0,0 +1,163 @@ +package de.nowchess.backcore.resource + +import io.quarkus.test.junit.QuarkusTest +import io.restassured.RestAssured +import org.hamcrest.Matchers.{containsString, equalTo, matchesPattern, notNullValue} +import org.junit.jupiter.api.Test + +@QuarkusTest +class ImportExportTest: + + private val startFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + + // ─── Import FEN ──────────────────────────────────────────────── + + @Test + def importFenReturns201WithCorrectPosition(): Unit = + RestAssured.`given`() + .contentType("application/json") + .body(s"""{"fen":"$startFen"}""") + .when() + .post("/api/board/game/import/fen") + .`then`() + .statusCode(201) + .body("gameId", matchesPattern("[A-Za-z0-9]{8}")) + .body("state.fen", equalTo(startFen)) + .body("state.turn", equalTo("white")) + + @Test + def importFenWithCustomPositionWorks(): Unit = + val fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" + RestAssured.`given`() + .contentType("application/json") + .body(s"""{"fen":"$fen"}""") + .when() + .post("/api/board/game/import/fen") + .`then`() + .statusCode(201) + .body("state.fen", equalTo(fen)) + .body("state.turn", equalTo("black")) + + @Test + def importFenWithInvalidFenReturns400(): Unit = + RestAssured.`given`() + .contentType("application/json") + .body("""{"fen":"not-a-fen"}""") + .when() + .post("/api/board/game/import/fen") + .`then`() + .statusCode(400) + + // ─── Import PGN ──────────────────────────────────────────────── + + @Test + def importPgnReturns201(): Unit = + RestAssured.`given`() + .contentType("application/json") + .body("""{"pgn":"1. e4 e5 2. Nf3 Nc6 *"}""") + .when() + .post("/api/board/game/import/pgn") + .`then`() + .statusCode(201) + .body("gameId", matchesPattern("[A-Za-z0-9]{8}")) + .body("state.turn", equalTo("white")) + + @Test + def importPgnWithInvalidPgnReturns400(): Unit = + RestAssured.`given`() + .contentType("application/json") + .body("""{"pgn":"1. z9 *"}""") + .when() + .post("/api/board/game/import/pgn") + .`then`() + .statusCode(400) + + // ─── Export FEN ──────────────────────────────────────────────── + + @Test + def exportFenReturnsStartingFen(): Unit = + val gameId = RestAssured.`given`() + .contentType("application/json") + .body("{}") + .when() + .post("/api/board/game") + .`then`() + .statusCode(201) + .extract() + .path[String]("gameId") + + RestAssured.`given`() + .when() + .get(s"/api/board/game/$gameId/export/fen") + .`then`() + .statusCode(200) + .body(equalTo(startFen)) + + @Test + def exportFenOnUnknownGameReturns404(): Unit = + RestAssured.`given`() + .when() + .get("/api/board/game/XXXXXXXX/export/fen") + .`then`() + .statusCode(404) + + // ─── Export PGN ──────────────────────────────────────────────── + + @Test + def exportPgnReturnsText(): Unit = + val gameId = RestAssured.`given`() + .contentType("application/json") + .body("{}") + .when() + .post("/api/board/game") + .`then`() + .statusCode(201) + .extract() + .path[String]("gameId") + + RestAssured.`given`() + .when() + .post(s"/api/board/game/$gameId/move/e2e4") + .`then`() + .statusCode(200) + + RestAssured.`given`() + .when() + .get(s"/api/board/game/$gameId/export/pgn") + .`then`() + .statusCode(200) + .body(containsString("e4")) + + // ─── Stream ──────────────────────────────────────────────────── + + @Test + def streamReturnsNdjsonSnapshot(): Unit = + val gameId = RestAssured.`given`() + .contentType("application/json") + .body("{}") + .when() + .post("/api/board/game") + .`then`() + .statusCode(201) + .extract() + .path[String]("gameId") + + val body = RestAssured.`given`() + .when() + .get(s"/api/board/game/$gameId/stream") + .`then`() + .statusCode(200) + .contentType("application/x-ndjson") + .extract() + .body() + .asString() + + assert(body.trim.startsWith("""{"type":"gameFull""""), s"Expected gameFull event, got: $body") + + @Test + def streamOnUnknownGameReturns404(): Unit = + RestAssured.`given`() + .when() + .get("/api/board/game/XXXXXXXX/stream") + .`then`() + .statusCode(404)