feat: true-microservices (#40)

Reviewed-on: #40
This commit is contained in:
2026-04-29 22:06:01 +02:00
parent 67511fc649
commit 590924254e
328 changed files with 10672 additions and 2939 deletions
@@ -0,0 +1,36 @@
quarkus:
http:
port: 8083
application:
name: nowchess-account
smallrye-openapi:
info-title: NowChess Account Service
path: /openapi
swagger-ui:
always-include: true
path: /swagger-ui
datasource:
db-kind: h2
username: sa
password: ""
jdbc:
url: "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"
hibernate-orm:
schema-management:
strategy: drop-and-create
mp:
jwt:
verify:
publickey:
location: keys/test-public.pem
issuer: nowchess
smallrye:
jwt:
sign:
key:
location: keys/test-private.pem
nowchess:
internal:
secret: test-secret
auth:
enabled: false
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4zBHgRLMez2b6
wfdvvTJVR8xxbr/kJUMiq4ot14KhtTaGikFW+77ezjoqabFWH7CNjDvASWCM2n7X
PxL4fhUwzvTbhRZ2XNM80lKB+OIjP3hoNLvgeSNHbS4CztOfk2JVtQFLQdYJ/gvB
oFPgBtZYO/SZVML28d5U92JrWRIC1e1Ht1oKwKJoOqtTJrs/RuOlKQ/du4kwY8m0
jPw05wFA1YRMUC78xKklCVYCufYewIUTdKxATK0ZKWBoPCJnxDg8gwgpnV1wHQrH
GcbZvhcVg3GWpDcYdnogV4rlssws57+uAhGRyQBkmmhVb+zT+LT7WXDPB46MnHkK
FIZaxEkHAgMBAAECggEAAvu4Zih1w8+RWAb9mZ4yS9Im6MXi7yny1YJzbp4GC9pD
ERT2TRMvV6V4puqh5EQKs55J8Ka+mkeEuLDZ+4z9hpYwucKCRFLnThoPHu4HqI4D
wZroVY1fFm4aygzQucjFU6DibnaXn/2r7upJsFor56zAHCGULCxnbHO58QW1Frqa
UrTndSkrxavBD9LL1ohPEy3saXlRCVAEM5l7jZbg52dPauIYAOv0e+EE3RETw/Xz
3EWukIZ7PKyoyuQm8Sv2u7lyISljDGlvrW5IjVRPMPqOKNOa/pV3qU4mbUY6GjbC
B4xt8kEKjVSkTeMXA+W0gnZddnQOtcQYSrYWWes+AQKBgQDzjmt1ZJktZG96M8+f
Ov9JznfzSLYxN7EboDhqjTVBOkb6flRSYrd9E6gReIIrq5Sjs9Z+toA/u8BmjQ/P
GTrrLVh6bLBicUGKcmQFKw/0D9lOlbxaMg8VO9rqSb/AslumJwjucU7DA+WAN52j
cyiLiw+EmWjL/DV51fHHI18SgQKBgQDCPRzpeP8Qox83/+tGR/6fSSRi5ec3ZVPy
aCCCZM6qqhLv3hJkV0djRruVVfe136PwUi20BW6aF0PXmxDIGRWqDLQGkvDNEhjw
ZLBv/dYtW2HBZhq4E0w8DiaNZCOWvpLQ3QCEtzmuhyHhNqYHzvmuerk+w4c/8fY6
DFyPyiAHhwKBgDrpO/zNNG/SV1SLq7CsKIvFsSXbdJY7Dk/MVVkQhs0cN4bnf6Xd
0twiIQj4ySOfAPkHyt4jbqn70/H6NNS3GZVBBqG2IIPvORcvzBmj7Nvv6XQkq8Z1
TUipja4V4JfPjHOIBZUHOzHYg26cBTk/5ZK7NCmyobKVcqnhofW1DI4BAoGAaRu4
8X5QSCh9VEhggH+lAX0K+5l9LTTf4GUIcocqbp/p73M0cKfqMYatK3qBuSF0DS/r
G2d1Gl1MkPeQdTddyc9l+8i4FcCdTjiuYWvy4kh49bbS7plCv5zIr+pod8JYoD13
clnUFOV7J+vynHccFZbDd3tHTQsaOv9Fd2nhOzECgYEA8SWBEmTuaBh+0vr6zS+E
wD+cwB3iaGo+7fP7TZ+v1kxoDlcDjPYM4ikiOB+OPGNkAfqc3MGsbhfgcxqD0+5r
kpCFyiyieyoT+7hkMpMsJCNwFO+29fc3DDqPX4Keqp26tMxtRzYea3GtVShiRXew
5i4ReFwm3/IWDn9kLmHT6Fg=
-----END PRIVATE KEY-----
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMwR4ESzHs9m+sH3b70y
VUfMcW6/5CVDIquKLdeCobU2hopBVvu+3s46KmmxVh+wjYw7wElgjNp+1z8S+H4V
MM7024UWdlzTPNJSgfjiIz94aDS74HkjR20uAs7Tn5NiVbUBS0HWCf4LwaBT4AbW
WDv0mVTC9vHeVPdia1kSAtXtR7daCsCiaDqrUya7P0bjpSkP3buJMGPJtIz8NOcB
QNWETFAu/MSpJQlWArn2HsCFE3SsQEytGSlgaDwiZ8Q4PIMIKZ1dcB0KxxnG2b4X
FYNxlqQ3GHZ6IFeK5bLMLOe/rgIRkckAZJpoVW/s0/i0+1lwzweOjJx5ChSGWsRJ
BwIDAQAB
-----END PUBLIC KEY-----
@@ -0,0 +1,107 @@
package de.nowchess.account.resource
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured
import io.restassured.http.ContentType
import org.hamcrest.Matchers.*
import org.junit.jupiter.api.Test
@QuarkusTest
class AccountResourceTest:
private def givenRequest() = RestAssured.`given`().contentType(ContentType.JSON)
private def registerBody(username: String, email: String = "", password: String = "secret") =
val resolvedEmail = if email.isEmpty then s"$username@example.com" else email
s"""{"username":"$username","email":"$resolvedEmail","password":"$password"}"""
private def loginBody(username: String, password: String = "secret") =
s"""{"username":"$username","password":"$password"}"""
private def registerAndLogin(username: String): String =
givenRequest()
.body(registerBody(username))
.when()
.post("/api/account")
.`then`()
.statusCode(200)
givenRequest()
.body(loginBody(username))
.when()
.post("/api/account/login")
.`then`()
.statusCode(200)
.extract()
.path[String]("token")
@Test
def registerReturns200(): Unit =
givenRequest()
.body(registerBody("alice"))
.when()
.post("/api/account")
.`then`()
.statusCode(200)
.body("username", is("alice"))
.body("rating", is(1500))
@Test
def registerConflictOnDuplicateUsername(): Unit =
givenRequest().body(registerBody("bob")).when().post("/api/account")
givenRequest()
.body(registerBody("bob"))
.when()
.post("/api/account")
.`then`()
.statusCode(409)
.body("error", containsString("bob"))
@Test
def loginReturns200WithToken(): Unit =
givenRequest().body(registerBody("charlie")).when().post("/api/account")
givenRequest()
.body(loginBody("charlie"))
.when()
.post("/api/account/login")
.`then`()
.statusCode(200)
.body("token", notNullValue())
@Test
def loginUnauthorizedOnWrongPassword(): Unit =
givenRequest().body(registerBody("dave")).when().post("/api/account")
givenRequest()
.body(loginBody("dave", "wrongpassword"))
.when()
.post("/api/account/login")
.`then`()
.statusCode(401)
@Test
def getMeReturns200(): Unit =
val token = registerAndLogin("eve")
givenRequest()
.header("Authorization", s"Bearer $token")
.when()
.get("/api/account/me")
.`then`()
.statusCode(200)
.body("username", is("eve"))
@Test
def getPublicProfileReturns200(): Unit =
givenRequest().body(registerBody("frank")).when().post("/api/account")
givenRequest()
.when()
.get("/api/account/frank")
.`then`()
.statusCode(200)
.body("username", is("frank"))
@Test
def getPublicProfileNotFound(): Unit =
givenRequest()
.when()
.get("/api/account/doesnotexist")
.`then`()
.statusCode(404)
@@ -0,0 +1,179 @@
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.{BeforeEach, Test}
import org.mockito.{ArgumentMatchers, Mockito}
@QuarkusTest
class ChallengeResourceTest:
@InjectMock
@RestClient
// scalafix:off DisableSyntax.var
var coreGameClient: CoreGameClient = scala.compiletime.uninitialized
// scalafix:on
@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 = "") =
val email = s"$username$suffix@test.com"
s"""{"username":"$username$suffix","email":"$email","password":"secret"}"""
private def loginBody(username: String, suffix: String = "") =
s"""{"username":"$username$suffix","password":"secret"}"""
private def registerAndLogin(username: String, suffix: String = ""): String =
givenRequest().body(registerBody(username, suffix)).when().post("/api/account")
givenRequest()
.body(loginBody(username, suffix))
.when()
.post("/api/account/login")
.`then`()
.statusCode(200)
.extract()
.path[String]("token")
private val clockBody =
"""{"color":"random","timeControl":{"type":"clock","limit":300,"increment":5}}"""
private def authed(token: String) =
givenRequest().header("Authorization", s"Bearer $token")
@Test
def createChallengeReturns201(): Unit =
val t1 = registerAndLogin("user1c")
registerAndLogin("user2c")
authed(t1)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/user2c")
.`then`()
.statusCode(201)
.body("status", is("created"))
.body("color", is("random"))
@Test
def createChallengeConflictOnDuplicate(): Unit =
val t1 = registerAndLogin("user1dup")
registerAndLogin("user2dup")
authed(t1).contentType(ContentType.JSON).body(clockBody).when().post("/api/challenge/user2dup")
authed(t1)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/user2dup")
.`then`()
.statusCode(409)
@Test
def createChallengeSelfForbidden(): Unit =
val token = registerAndLogin("selfuser")
authed(token)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/selfuser")
.`then`()
.statusCode(400)
@Test
def acceptChallengeReturns200(): Unit =
val t1 = registerAndLogin("accUser1")
val t2 = registerAndLogin("accUser2")
val challengeId = authed(t1)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/accUser2")
.`then`()
.statusCode(201)
.extract()
.path[String]("id")
authed(t2)
.when()
.post(s"/api/challenge/$challengeId/accept")
.`then`()
.statusCode(200)
.body("status", is("accepted"))
.body("gameId", is("test-game-id"))
@Test
def declineChallengeReturns200(): Unit =
val t1 = registerAndLogin("decUser1")
val t2 = registerAndLogin("decUser2")
val challengeId = authed(t1)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/decUser2")
.`then`()
.statusCode(201)
.extract()
.path[String]("id")
authed(t2)
.contentType(ContentType.JSON)
.body("""{"reason":"later"}""")
.when()
.post(s"/api/challenge/$challengeId/decline")
.`then`()
.statusCode(200)
.body("status", is("declined"))
.body("declineReason", is("later"))
@Test
def cancelChallengeReturns200(): Unit =
val t1 = registerAndLogin("canUser1")
registerAndLogin("canUser2")
val challengeId = authed(t1)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/canUser2")
.`then`()
.statusCode(201)
.extract()
.path[String]("id")
authed(t1)
.when()
.post(s"/api/challenge/$challengeId/cancel")
.`then`()
.statusCode(200)
.body("status", is("canceled"))
@Test
def listChallengesReturnsInAndOut(): Unit =
val t1 = registerAndLogin("listUser1")
registerAndLogin("listUser2")
registerAndLogin("listUser3")
authed(t1)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/listUser2")
.`then`()
.statusCode(201)
authed(t1)
.contentType(ContentType.JSON)
.body(clockBody)
.when()
.post("/api/challenge/listUser3")
.`then`()
.statusCode(201)
authed(t1)
.when()
.get("/api/challenge")
.`then`()
.statusCode(200)
.body("out.size()", is(2))
.body("in.size()", is(0))