fix(official-bots): auto-register official bots with account service
Build & Test (NowChessSystems) TeamCity build failed

On startup, OfficialBotService now calls POST /api/account/official-bots/sync
(internal endpoint) to ensure all bots in BotController are registered in the
account DB. Without this, OfficialChallengeResource returns 404 for all
official bot challenges because no entries exist.

The sync is idempotent: existing bots are skipped. Startup failure in the
account service is logged but does not prevent the bot service from starting.

Closes NCS-70
https://knockoutwhist.youtrack.cloud/issue/NCS-70

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 11:02:50 +02:00
parent 085e34f062
commit e3503642cd
9 changed files with 104 additions and 1 deletions
@@ -49,3 +49,5 @@ case class RotatedTokenDto(token: String)
case class OfficialBotAccountDto(id: String, name: String, rating: Int, createdAt: String)
case class OfficialChallengeResponse(gameId: String, botName: String, difficulty: Int)
case class SyncOfficialBotsRequest(bots: List[String])
@@ -89,6 +89,13 @@ class OfficialBotAccountRepository:
def findAll(): List[OfficialBotAccount] =
em.createQuery("FROM OfficialBotAccount", classOf[OfficialBotAccount]).getResultList.asScala.toList
def findByName(name: String): Option[OfficialBotAccount] =
em.createQuery("FROM OfficialBotAccount WHERE name = :name", classOf[OfficialBotAccount])
.setParameter("name", name)
.getResultList
.asScala
.headOption
def persist(bot: OfficialBotAccount): OfficialBotAccount =
em.persist(bot)
bot
@@ -4,6 +4,7 @@ import de.nowchess.account.domain.{BotAccount, OfficialBotAccount, UserAccount}
import de.nowchess.account.dto.*
import de.nowchess.account.error.AccountError
import de.nowchess.account.service.AccountService
import de.nowchess.security.InternalOnly
import jakarta.annotation.security.RolesAllowed
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
@@ -179,6 +180,13 @@ class AccountResource:
createdAt = bot.createdAt.toString,
)
@POST
@Path("/official-bots/sync")
@InternalOnly
def syncOfficialBots(req: SyncOfficialBotsRequest): Response =
accountService.syncOfficialBots(req.bots)
Response.noContent().build()
@GET
@Path("/official-bots")
def getOfficialBots: Response =
@@ -206,6 +206,17 @@ class AccountService:
officialBotAccountRepository.persist(bot)
Right(bot)
@Transactional
def syncOfficialBots(botNames: List[String]): Unit =
botNames.foreach { name =>
if officialBotAccountRepository.findByName(name).isEmpty then
val bot = new OfficialBotAccount()
bot.name = name
bot.createdAt = Instant.now()
officialBotAccountRepository.persist(bot)
log.infof("Auto-registered official bot: %s", name)
}
def getOfficialBotAccounts(): List[OfficialBotAccount] =
officialBotAccountRepository.findAll()
@@ -154,3 +154,34 @@ class AccountResourceTest:
.post("/api/account/refresh")
.`then`()
.statusCode(401)
@Test
def syncOfficialBotsCreatesNewBots(): Unit =
givenRequest()
.body("""{"bots":["sync-easy","sync-hard"]}""")
.when()
.post("/api/account/official-bots/sync")
.`then`()
.statusCode(204)
RestAssured.`given`()
.when()
.get("/api/account/official-bots")
.`then`()
.statusCode(200)
.body("name", hasItems("sync-easy", "sync-hard"))
@Test
def syncOfficialBotsIsIdempotent(): Unit =
val body = """{"bots":["idempotent-bot"]}"""
givenRequest()
.body(body)
.when()
.post("/api/account/official-bots/sync")
.`then`()
.statusCode(204)
givenRequest()
.body(body)
.when()
.post("/api/account/official-bots/sync")
.`then`()
.statusCode(204)