From e3503642cdada1fe7026cd0eff6341daecae6863 Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 3 Jun 2026 11:02:50 +0200 Subject: [PATCH] fix(official-bots): auto-register official bots with account service 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 --- .../scala/de/nowchess/account/dto/Dtos.scala | 2 ++ .../repository/AccountRepository.scala | 7 +++++ .../account/resource/AccountResource.scala | 8 +++++ .../account/service/AccountService.scala | 11 +++++++ .../resource/AccountResourceTest.scala | 31 +++++++++++++++++++ modules/official-bots/build.gradle.kts | 2 ++ .../src/main/resources/application.yml | 10 ++++++ .../bot/client/AccountServiceClient.scala | 20 ++++++++++++ .../bot/service/OfficialBotService.scala | 14 ++++++++- 9 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 modules/official-bots/src/main/scala/de/nowchess/bot/client/AccountServiceClient.scala diff --git a/modules/account/src/main/scala/de/nowchess/account/dto/Dtos.scala b/modules/account/src/main/scala/de/nowchess/account/dto/Dtos.scala index 7307bed..d811d97 100644 --- a/modules/account/src/main/scala/de/nowchess/account/dto/Dtos.scala +++ b/modules/account/src/main/scala/de/nowchess/account/dto/Dtos.scala @@ -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]) diff --git a/modules/account/src/main/scala/de/nowchess/account/repository/AccountRepository.scala b/modules/account/src/main/scala/de/nowchess/account/repository/AccountRepository.scala index 3d6803f..0ef811e 100644 --- a/modules/account/src/main/scala/de/nowchess/account/repository/AccountRepository.scala +++ b/modules/account/src/main/scala/de/nowchess/account/repository/AccountRepository.scala @@ -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 diff --git a/modules/account/src/main/scala/de/nowchess/account/resource/AccountResource.scala b/modules/account/src/main/scala/de/nowchess/account/resource/AccountResource.scala index df4d367..40f13f8 100644 --- a/modules/account/src/main/scala/de/nowchess/account/resource/AccountResource.scala +++ b/modules/account/src/main/scala/de/nowchess/account/resource/AccountResource.scala @@ -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 = diff --git a/modules/account/src/main/scala/de/nowchess/account/service/AccountService.scala b/modules/account/src/main/scala/de/nowchess/account/service/AccountService.scala index b7d8569..82421ad 100644 --- a/modules/account/src/main/scala/de/nowchess/account/service/AccountService.scala +++ b/modules/account/src/main/scala/de/nowchess/account/service/AccountService.scala @@ -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() diff --git a/modules/account/src/test/scala/de/nowchess/account/resource/AccountResourceTest.scala b/modules/account/src/test/scala/de/nowchess/account/resource/AccountResourceTest.scala index 834561f..e77ff45 100644 --- a/modules/account/src/test/scala/de/nowchess/account/resource/AccountResourceTest.scala +++ b/modules/account/src/test/scala/de/nowchess/account/resource/AccountResourceTest.scala @@ -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) diff --git a/modules/official-bots/build.gradle.kts b/modules/official-bots/build.gradle.kts index 2ccf3ff..fe19643 100644 --- a/modules/official-bots/build.gradle.kts +++ b/modules/official-bots/build.gradle.kts @@ -77,6 +77,8 @@ dependencies { implementation(project(":modules:api")) implementation(project(":modules:io")) implementation(project(":modules:rule")) + implementation(project(":modules:security")) + implementation("io.quarkus:quarkus-rest-client-jackson") implementation("com.microsoft.onnxruntime:onnxruntime:${versions["ONNXRUNTIME"]!!}") implementation("io.quarkus:quarkus-redis-client") diff --git a/modules/official-bots/src/main/resources/application.yml b/modules/official-bots/src/main/resources/application.yml index 1af66b5..cf77b52 100644 --- a/modules/official-bots/src/main/resources/application.yml +++ b/modules/official-bots/src/main/resources/application.yml @@ -5,6 +5,9 @@ quarkus: name: nowchess-official-bots redis: hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379} + rest-client: + account-service: + url: http://localhost:8083 smallrye-jwt: enabled: true log: @@ -15,6 +18,8 @@ nowchess: host: localhost port: 6379 prefix: nowchess + internal: + secret: 123abc "%deployed": quarkus: @@ -28,8 +33,13 @@ nowchess: exporter: otlp: endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317} + rest-client: + account-service: + url: ${ACCOUNT_SERVICE_URL} nowchess: redis: host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6379} prefix: ${REDIS_PREFIX:nowchess} + internal: + secret: ${INTERNAL_SECRET} diff --git a/modules/official-bots/src/main/scala/de/nowchess/bot/client/AccountServiceClient.scala b/modules/official-bots/src/main/scala/de/nowchess/bot/client/AccountServiceClient.scala new file mode 100644 index 0000000..d95c8c2 --- /dev/null +++ b/modules/official-bots/src/main/scala/de/nowchess/bot/client/AccountServiceClient.scala @@ -0,0 +1,20 @@ +package de.nowchess.bot.client + +import de.nowchess.security.{InternalClientHeadersFactory, InternalSecretClientFilter} +import jakarta.ws.rs.* +import jakarta.ws.rs.core.MediaType +import org.eclipse.microprofile.rest.client.annotation.{RegisterClientHeaders, RegisterProvider} +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient + +case class SyncOfficialBotsRequest(bots: List[String]) + +@Path("/api/account/official-bots") +@RegisterRestClient(configKey = "account-service") +@RegisterProvider(classOf[InternalSecretClientFilter]) +@RegisterClientHeaders(classOf[InternalClientHeadersFactory]) +trait AccountServiceClient: + + @POST + @Path("/sync") + @Consumes(Array(MediaType.APPLICATION_JSON)) + def syncBots(req: SyncOfficialBotsRequest): Unit diff --git a/modules/official-bots/src/main/scala/de/nowchess/bot/service/OfficialBotService.scala b/modules/official-bots/src/main/scala/de/nowchess/bot/service/OfficialBotService.scala index bc1a8a1..958f568 100644 --- a/modules/official-bots/src/main/scala/de/nowchess/bot/service/OfficialBotService.scala +++ b/modules/official-bots/src/main/scala/de/nowchess/bot/service/OfficialBotService.scala @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.bot.BotController import de.nowchess.bot.BotDifficulty +import de.nowchess.bot.client.{AccountServiceClient, SyncOfficialBotsRequest} import de.nowchess.bot.config.RedisConfig import de.nowchess.io.fen.FenParser import io.micrometer.core.instrument.MeterRegistry @@ -13,6 +14,8 @@ import jakarta.annotation.PostConstruct import jakarta.enterprise.context.ApplicationScoped import jakarta.enterprise.event.Observes import jakarta.inject.Inject +import org.eclipse.microprofile.rest.client.inject.RestClient +import org.jboss.logging.Logger import scala.compiletime.uninitialized import java.util.function.Consumer import java.util.concurrent.TimeUnit @@ -20,12 +23,18 @@ import java.util.concurrent.TimeUnit @ApplicationScoped class OfficialBotService: + private val log = Logger.getLogger(classOf[OfficialBotService]) + // scalafix:off DisableSyntax.var @Inject var redis: RedisDataSource = uninitialized @Inject var redisConfig: RedisConfig = uninitialized @Inject var objectMapper: ObjectMapper = uninitialized @Inject var botController: BotController = uninitialized @Inject var meterRegistry: MeterRegistry = uninitialized + + @Inject + @RestClient + var accountServiceClient: AccountServiceClient = uninitialized // scalafix:on DisableSyntax.var private val terminalStatuses = @@ -39,7 +48,10 @@ class OfficialBotService: } def onStart(@Observes event: StartupEvent): Unit = - BotController.listBots.foreach(subscribeToEventChannel) + val bots = BotController.listBots + try accountServiceClient.syncBots(SyncOfficialBotsRequest(bots)) + catch case ex: Exception => log.errorf(ex, "Failed to auto-register official bots with account service") + bots.foreach(subscribeToEventChannel) private def subscribeToEventChannel(botName: String): Unit = val handler: Consumer[String] = msg => handleBotEvent(botName, msg)