From 6712e84b1bdb96a1bc1ead2c28a6fb68c07e0a85 Mon Sep 17 00:00:00 2001 From: alyssa Date: Wed, 4 Sep 2024 08:44:51 +0100 Subject: [PATCH 1/6] ktor things --- server/build.gradle.kts | 14 +++++++++++- server/gradle.properties | 4 ++++ server/src/main/java/util/MainUtil.java | 12 ---------- server/src/main/kotlin/Application.kt | 17 ++++++++++++++ server/src/main/kotlin/plugins/Monitoring.kt | 22 +++++++++++++++++++ server/src/main/kotlin/plugins/Routing.kt | 20 +++++++++++++++++ server/src/main/kotlin/plugins/Security.kt | 22 +++++++++++++++++++ .../src/main/kotlin/plugins/Serialization.kt | 21 ++++++++++++++++++ server/src/main/resources/application.yaml | 6 +++++ 9 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 server/gradle.properties delete mode 100644 server/src/main/java/util/MainUtil.java create mode 100644 server/src/main/kotlin/Application.kt create mode 100644 server/src/main/kotlin/plugins/Monitoring.kt create mode 100644 server/src/main/kotlin/plugins/Routing.kt create mode 100644 server/src/main/kotlin/plugins/Security.kt create mode 100644 server/src/main/kotlin/plugins/Serialization.kt create mode 100644 server/src/main/resources/application.yaml diff --git a/server/build.gradle.kts b/server/build.gradle.kts index bb2ba71..119d2f6 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("Entropy.kotlin-common-conventions") id("com.ncorti.ktfmt.gradle") version "0.15.1" + id("io.ktor.plugin") version "2.3.12" application } @@ -8,10 +9,21 @@ ktfmt { kotlinLangStyle() } dependencies { implementation(project(":core")) + implementation("io.ktor:ktor-server-core-jvm") + implementation("io.ktor:ktor-server-content-negotiation-jvm") + implementation("io.ktor:ktor-serialization-jackson-jvm") + implementation("io.ktor:ktor-server-call-logging-jvm") + implementation("io.ktor:ktor-server-call-id-jvm") + implementation("io.ktor:ktor-server-host-common-jvm") + implementation("io.ktor:ktor-server-status-pages-jvm") + implementation("io.ktor:ktor-server-sessions-jvm") + implementation("io.ktor:ktor-server-netty-jvm") + implementation("io.ktor:ktor-server-config-yaml") + testImplementation("io.ktor:ktor-server-test-host-jvm") testImplementation(project(":test-core")) } application { // Define the main class for the application. - mainClass.set("server.EntropyServer") + mainClass.set("io.ktor.server.netty.EngineMain") } diff --git a/server/gradle.properties b/server/gradle.properties new file mode 100644 index 0000000..d507b58 --- /dev/null +++ b/server/gradle.properties @@ -0,0 +1,4 @@ +kotlin.code.style=official +ktor_version=2.3.12 +kotlin_version=2.0.20 +logback_version=1.4.14 \ No newline at end of file diff --git a/server/src/main/java/util/MainUtil.java b/server/src/main/java/util/MainUtil.java deleted file mode 100644 index f0e08bd..0000000 --- a/server/src/main/java/util/MainUtil.java +++ /dev/null @@ -1,12 +0,0 @@ -package util; - -import screen.DebugConsole; - -public class MainUtil -{ - public static void initialise() - { - Debug.initialise(new DebugConsole()); - EncryptionUtil.setBase64Interface(new Base64Desktop()); - } -} diff --git a/server/src/main/kotlin/Application.kt b/server/src/main/kotlin/Application.kt new file mode 100644 index 0000000..d1e8bc1 --- /dev/null +++ b/server/src/main/kotlin/Application.kt @@ -0,0 +1,17 @@ +import plugins.* +import io.ktor.server.application.* +import server.EntropyServer + +fun main(args: Array) { + io.ktor.server.netty.EngineMain.main(args) +} + +fun Application.module() { + configureSerialization() + configureMonitoring() + configureSecurity() + configureRouting() + + // Boot old stuff + EntropyServer.main(emptyArray()) +} diff --git a/server/src/main/kotlin/plugins/Monitoring.kt b/server/src/main/kotlin/plugins/Monitoring.kt new file mode 100644 index 0000000..f5233b5 --- /dev/null +++ b/server/src/main/kotlin/plugins/Monitoring.kt @@ -0,0 +1,22 @@ +package plugins + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.plugins.callid.* +import io.ktor.server.plugins.callloging.* +import io.ktor.server.request.* +import org.slf4j.event.* + +fun Application.configureMonitoring() { + install(CallLogging) { + level = Level.INFO + filter { call -> call.request.path().startsWith("/") } + callIdMdc("call-id") + } + install(CallId) { + header(HttpHeaders.XRequestId) + verify { callId: String -> + callId.isNotEmpty() + } + } +} diff --git a/server/src/main/kotlin/plugins/Routing.kt b/server/src/main/kotlin/plugins/Routing.kt new file mode 100644 index 0000000..dfdaf62 --- /dev/null +++ b/server/src/main/kotlin/plugins/Routing.kt @@ -0,0 +1,20 @@ +package plugins + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.plugins.statuspages.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Application.configureRouting() { + install(StatusPages) { + exception { call, cause -> + call.respondText(text = "500: $cause" , status = HttpStatusCode.InternalServerError) + } + } + routing { + get("/") { + call.respondText("Hello World!") + } + } +} diff --git a/server/src/main/kotlin/plugins/Security.kt b/server/src/main/kotlin/plugins/Security.kt new file mode 100644 index 0000000..57a381f --- /dev/null +++ b/server/src/main/kotlin/plugins/Security.kt @@ -0,0 +1,22 @@ +package plugins + +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.sessions.* + +fun Application.configureSecurity() { + data class MySession(val count: Int = 0) + install(Sessions) { + cookie("MY_SESSION") { + cookie.extensions["SameSite"] = "lax" + } + } + routing { + get("/session/increment") { + val session = call.sessions.get() ?: MySession() + call.sessions.set(session.copy(count = session.count + 1)) + call.respondText("Counter is ${session.count}. Refresh to increment.") + } + } +} diff --git a/server/src/main/kotlin/plugins/Serialization.kt b/server/src/main/kotlin/plugins/Serialization.kt new file mode 100644 index 0000000..c151537 --- /dev/null +++ b/server/src/main/kotlin/plugins/Serialization.kt @@ -0,0 +1,21 @@ +package plugins + +import com.fasterxml.jackson.databind.* +import io.ktor.serialization.jackson.* +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Application.configureSerialization() { + install(ContentNegotiation) { + jackson { + enable(SerializationFeature.INDENT_OUTPUT) + } + } + routing { + get("/json/jackson") { + call.respond(mapOf("hello" to "world")) + } + } +} diff --git a/server/src/main/resources/application.yaml b/server/src/main/resources/application.yaml new file mode 100644 index 0000000..52c28c3 --- /dev/null +++ b/server/src/main/resources/application.yaml @@ -0,0 +1,6 @@ +ktor: + application: + modules: + - ApplicationKt.module + deployment: + port: 8080 From 11b75bdfbb291a53e4e57f037065c9c65c05cd06 Mon Sep 17 00:00:00 2001 From: Alyssa Date: Sat, 7 Sep 2024 15:08:57 +0100 Subject: [PATCH 2/6] health check controller with component test --- server/src/main/kotlin/Application.kt | 4 +-- server/src/main/kotlin/plugins/Monitoring.kt | 4 +-- server/src/main/kotlin/plugins/Routing.kt | 11 +++----- server/src/main/kotlin/plugins/Security.kt | 22 ---------------- .../src/main/kotlin/plugins/Serialization.kt | 13 +--------- .../routes/health/HealthCheckController.kt | 18 +++++++++++++ .../routes/health/HealthCheckService.kt | 8 ++++++ .../routes/health/dto/HealthCheckResponse.kt | 3 +++ server/src/test/kotlin/TestUtils.kt | 13 ++++++++++ .../health/HealthCheckControllerTest.kt | 26 +++++++++++++++++++ 10 files changed, 76 insertions(+), 46 deletions(-) delete mode 100644 server/src/main/kotlin/plugins/Security.kt create mode 100644 server/src/main/kotlin/routes/health/HealthCheckController.kt create mode 100644 server/src/main/kotlin/routes/health/HealthCheckService.kt create mode 100644 server/src/main/kotlin/routes/health/dto/HealthCheckResponse.kt create mode 100644 server/src/test/kotlin/TestUtils.kt create mode 100644 server/src/test/kotlin/routes/health/HealthCheckControllerTest.kt diff --git a/server/src/main/kotlin/Application.kt b/server/src/main/kotlin/Application.kt index d1e8bc1..0f45a73 100644 --- a/server/src/main/kotlin/Application.kt +++ b/server/src/main/kotlin/Application.kt @@ -1,15 +1,15 @@ -import plugins.* import io.ktor.server.application.* +import plugins.* import server.EntropyServer fun main(args: Array) { io.ktor.server.netty.EngineMain.main(args) } +@Suppress("UNUSED") fun Application.module() { configureSerialization() configureMonitoring() - configureSecurity() configureRouting() // Boot old stuff diff --git a/server/src/main/kotlin/plugins/Monitoring.kt b/server/src/main/kotlin/plugins/Monitoring.kt index f5233b5..88a09ad 100644 --- a/server/src/main/kotlin/plugins/Monitoring.kt +++ b/server/src/main/kotlin/plugins/Monitoring.kt @@ -15,8 +15,6 @@ fun Application.configureMonitoring() { } install(CallId) { header(HttpHeaders.XRequestId) - verify { callId: String -> - callId.isNotEmpty() - } + verify { callId: String -> callId.isNotEmpty() } } } diff --git a/server/src/main/kotlin/plugins/Routing.kt b/server/src/main/kotlin/plugins/Routing.kt index dfdaf62..fd9ba8c 100644 --- a/server/src/main/kotlin/plugins/Routing.kt +++ b/server/src/main/kotlin/plugins/Routing.kt @@ -4,17 +4,14 @@ import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.plugins.statuspages.* import io.ktor.server.response.* -import io.ktor.server.routing.* +import routes.health.HealthCheckController fun Application.configureRouting() { install(StatusPages) { exception { call, cause -> - call.respondText(text = "500: $cause" , status = HttpStatusCode.InternalServerError) - } - } - routing { - get("/") { - call.respondText("Hello World!") + call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError) } } + + HealthCheckController.installRoutes(this) } diff --git a/server/src/main/kotlin/plugins/Security.kt b/server/src/main/kotlin/plugins/Security.kt deleted file mode 100644 index 57a381f..0000000 --- a/server/src/main/kotlin/plugins/Security.kt +++ /dev/null @@ -1,22 +0,0 @@ -package plugins - -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import io.ktor.server.sessions.* - -fun Application.configureSecurity() { - data class MySession(val count: Int = 0) - install(Sessions) { - cookie("MY_SESSION") { - cookie.extensions["SameSite"] = "lax" - } - } - routing { - get("/session/increment") { - val session = call.sessions.get() ?: MySession() - call.sessions.set(session.copy(count = session.count + 1)) - call.respondText("Counter is ${session.count}. Refresh to increment.") - } - } -} diff --git a/server/src/main/kotlin/plugins/Serialization.kt b/server/src/main/kotlin/plugins/Serialization.kt index c151537..81db178 100644 --- a/server/src/main/kotlin/plugins/Serialization.kt +++ b/server/src/main/kotlin/plugins/Serialization.kt @@ -4,18 +4,7 @@ import com.fasterxml.jackson.databind.* import io.ktor.serialization.jackson.* import io.ktor.server.application.* import io.ktor.server.plugins.contentnegotiation.* -import io.ktor.server.response.* -import io.ktor.server.routing.* fun Application.configureSerialization() { - install(ContentNegotiation) { - jackson { - enable(SerializationFeature.INDENT_OUTPUT) - } - } - routing { - get("/json/jackson") { - call.respond(mapOf("hello" to "world")) - } - } + install(ContentNegotiation) { jackson { enable(SerializationFeature.INDENT_OUTPUT) } } } diff --git a/server/src/main/kotlin/routes/health/HealthCheckController.kt b/server/src/main/kotlin/routes/health/HealthCheckController.kt new file mode 100644 index 0000000..36ed14d --- /dev/null +++ b/server/src/main/kotlin/routes/health/HealthCheckController.kt @@ -0,0 +1,18 @@ +package routes.health + +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +object HealthCheckController { + private val healthCheckService = HealthCheckService() + + fun installRoutes(application: Application) { + application.routing { get("/health-check") { doHealthCheck(call) } } + } + + private suspend fun doHealthCheck(call: ApplicationCall) { + val response = healthCheckService.healthCheck() + call.respond(response) + } +} diff --git a/server/src/main/kotlin/routes/health/HealthCheckService.kt b/server/src/main/kotlin/routes/health/HealthCheckService.kt new file mode 100644 index 0000000..0ddb7cf --- /dev/null +++ b/server/src/main/kotlin/routes/health/HealthCheckService.kt @@ -0,0 +1,8 @@ +package routes.health + +import routes.health.dto.HealthCheckResponse +import util.OnlineConstants + +class HealthCheckService { + fun healthCheck() = HealthCheckResponse(OnlineConstants.SERVER_VERSION) +} diff --git a/server/src/main/kotlin/routes/health/dto/HealthCheckResponse.kt b/server/src/main/kotlin/routes/health/dto/HealthCheckResponse.kt new file mode 100644 index 0000000..07d337c --- /dev/null +++ b/server/src/main/kotlin/routes/health/dto/HealthCheckResponse.kt @@ -0,0 +1,3 @@ +package routes.health.dto + +data class HealthCheckResponse(val version: String) diff --git a/server/src/test/kotlin/TestUtils.kt b/server/src/test/kotlin/TestUtils.kt new file mode 100644 index 0000000..80eeadc --- /dev/null +++ b/server/src/test/kotlin/TestUtils.kt @@ -0,0 +1,13 @@ +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import io.kotest.matchers.shouldBe + +infix fun String.shouldMatchJson(expected: String) { + val mapper = jacksonObjectMapper() + + val parsed = mapper.readValue(this) + val expectedParsed = mapper.readValue(expected) + + parsed shouldBe expectedParsed +} diff --git a/server/src/test/kotlin/routes/health/HealthCheckControllerTest.kt b/server/src/test/kotlin/routes/health/HealthCheckControllerTest.kt new file mode 100644 index 0000000..ec5e9a5 --- /dev/null +++ b/server/src/test/kotlin/routes/health/HealthCheckControllerTest.kt @@ -0,0 +1,26 @@ +package routes.health + +import io.kotest.matchers.shouldBe +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.testing.* +import main.kotlin.testCore.AbstractTest +import org.junit.jupiter.api.Test +import shouldMatchJson +import util.OnlineConstants + +class HealthCheckControllerTest : AbstractTest() { + @Test + fun `Should respond to a health check request`() = testApplication { + val response = client.get("/health-check") + response.status shouldBe HttpStatusCode.OK + response.bodyAsText() shouldMatchJson + """ + { + "version": "${OnlineConstants.SERVER_VERSION}" + } + """ + .trimIndent() + } +} From 1a3b7d559ee4a6e2b6f5b6d36a375c562d8e15c3 Mon Sep 17 00:00:00 2001 From: Alyssa Date: Sun, 8 Sep 2024 18:49:12 +0100 Subject: [PATCH 3/6] WIP client API code --- client/build.gradle.kts | 1 + client/src/main/java/screen/MainScreen.java | 226 +++++++++--------- client/src/main/kotlin/http/ApiResponse.kt | 13 + client/src/main/kotlin/http/HealthCheckApi.kt | 9 + client/src/main/kotlin/http/HttpClient.kt | 72 ++++++ client/src/main/kotlin/util/Globals.kt | 10 + .../main/kotlin/dto/ClientErrorResponse.kt | 3 + .../main/kotlin}/dto/HealthCheckResponse.kt | 2 +- core/src/main/kotlin/logging/Logger.kt | 2 +- .../routes/health/HealthCheckService.kt | 2 +- 10 files changed, 222 insertions(+), 118 deletions(-) create mode 100644 client/src/main/kotlin/http/ApiResponse.kt create mode 100644 client/src/main/kotlin/http/HealthCheckApi.kt create mode 100644 client/src/main/kotlin/http/HttpClient.kt create mode 100644 client/src/main/kotlin/util/Globals.kt create mode 100644 core/src/main/kotlin/dto/ClientErrorResponse.kt rename {server/src/main/kotlin/routes/health => core/src/main/kotlin}/dto/HealthCheckResponse.kt (67%) diff --git a/client/build.gradle.kts b/client/build.gradle.kts index e4ab79b..2fc41ad 100644 --- a/client/build.gradle.kts +++ b/client/build.gradle.kts @@ -24,6 +24,7 @@ task("runDev") { closureOf { group = "run" classpath = project.the()["main"].runtimeClasspath + mainClass = "EntropyMain" args = listOf("devMode") } ) diff --git a/client/src/main/java/screen/MainScreen.java b/client/src/main/java/screen/MainScreen.java index d80eaeb..1e2e2ca 100644 --- a/client/src/main/java/screen/MainScreen.java +++ b/client/src/main/java/screen/MainScreen.java @@ -60,119 +60,112 @@ public MainScreen() { super(); setTitle("Entropy"); - - try - { - init(); - - setFocusable(true); - addWindowListener(this); - getContentPane().setLayout(new BorderLayout(0, 0)); - achievementMessageBottom.setBorder(new BevelBorder(BevelBorder.RAISED, null, null, null, null)); - achievementMessageBottom.setBounds(13, 460, 395, 80); - sidePanel.add(achievementMessageBottom); - achievementMessageBottom.setLayout(null); - achievementIconBottom.setBounds(45, 11, 56, 56); - achievementMessageBottom.add(achievementIconBottom); - achievementEarnedBottom.setHorizontalAlignment(SwingConstants.CENTER); - achievementEarnedBottom.setFont(new Font("Tahoma", Font.BOLD, 15)); - achievementEarnedBottom.setBounds(128, 11, 169, 25); - achievementMessageBottom.add(achievementEarnedBottom); - achievementTitleBottom.setFont(new Font("Tahoma", Font.BOLD, 12)); - achievementTitleBottom.setHorizontalAlignment(SwingConstants.CENTER); - achievementTitleBottom.setBounds(128, 43, 169, 20); - achievementMessageBottom.add(achievementTitleBottom); - xButtonBottom.setVerticalAlignment(SwingConstants.TOP); - xButtonBottom.setHorizontalAlignment(SwingConstants.RIGHT); - xButtonBottom.setFont(new Font("Tahoma", Font.BOLD, 15)); - xButtonBottom.setBounds(368, 5, 16, 18); - achievementMessageBottom.add(xButtonBottom); - achievementMessageTop.setBorder(new BevelBorder(BevelBorder.RAISED, null, null, null, null)); - achievementMessageTop.setBounds(13, 373, 395, 80); - sidePanel.add(achievementMessageTop); - achievementMessageTop.setLayout(null); - achievementIconTop.setBounds(45, 11, 56, 56); - achievementMessageTop.add(achievementIconTop); - achievementEarnedTop.setHorizontalAlignment(SwingConstants.CENTER); - achievementEarnedTop.setFont(new Font("Tahoma", Font.BOLD, 15)); - achievementEarnedTop.setBounds(128, 11, 169, 25); - achievementMessageTop.add(achievementEarnedTop); - xButtonTop.setFont(new Font("Tahoma", Font.BOLD, 15)); - xButtonTop.setVerticalAlignment(SwingConstants.TOP); - xButtonTop.setHorizontalAlignment(SwingConstants.RIGHT); - xButtonTop.setBounds(368, 5, 16, 18); - achievementMessageTop.add(xButtonTop); - achievementTitleTop.setHorizontalAlignment(SwingConstants.CENTER); - achievementTitleTop.setFont(new Font("Tahoma", Font.BOLD, 12)); - achievementTitleTop.setBounds(128, 43, 169, 20); - achievementMessageTop.add(achievementTitleTop); - scrollPane.setBounds(115, 72, 194, 144); - scrollPane.setVisible(false); - lblBidHistory.setVisible(false); - btnReplay.setVisible(false); - getContentPane().add(menuBar, BorderLayout.NORTH); - menuBar.add(mnFile); - mntmNewGame.setAccelerator(KeyStroke.getKeyStroke('N', InputEvent.CTRL_DOWN_MASK)); - mnFile.add(mntmNewGame); - mnFile.add(mntmContinueGame); - mnFile.add(mntmPlayOnline); - mnFile.add(mntmStatistics); - mnFile.add(mntmClearData); - mntmContinueGame.setEnabled(isGameToContinue); - mnFile.add(mntmExit); - menuBar.add(mnOptions); - mnOptions.add(mntmPreferences); - mnOptions.add(mntmViewReplays); - mnOptions.add(mntmAchievements); - menuBar.add(mnHelp); - mnHelp.add(mntmViewHelp); - mntmViewHelp.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0)); - mnHelp.add(mntmAbout); - mnHelp.add(mntmReportBug); - history.setLocation(0, 119); - scrollPane.setViewportView(history); - history.setVisibleRowCount(4); - scrollPane.setBackground(new Color(0, 0, 0, 0)); - scrollPane.setOpaque(false); - history.setBackground(new Color(0, 0, 0, 0)); - history.setOpaque(false); - splitPane.setDividerLocation(622); - splitPane.setEnabled(false); - getContentPane().add(splitPane); - sidePanel.add(scrollPane); - splitPane.setRightComponent(sidePanel); - sidePanel.setLayout(null); - lblBidHistory.setBounds(172, 40, 80, 21); - sidePanel.add(lblBidHistory); - lblBidHistory.setHorizontalAlignment(SwingConstants.CENTER); - lblBidHistory.setFont(new Font("Tahoma", Font.PLAIN, 17)); - btnNextRound.setBounds(155, 261, 115, 23); - sidePanel.add(btnNextRound); - lblResult.setContentType("text/html"); - lblResult.setBounds(96, 296, 232, 56); - sidePanel.add(lblResult); - lblResult.setFont(new Font("Tahoma", Font.PLAIN, 18)); - lblResult.setVisible(false); - lblResult.setOpaque(false); - lblResult.setBorder(BorderFactory.createEmptyBorder()); - lblResult.setBackground(new Color(0,0,0,0)); - lblResult.setEditable(false); - lblResult.setFocusable(false); - btnNextRound.setVisible(false); - btnReplay.setBounds(116, 215, 192, 23); - sidePanel.add(btnReplay); - splitPane.setLeftComponent(leftPanel); - leftPanel.setLayout(new BorderLayout(0, 0)); - leftPanel.add(commandBar, BorderLayout.SOUTH); - history.setCellRenderer(new BidListCellRenderer()); - - addKeyListener(this); - commandBar.setCheatListener(this); - } - catch (Throwable t) - { - Debug.stackTrace(t); - } + + init(); + + setFocusable(true); + addWindowListener(this); + getContentPane().setLayout(new BorderLayout(0, 0)); + achievementMessageBottom.setBorder(new BevelBorder(BevelBorder.RAISED, null, null, null, null)); + achievementMessageBottom.setBounds(13, 460, 395, 80); + sidePanel.add(achievementMessageBottom); + achievementMessageBottom.setLayout(null); + achievementIconBottom.setBounds(45, 11, 56, 56); + achievementMessageBottom.add(achievementIconBottom); + achievementEarnedBottom.setHorizontalAlignment(SwingConstants.CENTER); + achievementEarnedBottom.setFont(new Font("Tahoma", Font.BOLD, 15)); + achievementEarnedBottom.setBounds(128, 11, 169, 25); + achievementMessageBottom.add(achievementEarnedBottom); + achievementTitleBottom.setFont(new Font("Tahoma", Font.BOLD, 12)); + achievementTitleBottom.setHorizontalAlignment(SwingConstants.CENTER); + achievementTitleBottom.setBounds(128, 43, 169, 20); + achievementMessageBottom.add(achievementTitleBottom); + xButtonBottom.setVerticalAlignment(SwingConstants.TOP); + xButtonBottom.setHorizontalAlignment(SwingConstants.RIGHT); + xButtonBottom.setFont(new Font("Tahoma", Font.BOLD, 15)); + xButtonBottom.setBounds(368, 5, 16, 18); + achievementMessageBottom.add(xButtonBottom); + achievementMessageTop.setBorder(new BevelBorder(BevelBorder.RAISED, null, null, null, null)); + achievementMessageTop.setBounds(13, 373, 395, 80); + sidePanel.add(achievementMessageTop); + achievementMessageTop.setLayout(null); + achievementIconTop.setBounds(45, 11, 56, 56); + achievementMessageTop.add(achievementIconTop); + achievementEarnedTop.setHorizontalAlignment(SwingConstants.CENTER); + achievementEarnedTop.setFont(new Font("Tahoma", Font.BOLD, 15)); + achievementEarnedTop.setBounds(128, 11, 169, 25); + achievementMessageTop.add(achievementEarnedTop); + xButtonTop.setFont(new Font("Tahoma", Font.BOLD, 15)); + xButtonTop.setVerticalAlignment(SwingConstants.TOP); + xButtonTop.setHorizontalAlignment(SwingConstants.RIGHT); + xButtonTop.setBounds(368, 5, 16, 18); + achievementMessageTop.add(xButtonTop); + achievementTitleTop.setHorizontalAlignment(SwingConstants.CENTER); + achievementTitleTop.setFont(new Font("Tahoma", Font.BOLD, 12)); + achievementTitleTop.setBounds(128, 43, 169, 20); + achievementMessageTop.add(achievementTitleTop); + scrollPane.setBounds(115, 72, 194, 144); + scrollPane.setVisible(false); + lblBidHistory.setVisible(false); + btnReplay.setVisible(false); + getContentPane().add(menuBar, BorderLayout.NORTH); + menuBar.add(mnFile); + mntmNewGame.setAccelerator(KeyStroke.getKeyStroke('N', InputEvent.CTRL_DOWN_MASK)); + mnFile.add(mntmNewGame); + mnFile.add(mntmContinueGame); + mnFile.add(mntmPlayOnline); + mnFile.add(mntmStatistics); + mnFile.add(mntmClearData); + mntmContinueGame.setEnabled(isGameToContinue); + mnFile.add(mntmExit); + menuBar.add(mnOptions); + mnOptions.add(mntmPreferences); + mnOptions.add(mntmViewReplays); + mnOptions.add(mntmAchievements); + menuBar.add(mnHelp); + mnHelp.add(mntmViewHelp); + mntmViewHelp.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0)); + mnHelp.add(mntmAbout); + mnHelp.add(mntmReportBug); + history.setLocation(0, 119); + scrollPane.setViewportView(history); + history.setVisibleRowCount(4); + scrollPane.setBackground(new Color(0, 0, 0, 0)); + scrollPane.setOpaque(false); + history.setBackground(new Color(0, 0, 0, 0)); + history.setOpaque(false); + splitPane.setDividerLocation(622); + splitPane.setEnabled(false); + getContentPane().add(splitPane); + sidePanel.add(scrollPane); + splitPane.setRightComponent(sidePanel); + sidePanel.setLayout(null); + lblBidHistory.setBounds(172, 40, 80, 21); + sidePanel.add(lblBidHistory); + lblBidHistory.setHorizontalAlignment(SwingConstants.CENTER); + lblBidHistory.setFont(new Font("Tahoma", Font.PLAIN, 17)); + btnNextRound.setBounds(155, 261, 115, 23); + sidePanel.add(btnNextRound); + lblResult.setContentType("text/html"); + lblResult.setBounds(96, 296, 232, 56); + sidePanel.add(lblResult); + lblResult.setFont(new Font("Tahoma", Font.PLAIN, 18)); + lblResult.setVisible(false); + lblResult.setOpaque(false); + lblResult.setBorder(BorderFactory.createEmptyBorder()); + lblResult.setBackground(new Color(0,0,0,0)); + lblResult.setEditable(false); + lblResult.setFocusable(false); + btnNextRound.setVisible(false); + btnReplay.setBounds(116, 215, 192, 23); + sidePanel.add(btnReplay); + splitPane.setLeftComponent(leftPanel); + leftPanel.setLayout(new BorderLayout(0, 0)); + leftPanel.add(commandBar, BorderLayout.SOUTH); + history.setCellRenderer(new BidListCellRenderer()); + + addKeyListener(this); + commandBar.setCheatListener(this); //Listeners setUpMenuListeners(); @@ -681,8 +674,11 @@ public String processCommand(String command) return textToShow; } } - - if (command.equals("simulator")) + + if (command.equals("health")) { + Globals.INSTANCE.getHealthCheckApi().doHealthCheck(); + } + else if (command.equals("simulator")) { SimulationDialog dialog = ScreenCache.getSimulationDialog(); dialog.initVariables(); diff --git a/client/src/main/kotlin/http/ApiResponse.kt b/client/src/main/kotlin/http/ApiResponse.kt new file mode 100644 index 0000000..e5a2ce3 --- /dev/null +++ b/client/src/main/kotlin/http/ApiResponse.kt @@ -0,0 +1,13 @@ +package http + +interface ApiResponse { + val statusCode: Int +} + +data class SuccessResponse(override val statusCode: Int, val body: T) : ApiResponse + +data class FailureResponse( + override val statusCode: Int, + val errorCode: String, + val errorMessage: String +) : ApiResponse diff --git a/client/src/main/kotlin/http/HealthCheckApi.kt b/client/src/main/kotlin/http/HealthCheckApi.kt new file mode 100644 index 0000000..d0edecf --- /dev/null +++ b/client/src/main/kotlin/http/HealthCheckApi.kt @@ -0,0 +1,9 @@ +package http + +import dto.HealthCheckResponse + +class HealthCheckApi(private val httpClient: HttpClient) { + fun doHealthCheck() { + httpClient.doCall("GET", "/health-check") + } +} diff --git a/client/src/main/kotlin/http/HttpClient.kt b/client/src/main/kotlin/http/HttpClient.kt new file mode 100644 index 0000000..ba7fb5b --- /dev/null +++ b/client/src/main/kotlin/http/HttpClient.kt @@ -0,0 +1,72 @@ +package http + +import dto.ClientErrorResponse +import java.util.* +import kong.unirest.HttpResponse +import kong.unirest.JsonObjectMapper +import kong.unirest.Unirest +import logging.Severity +import util.Globals.baseUrl +import utils.InjectedThings.logger + +class HttpClient { + val jsonObjectMapper = JsonObjectMapper() + + inline fun doCall( + method: String, + route: String, + payload: Any? = null + ): ApiResponse { + val requestId = UUID.randomUUID() + val requestJson = payload?.let { jsonObjectMapper.writeValue(payload) } + + logger.info( + "http.request", + "$method $route", + "payload" to payload, + "requestId" to requestId, + "requestBody" to requestJson + ) + + val request = Unirest.request(method, "${baseUrl}${route}") + payload?.let { request.body(payload) } + val response = request.asString() + + if (response.isSuccess) { + logResponse(Severity.INFO, requestId, route, method, requestJson, response) + val body = jsonObjectMapper.readValue(response.body, T::class.java) + return SuccessResponse(response.status, body) + } else { + logResponse(Severity.ERROR, requestId, route, method, requestJson, response) + val errorResponse = + jsonObjectMapper.readValue(response.body, ClientErrorResponse::class.java) + return FailureResponse( + response.status, + errorResponse.errorCode, + errorResponse.errorMessage + ) + } + } + + fun logResponse( + level: Severity, + requestId: UUID, + route: String, + method: String, + requestJson: String?, + response: HttpResponse + ) { + logger.log( + level, + "http.response", + "Received ${response.status} for $method $route", + null, + mapOf( + "requestId" to requestId, + "requestBody" to requestJson, + "responseCode" to response.status, + "responseBody" to response.body.toString() + ) + ) + } +} diff --git a/client/src/main/kotlin/util/Globals.kt b/client/src/main/kotlin/util/Globals.kt new file mode 100644 index 0000000..82fcab7 --- /dev/null +++ b/client/src/main/kotlin/util/Globals.kt @@ -0,0 +1,10 @@ +package util + +import http.HealthCheckApi +import http.HttpClient + +object Globals { + val httpClient = HttpClient() + val baseUrl = "http://localhost:8080" + var healthCheckApi = HealthCheckApi(httpClient) +} diff --git a/core/src/main/kotlin/dto/ClientErrorResponse.kt b/core/src/main/kotlin/dto/ClientErrorResponse.kt new file mode 100644 index 0000000..af2077c --- /dev/null +++ b/core/src/main/kotlin/dto/ClientErrorResponse.kt @@ -0,0 +1,3 @@ +package dto + +data class ClientErrorResponse(val errorCode: String, val errorMessage: String) diff --git a/server/src/main/kotlin/routes/health/dto/HealthCheckResponse.kt b/core/src/main/kotlin/dto/HealthCheckResponse.kt similarity index 67% rename from server/src/main/kotlin/routes/health/dto/HealthCheckResponse.kt rename to core/src/main/kotlin/dto/HealthCheckResponse.kt index 07d337c..46c9178 100644 --- a/server/src/main/kotlin/routes/health/dto/HealthCheckResponse.kt +++ b/core/src/main/kotlin/dto/HealthCheckResponse.kt @@ -1,3 +1,3 @@ -package routes.health.dto +package dto data class HealthCheckResponse(val version: String) diff --git a/core/src/main/kotlin/logging/Logger.kt b/core/src/main/kotlin/logging/Logger.kt index fa1cbd1..7f8724d 100644 --- a/core/src/main/kotlin/logging/Logger.kt +++ b/core/src/main/kotlin/logging/Logger.kt @@ -60,7 +60,7 @@ class Logger(private val destinations: List) { ) } - private fun log( + fun log( severity: Severity, code: String, message: String, diff --git a/server/src/main/kotlin/routes/health/HealthCheckService.kt b/server/src/main/kotlin/routes/health/HealthCheckService.kt index 0ddb7cf..1261cce 100644 --- a/server/src/main/kotlin/routes/health/HealthCheckService.kt +++ b/server/src/main/kotlin/routes/health/HealthCheckService.kt @@ -1,6 +1,6 @@ package routes.health -import routes.health.dto.HealthCheckResponse +import dto.HealthCheckResponse import util.OnlineConstants class HealthCheckService { From da24366d8c61c74656224b6cb89c7aec61f055d9 Mon Sep 17 00:00:00 2001 From: alyssa Date: Tue, 10 Sep 2024 08:59:31 +0100 Subject: [PATCH 4/6] tests for some of the generic route handling --- .../test/kotlin/bean/HyperlinkAdaptorTest.kt | 2 +- client/src/test/kotlin/bean/LinkLabelTest.kt | 2 +- .../test/kotlin/help/AbstractHelpPanelTest.kt | 2 +- client/src/test/kotlin/help/HelpPanelTest.kt | 2 +- .../src/test/kotlin/util/DialogUtilNewTest.kt | 2 +- .../src/test/kotlin/util/UpdateManagerTest.kt | 2 +- client/src/test/kotlin/util/UrlUtilTest.kt | 2 +- .../test/kotlin/bean/FocusableWindowTest.kt | 2 +- .../logging/LogDestinationSystemOutTest.kt | 2 +- core/src/test/kotlin/logging/LoggerTest.kt | 2 +- .../LoggerUncaughtExceptionHandlerTest.kt | 2 +- .../test/kotlin/logging/LoggingConsoleTest.kt | 2 +- .../test/kotlin/logging/LoggingUtilsTest.kt | 2 +- core/src/test/kotlin/utils/MathsUtilTest.kt | 2 +- server/src/main/kotlin/plugins/Routing.kt | 21 ++++++- .../src/main/kotlin/routes/ClientException.kt | 10 +++ server/src/test/kotlin/TestUtils.kt | 13 ---- server/src/test/kotlin/plugins/RoutingTest.kt | 61 +++++++++++++++++++ .../health/HealthCheckControllerTest.kt | 4 +- test-core/build.gradle.kts | 1 + .../src/main/kotlin/testCore/AbstractTest.kt | 4 +- .../testCore/BeforeAllTestsExtension.kt | 1 + .../src/main/kotlin/testCore/JsonUtils.kt | 7 +++ 23 files changed, 119 insertions(+), 31 deletions(-) create mode 100644 server/src/main/kotlin/routes/ClientException.kt delete mode 100644 server/src/test/kotlin/TestUtils.kt create mode 100644 server/src/test/kotlin/plugins/RoutingTest.kt create mode 100644 test-core/src/main/kotlin/testCore/JsonUtils.kt diff --git a/client/src/test/kotlin/bean/HyperlinkAdaptorTest.kt b/client/src/test/kotlin/bean/HyperlinkAdaptorTest.kt index e111a1c..eb4b937 100644 --- a/client/src/test/kotlin/bean/HyperlinkAdaptorTest.kt +++ b/client/src/test/kotlin/bean/HyperlinkAdaptorTest.kt @@ -9,8 +9,8 @@ import java.awt.Cursor import java.awt.event.MouseEvent import javax.swing.JButton import javax.swing.JPanel -import main.kotlin.testCore.AbstractTest import org.junit.jupiter.api.Test +import testCore.AbstractTest private val mouseEventOverLink = makeMouseEvent(JButton()) private val mouseEventNotOverLink = makeMouseEvent(JButton()) diff --git a/client/src/test/kotlin/bean/LinkLabelTest.kt b/client/src/test/kotlin/bean/LinkLabelTest.kt index f881a21..b9dac4e 100644 --- a/client/src/test/kotlin/bean/LinkLabelTest.kt +++ b/client/src/test/kotlin/bean/LinkLabelTest.kt @@ -8,8 +8,8 @@ import io.mockk.mockk import io.mockk.verify import java.awt.Color import java.awt.Cursor -import main.kotlin.testCore.AbstractTest import org.junit.jupiter.api.Test +import testCore.AbstractTest class LinkLabelTest : AbstractTest() { @Test diff --git a/client/src/test/kotlin/help/AbstractHelpPanelTest.kt b/client/src/test/kotlin/help/AbstractHelpPanelTest.kt index 1029b78..f2df471 100644 --- a/client/src/test/kotlin/help/AbstractHelpPanelTest.kt +++ b/client/src/test/kotlin/help/AbstractHelpPanelTest.kt @@ -2,8 +2,8 @@ package help import io.kotest.matchers.shouldBe import javax.swing.JTextPane -import main.kotlin.testCore.AbstractTest import org.junit.jupiter.api.Test +import testCore.AbstractTest import utils.getAllChildComponentsForType abstract class AbstractHelpPanelTest : AbstractTest() { diff --git a/client/src/test/kotlin/help/HelpPanelTest.kt b/client/src/test/kotlin/help/HelpPanelTest.kt index 6c0a379..6a06b87 100644 --- a/client/src/test/kotlin/help/HelpPanelTest.kt +++ b/client/src/test/kotlin/help/HelpPanelTest.kt @@ -4,8 +4,8 @@ import io.kotest.matchers.shouldBe import java.awt.Color import java.awt.Font import javax.swing.JTextPane -import main.kotlin.testCore.AbstractTest import org.junit.jupiter.api.Test +import testCore.AbstractTest import util.EntropyColour class HelpPanelTest : AbstractTest() { diff --git a/client/src/test/kotlin/util/DialogUtilNewTest.kt b/client/src/test/kotlin/util/DialogUtilNewTest.kt index 7e6219d..1b7d34f 100644 --- a/client/src/test/kotlin/util/DialogUtilNewTest.kt +++ b/client/src/test/kotlin/util/DialogUtilNewTest.kt @@ -11,12 +11,12 @@ import io.kotest.matchers.shouldBe import javax.swing.JDialog import javax.swing.SwingUtilities import logging.Severity -import main.kotlin.testCore.AbstractTest import main.kotlin.testCore.getErrorDialog import main.kotlin.testCore.getInfoDialog import main.kotlin.testCore.getQuestionDialog import main.kotlin.testCore.runAsync import org.junit.jupiter.api.Test +import testCore.AbstractTest class DialogUtilNewTest : AbstractTest() { @Test diff --git a/client/src/test/kotlin/util/UpdateManagerTest.kt b/client/src/test/kotlin/util/UpdateManagerTest.kt index 3456f33..2e90d58 100644 --- a/client/src/test/kotlin/util/UpdateManagerTest.kt +++ b/client/src/test/kotlin/util/UpdateManagerTest.kt @@ -24,7 +24,6 @@ import kong.unirest.UnirestException import kong.unirest.json.JSONException import kong.unirest.json.JSONObject import logging.Severity -import main.kotlin.testCore.AbstractTest import main.kotlin.testCore.getDialogMessage import main.kotlin.testCore.getErrorDialog import main.kotlin.testCore.getInfoDialog @@ -34,6 +33,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import screen.LoadingDialog +import testCore.AbstractTest import testCore.assertDoesNotExit import testCore.assertExits diff --git a/client/src/test/kotlin/util/UrlUtilTest.kt b/client/src/test/kotlin/util/UrlUtilTest.kt index e6625df..d426447 100644 --- a/client/src/test/kotlin/util/UrlUtilTest.kt +++ b/client/src/test/kotlin/util/UrlUtilTest.kt @@ -6,8 +6,8 @@ import io.mockk.mockk import io.mockk.verify import java.io.IOException import logging.Severity -import main.kotlin.testCore.AbstractTest import org.junit.jupiter.api.Test +import testCore.AbstractTest class UrlUtilTest : AbstractTest() { @Test diff --git a/core/src/test/kotlin/bean/FocusableWindowTest.kt b/core/src/test/kotlin/bean/FocusableWindowTest.kt index bebfbbe..7297db9 100644 --- a/core/src/test/kotlin/bean/FocusableWindowTest.kt +++ b/core/src/test/kotlin/bean/FocusableWindowTest.kt @@ -3,8 +3,8 @@ package bean import io.kotest.matchers.shouldBe import io.mockk.mockk import logging.KEY_ACTIVE_WINDOW -import main.kotlin.testCore.AbstractTest import org.junit.jupiter.api.Test +import testCore.AbstractTest import utils.InjectedThings.logger class FocusableWindowTest : AbstractTest() { diff --git a/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt b/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt index 92e4ddf..b9fefb9 100644 --- a/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt +++ b/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt @@ -3,12 +3,12 @@ package logging import io.kotest.matchers.string.shouldContain import java.io.ByteArrayOutputStream import java.io.PrintStream -import main.kotlin.testCore.AbstractTest import main.kotlin.testCore.CURRENT_TIME_STRING import main.kotlin.testCore.makeLogRecord import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import testCore.AbstractTest class LogDestinationSystemOutTest : AbstractTest() { private val originalOut = System.out diff --git a/core/src/test/kotlin/logging/LoggerTest.kt b/core/src/test/kotlin/logging/LoggerTest.kt index 52c0abb..63c3fed 100644 --- a/core/src/test/kotlin/logging/LoggerTest.kt +++ b/core/src/test/kotlin/logging/LoggerTest.kt @@ -5,11 +5,11 @@ import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe import io.mockk.mockk import io.mockk.verify -import main.kotlin.testCore.AbstractTest import main.kotlin.testCore.CURRENT_TIME import main.kotlin.testCore.FakeLogDestination import main.kotlin.testCore.shouldContainKeyValues import org.junit.jupiter.api.Test +import testCore.AbstractTest class LoggerTest : AbstractTest() { @Test diff --git a/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt b/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt index 0d207e0..e2ca895 100644 --- a/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt +++ b/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt @@ -2,9 +2,9 @@ package logging import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain -import main.kotlin.testCore.AbstractTest import main.kotlin.testCore.shouldContainKeyValues import org.junit.jupiter.api.Test +import testCore.AbstractTest class LoggerUncaughtExceptionHandlerTest : AbstractTest() { @Test diff --git a/core/src/test/kotlin/logging/LoggingConsoleTest.kt b/core/src/test/kotlin/logging/LoggingConsoleTest.kt index 305b582..aaf9674 100644 --- a/core/src/test/kotlin/logging/LoggingConsoleTest.kt +++ b/core/src/test/kotlin/logging/LoggingConsoleTest.kt @@ -9,9 +9,9 @@ import io.kotest.matchers.string.shouldContain import java.awt.Color import javax.swing.JLabel import javax.swing.text.StyleConstants -import main.kotlin.testCore.AbstractTest import main.kotlin.testCore.makeLogRecord import org.junit.jupiter.api.Test +import testCore.AbstractTest import utils.getAllChildComponentsForType class LoggingConsoleTest : AbstractTest() { diff --git a/core/src/test/kotlin/logging/LoggingUtilsTest.kt b/core/src/test/kotlin/logging/LoggingUtilsTest.kt index 125e412..3c32ff2 100644 --- a/core/src/test/kotlin/logging/LoggingUtilsTest.kt +++ b/core/src/test/kotlin/logging/LoggingUtilsTest.kt @@ -2,8 +2,8 @@ package logging import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain -import main.kotlin.testCore.AbstractTest import org.junit.jupiter.api.Test +import testCore.AbstractTest class LoggingUtilsTest : AbstractTest() { @Test diff --git a/core/src/test/kotlin/utils/MathsUtilTest.kt b/core/src/test/kotlin/utils/MathsUtilTest.kt index 07eea9a..1096a76 100644 --- a/core/src/test/kotlin/utils/MathsUtilTest.kt +++ b/core/src/test/kotlin/utils/MathsUtilTest.kt @@ -2,8 +2,8 @@ package utils import getPercentage import io.kotest.matchers.shouldBe -import main.kotlin.testCore.AbstractTest import org.junit.jupiter.api.Test +import testCore.AbstractTest class MathsUtilTest : AbstractTest() { @Test diff --git a/server/src/main/kotlin/plugins/Routing.kt b/server/src/main/kotlin/plugins/Routing.kt index fd9ba8c..94ecb01 100644 --- a/server/src/main/kotlin/plugins/Routing.kt +++ b/server/src/main/kotlin/plugins/Routing.kt @@ -1,15 +1,34 @@ package plugins +import dto.ClientErrorResponse import io.ktor.http.* import io.ktor.server.application.* +import io.ktor.server.logging.toLogString import io.ktor.server.plugins.statuspages.* import io.ktor.server.response.* +import routes.ClientException import routes.health.HealthCheckController +import utils.InjectedThings.logger fun Application.configureRouting() { install(StatusPages) { + exception { call, cause -> + logger.info( + "clientError", + "Error handling ${call.request.toLogString()}: ${cause.message}", + "clientErrorCode" to cause.errorCode, + "clientErrorMessage" to cause.message + ) + call.respond(cause.statusCode, ClientErrorResponse(cause.errorCode, cause.message)) + } + exception { call, cause -> - call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError) + val errorMessage = "Error handling ${call.request.toLogString()}" + logger.error("internalServerError", errorMessage, cause) + call.respond( + HttpStatusCode.InternalServerError, + ClientErrorResponse("internalServerError", errorMessage) + ) } } diff --git a/server/src/main/kotlin/routes/ClientException.kt b/server/src/main/kotlin/routes/ClientException.kt new file mode 100644 index 0000000..04e2da3 --- /dev/null +++ b/server/src/main/kotlin/routes/ClientException.kt @@ -0,0 +1,10 @@ +package routes + +import io.ktor.http.HttpStatusCode + +class ClientException( + val statusCode: HttpStatusCode, + val errorCode: String, + override val message: String, + override val cause: Throwable? = null +) : RuntimeException(message) diff --git a/server/src/test/kotlin/TestUtils.kt b/server/src/test/kotlin/TestUtils.kt deleted file mode 100644 index 80eeadc..0000000 --- a/server/src/test/kotlin/TestUtils.kt +++ /dev/null @@ -1,13 +0,0 @@ -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import io.kotest.matchers.shouldBe - -infix fun String.shouldMatchJson(expected: String) { - val mapper = jacksonObjectMapper() - - val parsed = mapper.readValue(this) - val expectedParsed = mapper.readValue(expected) - - parsed shouldBe expectedParsed -} diff --git a/server/src/test/kotlin/plugins/RoutingTest.kt b/server/src/test/kotlin/plugins/RoutingTest.kt new file mode 100644 index 0000000..41afe6e --- /dev/null +++ b/server/src/test/kotlin/plugins/RoutingTest.kt @@ -0,0 +1,61 @@ +package plugins + +import io.kotest.matchers.shouldBe +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application +import io.ktor.server.routing.get +import io.ktor.server.routing.routing +import io.ktor.server.testing.testApplication +import org.junit.jupiter.api.Test +import routes.ClientException +import testCore.AbstractTest +import testCore.shouldMatchJson + +class RoutingTest : AbstractTest() { + @Test + fun `Should handle client errors`() = testApplication { + application { ErrorThrowingController.installRoutes(this) } + + val response = client.get("/client-error") + response.status shouldBe HttpStatusCode.Conflict + response.bodyAsText() shouldMatchJson + """{ + "errorCode": "conflictingEntity", + "errorMessage": "Entity conflicts with another" + }""" + .trimIndent() + } + + @Test + fun `Should handle unexpected errors`() = testApplication { + application { ErrorThrowingController.installRoutes(this) } + + val response = client.get("/internal-error") + response.status shouldBe HttpStatusCode.InternalServerError + response.bodyAsText() shouldMatchJson + """{ + "errorCode": "internalServerError", + "errorMessage": "Error handling GET - /internal-error" + }""" + .trimIndent() + + errorLogged() shouldBe true + } +} + +private object ErrorThrowingController { + fun installRoutes(application: Application) { + application.routing { + get("/internal-error") { throw NullPointerException("Test error") } + get("/client-error") { + throw ClientException( + HttpStatusCode.Conflict, + "conflictingEntity", + "Entity conflicts with another" + ) + } + } + } +} diff --git a/server/src/test/kotlin/routes/health/HealthCheckControllerTest.kt b/server/src/test/kotlin/routes/health/HealthCheckControllerTest.kt index ec5e9a5..daffc30 100644 --- a/server/src/test/kotlin/routes/health/HealthCheckControllerTest.kt +++ b/server/src/test/kotlin/routes/health/HealthCheckControllerTest.kt @@ -5,9 +5,9 @@ import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.testing.* -import main.kotlin.testCore.AbstractTest import org.junit.jupiter.api.Test -import shouldMatchJson +import testCore.AbstractTest +import testCore.shouldMatchJson import util.OnlineConstants class HealthCheckControllerTest : AbstractTest() { diff --git a/test-core/build.gradle.kts b/test-core/build.gradle.kts index 2cf9417..c1655c0 100644 --- a/test-core/build.gradle.kts +++ b/test-core/build.gradle.kts @@ -12,4 +12,5 @@ dependencies { implementation("io.mockk:mockk:1.13.4") implementation("io.kotest:kotest-assertions-core:5.5.4") implementation("com.github.alexburlton:swing-test:4.0.0") + implementation("org.skyscreamer:jsonassert:1.5.3") } diff --git a/test-core/src/main/kotlin/testCore/AbstractTest.kt b/test-core/src/main/kotlin/testCore/AbstractTest.kt index bf874bb..90684ac 100644 --- a/test-core/src/main/kotlin/testCore/AbstractTest.kt +++ b/test-core/src/main/kotlin/testCore/AbstractTest.kt @@ -1,4 +1,4 @@ -package main.kotlin.testCore +package testCore import com.github.alyssaburlton.swingtest.purgeWindows import io.kotest.assertions.fail @@ -9,6 +9,8 @@ import logging.LogDestinationSystemOut import logging.LogRecord import logging.Logger import logging.Severity +import main.kotlin.testCore.BeforeAllTestsExtension +import main.kotlin.testCore.FakeLogDestination import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.extension.ExtendWith diff --git a/test-core/src/main/kotlin/testCore/BeforeAllTestsExtension.kt b/test-core/src/main/kotlin/testCore/BeforeAllTestsExtension.kt index 33088e0..da14066 100644 --- a/test-core/src/main/kotlin/testCore/BeforeAllTestsExtension.kt +++ b/test-core/src/main/kotlin/testCore/BeforeAllTestsExtension.kt @@ -5,6 +5,7 @@ import java.time.ZoneId import logging.LoggerUncaughtExceptionHandler import org.junit.jupiter.api.extension.BeforeAllCallback import org.junit.jupiter.api.extension.ExtensionContext +import testCore.logger import utils.InjectedThings var doneOneTimeSetup = false diff --git a/test-core/src/main/kotlin/testCore/JsonUtils.kt b/test-core/src/main/kotlin/testCore/JsonUtils.kt new file mode 100644 index 0000000..f2f02c0 --- /dev/null +++ b/test-core/src/main/kotlin/testCore/JsonUtils.kt @@ -0,0 +1,7 @@ +package testCore + +import org.skyscreamer.jsonassert.JSONAssert + +infix fun String.shouldMatchJson(expected: String) { + JSONAssert.assertEquals(expected, this, false) +} From e1def6b2d990d656742736dd31b1175ce5f9754c Mon Sep 17 00:00:00 2001 From: alyssa Date: Wed, 11 Sep 2024 08:50:54 +0100 Subject: [PATCH 5/6] start on client-side tests --- client/build.gradle.kts | 1 + client/src/main/kotlin/http/HealthCheckApi.kt | 3 +- client/src/main/kotlin/http/HttpClient.kt | 45 ++++++--- client/src/main/kotlin/util/Globals.kt | 2 +- client/src/test/kotlin/http/HttpClientTest.kt | 97 +++++++++++++++++++ server/gradle.properties | 4 - server/src/test/kotlin/plugins/RoutingTest.kt | 8 +- 7 files changed, 139 insertions(+), 21 deletions(-) create mode 100644 client/src/test/kotlin/http/HttpClientTest.kt delete mode 100644 server/gradle.properties diff --git a/client/build.gradle.kts b/client/build.gradle.kts index 2fc41ad..0cb0b2b 100644 --- a/client/build.gradle.kts +++ b/client/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation("com.konghq:unirest-java:3.14.2") implementation(project(":core")) testImplementation(project(":test-core")) + testImplementation("com.konghq:unirest-mocks:3.14.2") } application { diff --git a/client/src/main/kotlin/http/HealthCheckApi.kt b/client/src/main/kotlin/http/HealthCheckApi.kt index d0edecf..7e01cc0 100644 --- a/client/src/main/kotlin/http/HealthCheckApi.kt +++ b/client/src/main/kotlin/http/HealthCheckApi.kt @@ -1,9 +1,10 @@ package http import dto.HealthCheckResponse +import kong.unirest.HttpMethod class HealthCheckApi(private val httpClient: HttpClient) { fun doHealthCheck() { - httpClient.doCall("GET", "/health-check") + httpClient.doCall(HttpMethod.GET, "/health-check") } } diff --git a/client/src/main/kotlin/http/HttpClient.kt b/client/src/main/kotlin/http/HttpClient.kt index ba7fb5b..e67f1e3 100644 --- a/client/src/main/kotlin/http/HttpClient.kt +++ b/client/src/main/kotlin/http/HttpClient.kt @@ -2,6 +2,7 @@ package http import dto.ClientErrorResponse import java.util.* +import kong.unirest.HttpMethod import kong.unirest.HttpResponse import kong.unirest.JsonObjectMapper import kong.unirest.Unirest @@ -13,9 +14,9 @@ class HttpClient { val jsonObjectMapper = JsonObjectMapper() inline fun doCall( - method: String, + method: HttpMethod, route: String, - payload: Any? = null + payload: Any? = null, ): ApiResponse { val requestId = UUID.randomUUID() val requestJson = payload?.let { jsonObjectMapper.writeValue(payload) } @@ -23,13 +24,12 @@ class HttpClient { logger.info( "http.request", "$method $route", - "payload" to payload, "requestId" to requestId, - "requestBody" to requestJson + "requestBody" to requestJson, ) - val request = Unirest.request(method, "${baseUrl}${route}") - payload?.let { request.body(payload) } + val request = Unirest.request(method.toString(), "${baseUrl}${route}") + payload?.let { request.body(requestJson) } val response = request.asString() if (response.isSuccess) { @@ -37,24 +37,39 @@ class HttpClient { val body = jsonObjectMapper.readValue(response.body, T::class.java) return SuccessResponse(response.status, body) } else { - logResponse(Severity.ERROR, requestId, route, method, requestJson, response) - val errorResponse = - jsonObjectMapper.readValue(response.body, ClientErrorResponse::class.java) + val errorResponse = tryParseErrorResponse(response) + logResponse( + Severity.ERROR, + requestId, + route, + method, + requestJson, + response, + errorResponse, + ) return FailureResponse( response.status, errorResponse.errorCode, - errorResponse.errorMessage + errorResponse.errorMessage, ) } } + fun tryParseErrorResponse(response: HttpResponse) = + try { + jsonObjectMapper.readValue(response.body, ClientErrorResponse::class.java) + } catch (e: Exception) { + null + } + fun logResponse( level: Severity, requestId: UUID, route: String, - method: String, + method: HttpMethod, requestJson: String?, - response: HttpResponse + response: HttpResponse, + errorResponse: ClientErrorResponse? = null, ) { logger.log( level, @@ -65,8 +80,10 @@ class HttpClient { "requestId" to requestId, "requestBody" to requestJson, "responseCode" to response.status, - "responseBody" to response.body.toString() - ) + "responseBody" to response.body?.toString(), + "clientErrorCode" to errorResponse?.errorCode, + "clientErrorMessage" to errorResponse?.errorMessage, + ), ) } } diff --git a/client/src/main/kotlin/util/Globals.kt b/client/src/main/kotlin/util/Globals.kt index 82fcab7..bb7933b 100644 --- a/client/src/main/kotlin/util/Globals.kt +++ b/client/src/main/kotlin/util/Globals.kt @@ -4,7 +4,7 @@ import http.HealthCheckApi import http.HttpClient object Globals { - val httpClient = HttpClient() + private val httpClient = HttpClient() val baseUrl = "http://localhost:8080" var healthCheckApi = HealthCheckApi(httpClient) } diff --git a/client/src/test/kotlin/http/HttpClientTest.kt b/client/src/test/kotlin/http/HttpClientTest.kt new file mode 100644 index 0000000..423009c --- /dev/null +++ b/client/src/test/kotlin/http/HttpClientTest.kt @@ -0,0 +1,97 @@ +package http + +import dto.ClientErrorResponse +import io.kotest.matchers.maps.shouldContain +import io.kotest.matchers.maps.shouldContainKeys +import io.kotest.matchers.shouldBe +import java.util.UUID +import kong.unirest.HttpMethod +import kong.unirest.HttpStatus +import kong.unirest.JsonObjectMapper +import kong.unirest.MockClient +import kong.unirest.MockResponse +import logging.Severity +import org.junit.jupiter.api.Test +import testCore.AbstractTest +import util.Globals + +class HttpClientTest : AbstractTest() { + @Test + fun `Successful GET request`() { + val mockClient = MockClient.register() + + mockClient + .expect(HttpMethod.GET, "${Globals.baseUrl}/test-endpoint") + .thenReturn( + """{ + "fieldOne": "foo", + "fieldTwo": 500 + }""" + .trimIndent() + ) + + val client = HttpClient() + val response = client.doCall(HttpMethod.GET, "/test-endpoint") + response shouldBe SuccessResponse(200, TestApiResponse("foo", 500)) + + val requestLog = verifyLog("http.request", Severity.INFO) + requestLog.message shouldBe "GET /test-endpoint" + requestLog.keyValuePairs.shouldContainKeys("requestId") + } + + @Test + fun `GET with error response`() { + val mockClient = MockClient.register() + val errorResponse = ClientErrorResponse("oh.dear", "a bid already exists") + + mockClient + .expect(HttpMethod.GET, "${Globals.baseUrl}/test-endpoint") + .thenReturn( + MockResponse( + HttpStatus.CONFLICT, + "Conflict", + JsonObjectMapper().writeValue(errorResponse), + ) + ) + + val client = HttpClient() + val response = client.doCall(HttpMethod.GET, "/test-endpoint") + response shouldBe FailureResponse(HttpStatus.CONFLICT, "oh.dear", "a bid already exists") + + val responseLog = verifyLog("http.response", Severity.ERROR) + responseLog.message shouldBe "Received 409 for GET /test-endpoint" + responseLog.keyValuePairs["responseCode"] shouldBe 409 + responseLog.keyValuePairs["clientErrorCode"] shouldBe "oh.dear" + responseLog.keyValuePairs["clientErrorMessage"] shouldBe "a bid already exists" + } + + @Test + fun `Successful POST with body`() { + val mockClient = MockClient.register() + val request = TestApiRequest(UUID.randomUUID()) + val expectedBody = JsonObjectMapper().writeValue(request) + + mockClient + .expect(HttpMethod.POST, "${Globals.baseUrl}/test-endpoint") + .thenReturn(MockResponse(HttpStatus.NO_CONTENT, "No Content", null)) + + val client = HttpClient() + val response = client.doCall(HttpMethod.POST, "/test-endpoint", request) + response shouldBe SuccessResponse(204, null) + + val requestLog = verifyLog("http.request", Severity.INFO) + requestLog.message shouldBe "POST /test-endpoint" + requestLog.keyValuePairs.shouldContainKeys("requestId") + requestLog.keyValuePairs.shouldContain("requestBody" to expectedBody) + + val responseLog = verifyLog("http.response", Severity.INFO) + responseLog.message shouldBe "Received 204 for POST /test-endpoint" + responseLog.keyValuePairs["requestId"] shouldBe requestLog.keyValuePairs["requestId"] + responseLog.keyValuePairs["responseCode"] shouldBe 204 + responseLog.keyValuePairs["responseBody"] shouldBe "null" + } +} + +private data class TestApiRequest(val userId: UUID) + +private data class TestApiResponse(val fieldOne: String, val fieldTwo: Int) diff --git a/server/gradle.properties b/server/gradle.properties deleted file mode 100644 index d507b58..0000000 --- a/server/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -kotlin.code.style=official -ktor_version=2.3.12 -kotlin_version=2.0.20 -logback_version=1.4.14 \ No newline at end of file diff --git a/server/src/test/kotlin/plugins/RoutingTest.kt b/server/src/test/kotlin/plugins/RoutingTest.kt index 41afe6e..6705a9c 100644 --- a/server/src/test/kotlin/plugins/RoutingTest.kt +++ b/server/src/test/kotlin/plugins/RoutingTest.kt @@ -43,6 +43,12 @@ class RoutingTest : AbstractTest() { errorLogged() shouldBe true } + + @Test + fun `Should 404 for non-existent route`() = testApplication { + val response = client.get("/non-existent-route") + response.status shouldBe HttpStatusCode.NotFound + } } private object ErrorThrowingController { @@ -53,7 +59,7 @@ private object ErrorThrowingController { throw ClientException( HttpStatusCode.Conflict, "conflictingEntity", - "Entity conflicts with another" + "Entity conflicts with another", ) } } From 8de6cf7a5c12e5c1708bc8c5541ca92d4116c611 Mon Sep 17 00:00:00 2001 From: Alyssa Date: Sun, 15 Sep 2024 14:16:37 +0100 Subject: [PATCH 6/6] Handle unstructured error responses --- client/src/main/kotlin/http/ApiResponse.kt | 4 ++-- client/src/main/kotlin/http/HttpClient.kt | 4 ++-- client/src/test/kotlin/http/HttpClientTest.kt | 19 ++++++++++++++++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/client/src/main/kotlin/http/ApiResponse.kt b/client/src/main/kotlin/http/ApiResponse.kt index e5a2ce3..d8398c2 100644 --- a/client/src/main/kotlin/http/ApiResponse.kt +++ b/client/src/main/kotlin/http/ApiResponse.kt @@ -8,6 +8,6 @@ data class SuccessResponse(override val statusCode: Int, val body: T) : ApiRe data class FailureResponse( override val statusCode: Int, - val errorCode: String, - val errorMessage: String + val errorCode: String?, + val errorMessage: String? ) : ApiResponse diff --git a/client/src/main/kotlin/http/HttpClient.kt b/client/src/main/kotlin/http/HttpClient.kt index e67f1e3..21dd63c 100644 --- a/client/src/main/kotlin/http/HttpClient.kt +++ b/client/src/main/kotlin/http/HttpClient.kt @@ -49,8 +49,8 @@ class HttpClient { ) return FailureResponse( response.status, - errorResponse.errorCode, - errorResponse.errorMessage, + errorResponse?.errorCode, + errorResponse?.errorMessage, ) } } diff --git a/client/src/test/kotlin/http/HttpClientTest.kt b/client/src/test/kotlin/http/HttpClientTest.kt index 423009c..86281c9 100644 --- a/client/src/test/kotlin/http/HttpClientTest.kt +++ b/client/src/test/kotlin/http/HttpClientTest.kt @@ -40,7 +40,24 @@ class HttpClientTest : AbstractTest() { } @Test - fun `GET with error response`() { + fun `GET with generic error response`() { + val mockClient = MockClient.register() + + mockClient + .expect(HttpMethod.GET, "${Globals.baseUrl}/test-endpoint") + .thenReturn(MockResponse(HttpStatus.NOT_FOUND, "Not found", null)) + + val client = HttpClient() + val response = client.doCall(HttpMethod.GET, "/test-endpoint") + response shouldBe FailureResponse(HttpStatus.NOT_FOUND, null, null) + + val responseLog = verifyLog("http.response", Severity.ERROR) + responseLog.message shouldBe "Received 404 for GET /test-endpoint" + responseLog.keyValuePairs["responseCode"] shouldBe 404 + } + + @Test + fun `GET with structured error response`() { val mockClient = MockClient.register() val errorResponse = ClientErrorResponse("oh.dear", "a bid already exists")