From a12f979023c4b804438c7d00fae1b804f73119a3 Mon Sep 17 00:00:00 2001 From: Shahd Lala Date: Wed, 3 Jun 2026 13:27:03 +0200 Subject: [PATCH] fix(official-bots): NCS-70-auto-register official bots with account service (#59) Co-authored-by: Janis Reviewed-on: https://git.janis-eccarius.de/NowChess/NowChessSystems/pulls/59 --- .../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 | 32 +++++++++++++++++++ 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, 105 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 f6f6726..bd68232 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, token: Option[String] = None) 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 e75c437..b386612 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 f9027e8..4d97bdb 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 @@ -209,6 +209,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..6c7f3d5 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,35 @@ 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)