feat(game): introduce game modes and time control features
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
This commit is contained in:
@@ -32,6 +32,8 @@ val quarkusPlatformVersion: String by project
|
||||
|
||||
dependencies {
|
||||
|
||||
runtimeOnly("io.quarkus:quarkus-jdbc-h2")
|
||||
|
||||
compileOnly("org.scala-lang:scala3-compiler_3") {
|
||||
version {
|
||||
strictly(versions["SCALA3"]!!)
|
||||
@@ -46,6 +48,7 @@ dependencies {
|
||||
implementation(platform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
|
||||
implementation("io.quarkus:quarkus-rest")
|
||||
implementation("io.quarkus:quarkus-rest-jackson")
|
||||
implementation("io.quarkus:quarkus-rest-client-jackson")
|
||||
implementation("io.quarkus:quarkus-config-yaml")
|
||||
implementation("io.quarkus:quarkus-arc")
|
||||
implementation("io.quarkus:quarkus-hibernate-orm-panache")
|
||||
@@ -66,6 +69,7 @@ dependencies {
|
||||
testImplementation("io.rest-assured:rest-assured")
|
||||
testImplementation("io.quarkus:quarkus-jdbc-h2")
|
||||
testImplementation("io.quarkus:quarkus-test-security")
|
||||
testImplementation("io.quarkus:quarkus-junit5-mockito")
|
||||
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"reflection": [
|
||||
{ "type": "scala.Tuple1[]" },
|
||||
{ "type": "scala.Tuple2[]" },
|
||||
{ "type": "scala.Tuple3[]" },
|
||||
{ "type": "scala.Tuple4[]" },
|
||||
{ "type": "scala.Tuple5[]" },
|
||||
{ "type": "scala.Tuple6[]" },
|
||||
{ "type": "scala.Tuple7[]" },
|
||||
{ "type": "scala.Tuple8[]" },
|
||||
{ "type": "scala.Tuple9[]" },
|
||||
{ "type": "scala.Tuple10[]" },
|
||||
{ "type": "scala.Tuple11[]" },
|
||||
{ "type": "scala.Tuple12[]" },
|
||||
{ "type": "scala.Tuple13[]" },
|
||||
{ "type": "scala.Tuple14[]" },
|
||||
{ "type": "scala.Tuple15[]" },
|
||||
{ "type": "scala.Tuple16[]" },
|
||||
{ "type": "scala.Tuple17[]" },
|
||||
{ "type": "scala.Tuple18[]" },
|
||||
{ "type": "scala.Tuple19[]" },
|
||||
{ "type": "scala.Tuple20[]" },
|
||||
{ "type": "scala.Tuple21[]" },
|
||||
{ "type": "scala.Tuple22[]" },
|
||||
{ "type": "com.fasterxml.jackson.module.scala.introspect.PropertyDescriptor[]" }
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,9 @@ quarkus:
|
||||
port: 8083
|
||||
application:
|
||||
name: nowchess-account
|
||||
rest-client:
|
||||
core-service:
|
||||
url: http://localhost:8080
|
||||
smallrye-openapi:
|
||||
info-title: NowChess Account Service
|
||||
path: /openapi
|
||||
@@ -10,17 +13,29 @@ quarkus:
|
||||
always-include: true
|
||||
path: /swagger-ui
|
||||
datasource:
|
||||
postgres:
|
||||
db-kind: postgresql
|
||||
username: ${DB_USER:nowchess}
|
||||
password: ${DB_PASSWORD:nowchess}
|
||||
jdbc:
|
||||
url: ${DB_URL:jdbc:postgresql://localhost:5432/nowchess_account}
|
||||
db-kind: h2
|
||||
username: sa
|
||||
password: ""
|
||||
jdbc:
|
||||
url: jdbc:h2:mem:nowchess;DB_CLOSE_DELAY=-1
|
||||
hibernate-orm:
|
||||
schema-management:
|
||||
strategy: update
|
||||
strategy: drop-and-create
|
||||
|
||||
"%prod":
|
||||
"%live":
|
||||
quarkus:
|
||||
rest-client:
|
||||
core-service:
|
||||
url: ${CORE_SERVICE_URL}
|
||||
datasource:
|
||||
db-kind: postgresql
|
||||
username: ${DB_USER}
|
||||
password: ${DB_PASSWORD}
|
||||
jdbc:
|
||||
url: ${DB_URL}
|
||||
hibernate-orm:
|
||||
schema-management:
|
||||
strategy: update
|
||||
mp:
|
||||
jwt:
|
||||
verify:
|
||||
@@ -31,11 +46,4 @@ quarkus:
|
||||
jwt:
|
||||
sign:
|
||||
key:
|
||||
location: ${JWT_PRIVATE_KEY_PATH:keys/private.pem}
|
||||
quarkus:
|
||||
datasource:
|
||||
postgres:
|
||||
active: true
|
||||
hibernate-orm:
|
||||
postgres:
|
||||
active: true
|
||||
location: ${JWT_PRIVATE_KEY_PATH:keys/private.pem}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.nowchess.account.client
|
||||
|
||||
import jakarta.ws.rs.*
|
||||
import jakarta.ws.rs.core.MediaType
|
||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
||||
|
||||
case class CorePlayerInfo(id: String, displayName: String)
|
||||
case class CoreTimeControl(limitSeconds: Option[Int], incrementSeconds: Option[Int], daysPerMove: Option[Int])
|
||||
case class CoreCreateGameRequest(
|
||||
white: Option[CorePlayerInfo],
|
||||
black: Option[CorePlayerInfo],
|
||||
timeControl: Option[CoreTimeControl],
|
||||
mode: Option[String],
|
||||
)
|
||||
case class CoreGameResponse(gameId: String)
|
||||
|
||||
@Path("/api/board/game")
|
||||
@RegisterRestClient(configKey = "core-service")
|
||||
trait CoreGameClient:
|
||||
|
||||
@POST
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def createGame(req: CoreCreateGameRequest): CoreGameResponse
|
||||
+14
-2
@@ -1,12 +1,19 @@
|
||||
package de.nowchess.account.config
|
||||
|
||||
import de.nowchess.account.domain.{ChallengeColor, ChallengeStatus, DeclineReason}
|
||||
import de.nowchess.account.client.{CoreCreateGameRequest, CoreGameResponse, CorePlayerInfo, CoreTimeControl}
|
||||
import de.nowchess.account.domain.{Account, Challenge, ChallengeColor, ChallengeStatus, DeclineReason, TimeControl}
|
||||
import de.nowchess.account.dto.*
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection
|
||||
|
||||
@RegisterForReflection(
|
||||
targets = Array(
|
||||
classOf[RegisterRequest],
|
||||
classOf[Account],
|
||||
classOf[Challenge],
|
||||
classOf[ChallengeColor],
|
||||
classOf[ChallengeStatus],
|
||||
classOf[DeclineReason],
|
||||
classOf[TimeControl],
|
||||
|
||||
classOf[LoginRequest],
|
||||
classOf[TokenResponse],
|
||||
classOf[PlayerInfo],
|
||||
@@ -20,6 +27,11 @@ import io.quarkus.runtime.annotations.RegisterForReflection
|
||||
classOf[ChallengeStatus],
|
||||
classOf[ChallengeColor],
|
||||
classOf[DeclineReason],
|
||||
|
||||
classOf[CorePlayerInfo],
|
||||
classOf[CoreTimeControl],
|
||||
classOf[CoreCreateGameRequest],
|
||||
classOf[CoreGameResponse],
|
||||
),
|
||||
)
|
||||
class NativeReflectionConfig
|
||||
|
||||
@@ -45,6 +45,10 @@ class Challenge extends PanacheEntityBase:
|
||||
|
||||
var expiresAt: Instant = uninitialized
|
||||
|
||||
@Column(nullable = true)
|
||||
var gameId: String = uninitialized
|
||||
|
||||
def gameIdOpt: Option[String] = Option(gameId)
|
||||
def declineReasonOpt: Option[DeclineReason] = Option(declineReason)
|
||||
def timeControlLimitOpt: Option[Int] = Option(timeControlLimit).map(_.intValue())
|
||||
def timeControlIncrementOpt: Option[Int] = Option(timeControlIncrement).map(_.intValue())
|
||||
|
||||
@@ -23,6 +23,7 @@ case class ChallengeDto(
|
||||
timeControl: TimeControlDto,
|
||||
status: String,
|
||||
declineReason: Option[String],
|
||||
gameId: Option[String],
|
||||
createdAt: String,
|
||||
expiresAt: String,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.nowchess.account.error
|
||||
|
||||
enum AccountError:
|
||||
case UsernameTaken(username: String)
|
||||
case EmailAlreadyRegistered(email: String)
|
||||
case InvalidCredentials
|
||||
|
||||
def message: String = this match
|
||||
case UsernameTaken(u) => s"Username '$u' is already taken"
|
||||
case EmailAlreadyRegistered(e) => s"Email '$e' is already registered"
|
||||
case InvalidCredentials => "Invalid credentials"
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.nowchess.account.error
|
||||
|
||||
enum ChallengeError:
|
||||
case UserNotFound(username: String)
|
||||
case ChallengerNotFound
|
||||
case CannotChallengeSelf
|
||||
case DuplicateChallenge
|
||||
case InvalidColor(color: String)
|
||||
case InvalidDeclineReason(reason: String)
|
||||
case ChallengeNotFound
|
||||
case ChallengeNotActive
|
||||
case NotAuthorized
|
||||
case GameCreationFailed
|
||||
|
||||
def message: String = this match
|
||||
case UserNotFound(u) => s"User '$u' not found"
|
||||
case ChallengerNotFound => "Challenger not found"
|
||||
case CannotChallengeSelf => "Cannot challenge yourself"
|
||||
case DuplicateChallenge => "Active challenge to this user already exists"
|
||||
case InvalidColor(c) => s"Unknown color: $c"
|
||||
case InvalidDeclineReason(r) => s"Unknown decline reason: $r"
|
||||
case ChallengeNotFound => "Challenge not found"
|
||||
case ChallengeNotActive => "Challenge is not active"
|
||||
case NotAuthorized => "Not authorized"
|
||||
case GameCreationFailed => "Failed to create game"
|
||||
@@ -2,6 +2,7 @@ package de.nowchess.account.resource
|
||||
|
||||
import de.nowchess.account.domain.Account
|
||||
import de.nowchess.account.dto.*
|
||||
import de.nowchess.account.error.AccountError
|
||||
import de.nowchess.account.service.AccountService
|
||||
import jakarta.annotation.security.RolesAllowed
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
@@ -31,7 +32,7 @@ class AccountResource:
|
||||
case Right(account) =>
|
||||
Response.ok(toPublicDto(account)).build()
|
||||
case Left(error) =>
|
||||
Response.status(Response.Status.CONFLICT).entity(ErrorDto(error)).build()
|
||||
Response.status(Response.Status.CONFLICT).entity(ErrorDto(error.message)).build()
|
||||
|
||||
@POST
|
||||
@Path("/login")
|
||||
@@ -40,7 +41,7 @@ class AccountResource:
|
||||
case Right(token) =>
|
||||
Response.ok(TokenResponse(token)).build()
|
||||
case Left(error) =>
|
||||
Response.status(Response.Status.UNAUTHORIZED).entity(ErrorDto(error)).build()
|
||||
Response.status(Response.Status.UNAUTHORIZED).entity(ErrorDto(error.message)).build()
|
||||
|
||||
@GET
|
||||
@Path("/me")
|
||||
|
||||
+13
-10
@@ -1,6 +1,7 @@
|
||||
package de.nowchess.account.resource
|
||||
|
||||
import de.nowchess.account.dto.*
|
||||
import de.nowchess.account.error.ChallengeError
|
||||
import de.nowchess.account.service.ChallengeService
|
||||
import jakarta.annotation.security.RolesAllowed
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
@@ -33,10 +34,11 @@ class ChallengeResource:
|
||||
case Right(challenge) =>
|
||||
Response.status(Response.Status.CREATED).entity(challengeService.toDto(challenge)).build()
|
||||
case Left(error) =>
|
||||
val status = if error.contains("not found") then Response.Status.NOT_FOUND
|
||||
else if error.contains("yourself") then Response.Status.BAD_REQUEST
|
||||
else Response.Status.CONFLICT
|
||||
Response.status(status).entity(ErrorDto(error)).build()
|
||||
val status = error match
|
||||
case ChallengeError.UserNotFound(_) | ChallengeError.ChallengerNotFound => Response.Status.NOT_FOUND
|
||||
case ChallengeError.CannotChallengeSelf => Response.Status.BAD_REQUEST
|
||||
case _ => Response.Status.CONFLICT
|
||||
Response.status(status).entity(ErrorDto(error.message)).build()
|
||||
|
||||
@GET
|
||||
def list(): Response =
|
||||
@@ -67,9 +69,10 @@ class ChallengeResource:
|
||||
case Right(challenge) => Response.ok(challengeService.toDto(challenge)).build()
|
||||
case Left(error) => errorResponse(error)
|
||||
|
||||
private def errorResponse(error: String): Response =
|
||||
val status =
|
||||
if error.contains("not found") then Response.Status.NOT_FOUND
|
||||
else if error.contains("authorized") then Response.Status.FORBIDDEN
|
||||
else Response.Status.BAD_REQUEST
|
||||
Response.status(status).entity(ErrorDto(error)).build()
|
||||
private def errorResponse(error: ChallengeError): Response =
|
||||
val status = error match
|
||||
case ChallengeError.ChallengeNotFound => Response.Status.NOT_FOUND
|
||||
case ChallengeError.NotAuthorized => Response.Status.FORBIDDEN
|
||||
case ChallengeError.GameCreationFailed => Response.Status.INTERNAL_SERVER_ERROR
|
||||
case _ => Response.Status.BAD_REQUEST
|
||||
Response.status(status).entity(ErrorDto(error.message)).build()
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.nowchess.account.service
|
||||
|
||||
import de.nowchess.account.domain.Account
|
||||
import de.nowchess.account.dto.{LoginRequest, RegisterRequest}
|
||||
import de.nowchess.account.error.AccountError
|
||||
import de.nowchess.account.repository.AccountRepository
|
||||
import io.quarkus.elytron.security.common.BcryptUtil
|
||||
import io.smallrye.jwt.build.Jwt
|
||||
@@ -20,11 +21,11 @@ class AccountService:
|
||||
var accountRepository: AccountRepository = uninitialized
|
||||
|
||||
@Transactional
|
||||
def register(req: RegisterRequest): Either[String, Account] =
|
||||
def register(req: RegisterRequest): Either[AccountError, Account] =
|
||||
if accountRepository.findByUsername(req.username).isDefined then
|
||||
Left(s"Username '${req.username}' is already taken")
|
||||
Left(AccountError.UsernameTaken(req.username))
|
||||
else if accountRepository.findByEmail(req.email).isDefined then
|
||||
Left(s"Email '${req.email}' is already registered")
|
||||
Left(AccountError.EmailAlreadyRegistered(req.email))
|
||||
else
|
||||
val account = new Account()
|
||||
account.username = req.username
|
||||
@@ -34,7 +35,7 @@ class AccountService:
|
||||
accountRepository.persist(account)
|
||||
Right(account)
|
||||
|
||||
def login(req: LoginRequest): Either[String, String] =
|
||||
def login(req: LoginRequest): Either[AccountError, String] =
|
||||
accountRepository
|
||||
.findByUsername(req.username)
|
||||
.filter(a => BcryptUtil.matches(req.password, a.passwordHash))
|
||||
@@ -45,7 +46,7 @@ class AccountService:
|
||||
.claim("username", account.username)
|
||||
.sign()
|
||||
}
|
||||
.toRight("Invalid credentials")
|
||||
.toRight(AccountError.InvalidCredentials)
|
||||
|
||||
def findByUsername(username: String): Option[Account] =
|
||||
accountRepository.findByUsername(username)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package de.nowchess.account.service
|
||||
|
||||
import de.nowchess.account.client.{CoreCreateGameRequest, CoreGameClient, CoreGameResponse, CorePlayerInfo, CoreTimeControl}
|
||||
import de.nowchess.account.domain.{Challenge, ChallengeColor, ChallengeStatus, DeclineReason}
|
||||
import de.nowchess.account.dto.{ChallengeDto, ChallengeListDto, ChallengeRequest, DeclineRequest, PlayerInfo, TimeControlDto}
|
||||
import de.nowchess.account.error.ChallengeError
|
||||
import de.nowchess.account.repository.{AccountRepository, ChallengeRepository}
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.transaction.Transactional
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient
|
||||
import scala.compiletime.uninitialized
|
||||
|
||||
import java.time.Instant
|
||||
@@ -21,16 +24,20 @@ class ChallengeService:
|
||||
@Inject
|
||||
var challengeRepository: ChallengeRepository = uninitialized
|
||||
|
||||
@Inject
|
||||
@RestClient
|
||||
var coreGameClient: CoreGameClient = uninitialized
|
||||
|
||||
@Transactional
|
||||
def create(challengerId: UUID, destUsername: String, req: ChallengeRequest): Either[String, Challenge] =
|
||||
def create(challengerId: UUID, destUsername: String, req: ChallengeRequest): Either[ChallengeError, Challenge] =
|
||||
for
|
||||
destUser <- accountRepository.findByUsername(destUsername).toRight(s"User '$destUsername' not found")
|
||||
challenger <- accountRepository.findById(challengerId).toRight("Challenger not found")
|
||||
_ <- Either.cond(challenger.id != destUser.id, (), "Cannot challenge yourself")
|
||||
destUser <- accountRepository.findByUsername(destUsername).toRight(ChallengeError.UserNotFound(destUsername))
|
||||
challenger <- accountRepository.findById(challengerId).toRight(ChallengeError.ChallengerNotFound)
|
||||
_ <- Either.cond(challenger.id != destUser.id, (), ChallengeError.CannotChallengeSelf)
|
||||
_ <- Either.cond(
|
||||
challengeRepository.findDuplicateChallenge(challengerId, destUser.id).isEmpty,
|
||||
(),
|
||||
"Active challenge to this user already exists",
|
||||
ChallengeError.DuplicateChallenge,
|
||||
)
|
||||
color <- parseColor(req.color)
|
||||
yield
|
||||
@@ -50,22 +57,24 @@ class ChallengeService:
|
||||
challenge
|
||||
|
||||
@Transactional
|
||||
def accept(challengeId: UUID, userId: UUID): Either[String, Challenge] =
|
||||
def accept(challengeId: UUID, userId: UUID): Either[ChallengeError, Challenge] =
|
||||
for
|
||||
challenge <- challengeRepository.findById(challengeId).toRight("Challenge not found")
|
||||
_ <- Either.cond(challenge.status == ChallengeStatus.Created, (), "Challenge is not active")
|
||||
_ <- Either.cond(challenge.destUser.id == userId, (), "Not authorized to accept this challenge")
|
||||
challenge <- challengeRepository.findById(challengeId).toRight(ChallengeError.ChallengeNotFound)
|
||||
_ <- Either.cond(challenge.status == ChallengeStatus.Created, (), ChallengeError.ChallengeNotActive)
|
||||
_ <- Either.cond(challenge.destUser.id == userId, (), ChallengeError.NotAuthorized)
|
||||
gameId <- createGame(challenge)
|
||||
yield
|
||||
challenge.status = ChallengeStatus.Accepted
|
||||
challenge.gameId = gameId
|
||||
challengeRepository.merge(challenge)
|
||||
challenge
|
||||
|
||||
@Transactional
|
||||
def decline(challengeId: UUID, userId: UUID, req: DeclineRequest): Either[String, Challenge] =
|
||||
def decline(challengeId: UUID, userId: UUID, req: DeclineRequest): Either[ChallengeError, Challenge] =
|
||||
for
|
||||
challenge <- challengeRepository.findById(challengeId).toRight("Challenge not found")
|
||||
_ <- Either.cond(challenge.status == ChallengeStatus.Created, (), "Challenge is not active")
|
||||
_ <- Either.cond(challenge.destUser.id == userId, (), "Not authorized to decline this challenge")
|
||||
challenge <- challengeRepository.findById(challengeId).toRight(ChallengeError.ChallengeNotFound)
|
||||
_ <- Either.cond(challenge.status == ChallengeStatus.Created, (), ChallengeError.ChallengeNotActive)
|
||||
_ <- Either.cond(challenge.destUser.id == userId, (), ChallengeError.NotAuthorized)
|
||||
reason <- parseDeclineReason(req.reason)
|
||||
yield
|
||||
challenge.status = ChallengeStatus.Declined
|
||||
@@ -74,11 +83,11 @@ class ChallengeService:
|
||||
challenge
|
||||
|
||||
@Transactional
|
||||
def cancel(challengeId: UUID, userId: UUID): Either[String, Challenge] =
|
||||
def cancel(challengeId: UUID, userId: UUID): Either[ChallengeError, Challenge] =
|
||||
for
|
||||
challenge <- challengeRepository.findById(challengeId).toRight("Challenge not found")
|
||||
_ <- Either.cond(challenge.status == ChallengeStatus.Created, (), "Challenge is not active")
|
||||
_ <- Either.cond(challenge.challenger.id == userId, (), "Not authorized to cancel this challenge")
|
||||
challenge <- challengeRepository.findById(challengeId).toRight(ChallengeError.ChallengeNotFound)
|
||||
_ <- Either.cond(challenge.status == ChallengeStatus.Created, (), ChallengeError.ChallengeNotActive)
|
||||
_ <- Either.cond(challenge.challenger.id == userId, (), ChallengeError.NotAuthorized)
|
||||
yield
|
||||
challenge.status = ChallengeStatus.Canceled
|
||||
challengeRepository.merge(challenge)
|
||||
@@ -89,20 +98,45 @@ class ChallengeService:
|
||||
val outgoing = challengeRepository.findActiveByChallengerId(userId).map(toDto)
|
||||
ChallengeListDto(in = incoming, out = outgoing)
|
||||
|
||||
private def parseColor(raw: String): Either[String, ChallengeColor] =
|
||||
// scalafix:off DisableSyntax.null
|
||||
private def createGame(challenge: Challenge): Either[ChallengeError, String] =
|
||||
try
|
||||
val (white, black) = assignColors(challenge)
|
||||
val tc = buildTimeControl(challenge)
|
||||
val req = CoreCreateGameRequest(Some(white), Some(black), tc, Some("Authenticated"))
|
||||
Right(coreGameClient.createGame(req).gameId)
|
||||
catch case _ => Left(ChallengeError.GameCreationFailed)
|
||||
// scalafix:on DisableSyntax.null
|
||||
|
||||
private def assignColors(challenge: Challenge): (CorePlayerInfo, CorePlayerInfo) =
|
||||
val challenger = CorePlayerInfo(challenge.challenger.id.toString, challenge.challenger.username)
|
||||
val destUser = CorePlayerInfo(challenge.destUser.id.toString, challenge.destUser.username)
|
||||
challenge.color match
|
||||
case ChallengeColor.White => (challenger, destUser)
|
||||
case ChallengeColor.Black => (destUser, challenger)
|
||||
case ChallengeColor.Random =>
|
||||
if scala.util.Random.nextBoolean() then (challenger, destUser) else (destUser, challenger)
|
||||
|
||||
private def buildTimeControl(challenge: Challenge): Option[CoreTimeControl] =
|
||||
challenge.timeControlType match
|
||||
case "unlimited" => None
|
||||
case "correspondence" => Some(CoreTimeControl(None, None, challenge.timeControlLimitOpt))
|
||||
case _ => Some(CoreTimeControl(challenge.timeControlLimitOpt, challenge.timeControlIncrementOpt, None))
|
||||
|
||||
private def parseColor(raw: String): Either[ChallengeError, ChallengeColor] =
|
||||
raw.toLowerCase match
|
||||
case "white" => Right(ChallengeColor.White)
|
||||
case "black" => Right(ChallengeColor.Black)
|
||||
case "random" => Right(ChallengeColor.Random)
|
||||
case _ => Left(s"Unknown color: $raw")
|
||||
case _ => Left(ChallengeError.InvalidColor(raw))
|
||||
|
||||
private def parseDeclineReason(raw: Option[String]): Either[String, Option[DeclineReason]] =
|
||||
private def parseDeclineReason(raw: Option[String]): Either[ChallengeError, Option[DeclineReason]] =
|
||||
raw match
|
||||
case None => Right(None)
|
||||
case Some(r) =>
|
||||
DeclineReason.values.find(_.toString.equalsIgnoreCase(r)) match
|
||||
case Some(reason) => Right(Some(reason))
|
||||
case None => Left(s"Unknown decline reason: $r")
|
||||
case None => Left(ChallengeError.InvalidDeclineReason(r))
|
||||
|
||||
def toDto(c: Challenge): ChallengeDto =
|
||||
ChallengeDto(
|
||||
@@ -114,6 +148,7 @@ class ChallengeService:
|
||||
timeControl = TimeControlDto(c.timeControlType, c.timeControlLimitOpt, c.timeControlIncrementOpt),
|
||||
status = c.status.toString.toLowerCase,
|
||||
declineReason = c.declineReasonOpt.map(_.toString.toLowerCase),
|
||||
gameId = c.gameIdOpt,
|
||||
createdAt = c.createdAt.toString,
|
||||
expiresAt = c.expiresAt.toString,
|
||||
)
|
||||
|
||||
+14
-1
@@ -1,14 +1,26 @@
|
||||
package de.nowchess.account.resource
|
||||
|
||||
import de.nowchess.account.client.{CoreGameClient, CoreGameResponse}
|
||||
import io.quarkus.test.InjectMock
|
||||
import io.quarkus.test.junit.QuarkusTest
|
||||
import io.restassured.RestAssured
|
||||
import io.restassured.http.ContentType
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient
|
||||
import org.hamcrest.Matchers.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.{BeforeEach, Test}
|
||||
import org.mockito.{ArgumentMatchers, Mockito}
|
||||
|
||||
@QuarkusTest
|
||||
class ChallengeResourceTest:
|
||||
|
||||
@InjectMock
|
||||
@RestClient
|
||||
var coreGameClient: CoreGameClient = scala.compiletime.uninitialized
|
||||
|
||||
@BeforeEach
|
||||
def setup(): Unit =
|
||||
Mockito.when(coreGameClient.createGame(ArgumentMatchers.any())).thenReturn(CoreGameResponse("test-game-id"))
|
||||
|
||||
private def givenRequest() = RestAssured.`given`().contentType(ContentType.JSON)
|
||||
|
||||
private def registerBody(username: String, suffix: String = "") =
|
||||
@@ -92,6 +104,7 @@ class ChallengeResourceTest:
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body("status", is("accepted"))
|
||||
.body("gameId", is("test-game-id"))
|
||||
|
||||
@Test
|
||||
def declineChallengeReturns200(): Unit =
|
||||
|
||||
Reference in New Issue
Block a user