diff --git a/client/src/main/kotlin/http/DevApi.kt b/client/src/main/kotlin/http/DevApi.kt index cea22cc..278d85d 100644 --- a/client/src/main/kotlin/http/DevApi.kt +++ b/client/src/main/kotlin/http/DevApi.kt @@ -1,6 +1,6 @@ package http -import dto.DevCommandRequest +import http.dto.DevCommandRequest import kong.unirest.HttpMethod class DevApi(private val httpClient: HttpClient) { diff --git a/client/src/main/kotlin/http/HealthCheckApi.kt b/client/src/main/kotlin/http/HealthCheckApi.kt index 0c6b7eb..b029a15 100644 --- a/client/src/main/kotlin/http/HealthCheckApi.kt +++ b/client/src/main/kotlin/http/HealthCheckApi.kt @@ -1,6 +1,6 @@ package http -import dto.HealthCheckResponse +import http.dto.HealthCheckResponse import kong.unirest.HttpMethod class HealthCheckApi(private val httpClient: HttpClient) { diff --git a/client/src/main/kotlin/http/HttpClient.kt b/client/src/main/kotlin/http/HttpClient.kt index e4747f3..5bd4d13 100644 --- a/client/src/main/kotlin/http/HttpClient.kt +++ b/client/src/main/kotlin/http/HttpClient.kt @@ -1,6 +1,6 @@ package http -import dto.ClientErrorResponse +import http.dto.ClientErrorResponse import java.util.* import kong.unirest.HttpMethod import kong.unirest.HttpResponse diff --git a/client/src/test/kotlin/http/HttpClientTest.kt b/client/src/test/kotlin/http/HttpClientTest.kt index 2b93092..8f7a046 100644 --- a/client/src/test/kotlin/http/HttpClientTest.kt +++ b/client/src/test/kotlin/http/HttpClientTest.kt @@ -1,6 +1,6 @@ package http -import dto.ClientErrorResponse +import http.dto.ClientErrorResponse import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.matchers.maps.shouldContain import io.kotest.matchers.maps.shouldContainKeys diff --git a/core/src/main/kotlin/dto/ClientErrorResponse.kt b/core/src/main/kotlin/http/dto/ClientErrorResponse.kt similarity index 82% rename from core/src/main/kotlin/dto/ClientErrorResponse.kt rename to core/src/main/kotlin/http/dto/ClientErrorResponse.kt index af2077c..2c932f0 100644 --- a/core/src/main/kotlin/dto/ClientErrorResponse.kt +++ b/core/src/main/kotlin/http/dto/ClientErrorResponse.kt @@ -1,3 +1,3 @@ -package dto +package http.dto data class ClientErrorResponse(val errorCode: String, val errorMessage: String) diff --git a/core/src/main/kotlin/dto/DevCommandRequest.kt b/core/src/main/kotlin/http/dto/DevCommandRequest.kt similarity index 75% rename from core/src/main/kotlin/dto/DevCommandRequest.kt rename to core/src/main/kotlin/http/dto/DevCommandRequest.kt index 81dbe6b..edc5370 100644 --- a/core/src/main/kotlin/dto/DevCommandRequest.kt +++ b/core/src/main/kotlin/http/dto/DevCommandRequest.kt @@ -1,3 +1,3 @@ -package dto +package http.dto data class DevCommandRequest(val command: String) diff --git a/core/src/main/kotlin/dto/HealthCheckResponse.kt b/core/src/main/kotlin/http/dto/HealthCheckResponse.kt similarity index 75% rename from core/src/main/kotlin/dto/HealthCheckResponse.kt rename to core/src/main/kotlin/http/dto/HealthCheckResponse.kt index 46c9178..ebb7790 100644 --- a/core/src/main/kotlin/dto/HealthCheckResponse.kt +++ b/core/src/main/kotlin/http/dto/HealthCheckResponse.kt @@ -1,3 +1,3 @@ -package dto +package http.dto data class HealthCheckResponse(val version: String) diff --git a/server/src/main/java/object/Room.java b/server/src/main/java/object/Room.java index 94f2e29..5c52661 100644 --- a/server/src/main/java/object/Room.java +++ b/server/src/main/java/object/Room.java @@ -1,21 +1,16 @@ package object; +import auth.UserConnection; +import org.w3c.dom.Document; +import server.EntropyServer; +import util.*; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; -import org.w3c.dom.Document; - -import server.EntropyServer; -import util.CardsUtil; -import util.Debug; -import util.EntropyUtil; -import util.StatisticsUtil; -import util.XmlBuilderServer; -import util.XmlConstants; - /** * Server-side version of a Room */ diff --git a/server/src/main/java/object/ServerRunnable.java b/server/src/main/java/object/ServerRunnable.java index 77bcafe..3d0b517 100644 --- a/server/src/main/java/object/ServerRunnable.java +++ b/server/src/main/java/object/ServerRunnable.java @@ -1,6 +1,8 @@ package object; +import auth.UserConnection; + public interface ServerRunnable extends Runnable { public abstract String getDetails(); diff --git a/server/src/main/java/object/ServerThread.java b/server/src/main/java/object/ServerThread.java index 5b4b6eb..b82490b 100644 --- a/server/src/main/java/object/ServerThread.java +++ b/server/src/main/java/object/ServerThread.java @@ -1,6 +1,6 @@ package object; -import util.Debug; +import auth.UserConnection; public class ServerThread extends Thread { diff --git a/server/src/main/java/object/UserConnection.java b/server/src/main/java/object/UserConnection.java deleted file mode 100644 index e6117be..0000000 --- a/server/src/main/java/object/UserConnection.java +++ /dev/null @@ -1,216 +0,0 @@ -package object; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.crypto.SecretKey; - -import org.w3c.dom.Document; - -import server.EntropyServer; -import server.NotificationRunnable; -import util.ColourGenerator; -import util.Debug; -import util.EncryptionUtil; -import util.XmlConstants; - -public class UserConnection -{ - private String ipAddress = null; - private SecretKey symmetricKey = null; - private String username = null; - private String colour = null; - private long lastActive = -1; - private boolean mobile = false; - private HashMap> hmNotificationQueueBySocketName = new HashMap<>(); - private HashMap hmSocketBySocketName = new HashMap<>(); - private HashMap hmWaitObjBySocketName = new HashMap<>(); - - /** - * When a connection is initiated - */ - public UserConnection(String ipAddress, SecretKey symmetricKey) - { - this.ipAddress = ipAddress; - this.symmetricKey = symmetricKey; - - initialiseSocketHashMaps(); - } - - /** - * When the logon process is completed - */ - public void update(String username, boolean mobile) - { - this.username = username; - this.mobile = mobile; - - colour = ColourGenerator.generateNextColour(); - setLastActiveNow(); - - Debug.append("New user connected: " + username); - } - - public SecretKey getSymmetricKey() - { - return symmetricKey; - } - public String getUsername() - { - return username; - } - public void setUsername(String username) - { - this.username = username; - } - public String getColour() - { - return colour; - } - public void setColour(String colour) - { - this.colour = colour; - } - public String getIpAddress() - { - return ipAddress; - } - - public long getLastActive() - { - return lastActive; - } - public void setLastActiveNow() - { - this.lastActive = System.currentTimeMillis(); - } - public boolean getMobile() - { - return mobile; - } - - public void destroyNotificationSockets() - { - Iterator it = hmSocketBySocketName.keySet().iterator(); - for (; it.hasNext(); ) - { - String socketType = it.next(); - replaceNotificationSocket(socketType, null); - } - } - - /** - * Synchronize on the wait object so we wait for any current attempt to send a Notification to go into its - * wait() block. This way we ensure it gets our notify() call. If we sneak in here before it that's fine too, - * as when it tries to send there'll be a fresh new socket. - */ - public void replaceNotificationSocket(String socketType, NotificationSocket socket) - { - Object notificationWaitObj = hmWaitObjBySocketName.get(socketType); - - synchronized (notificationWaitObj) - { - NotificationSocket existingSocket = hmSocketBySocketName.get(socketType); - if (existingSocket != null) - { - existingSocket.closeResources(); - } - - hmSocketBySocketName.put(socketType, socket); - notificationWaitObj.notify(); - } - } - - public NotificationSocket getNotificationSocket(String socketType) - { - return hmSocketBySocketName.get(socketType); - } - - @Override - public String toString() - { - String desc = username + " @ " + ipAddress; - - if (mobile) - { - desc += " (mob)"; - } - - if (symmetricKey != null) - { - desc += ", " + EncryptionUtil.convertSecretKeyToString(symmetricKey); - } - - return desc; - } - - public void sendNotificationInWorkerPool(Document message, EntropyServer server, String socketName, AtomicInteger counter) - { - NotificationRunnable runnable = new NotificationRunnable(server, message, this, counter, socketName); - server.executeInWorkerPool(runnable); - } - - public void addNotificationToQueue(String socketType, Document message) - { - ArrayList notificationQueue = hmNotificationQueueBySocketName.get(socketType); - notificationQueue.add(message); - } - public ArrayList getNotificationQueue(String socketName) - { - return hmNotificationQueueBySocketName.get(socketName); - } - public Document getNextNotificationToSend(String socketType) - { - ArrayList notificationQueue = hmNotificationQueueBySocketName.get(socketType); - int size = notificationQueue.size(); - if (size > 0) - { - return notificationQueue.remove(0); - } - else - { - return null; - } - } - public int getNotificationQueueSize(String socketName) - { - ArrayList notificationQueue = hmNotificationQueueBySocketName.get(socketName); - return notificationQueue.size(); - } - - public void waitForNewNotificationSocket(String socketName) - { - Object waitObj = hmWaitObjBySocketName.get(socketName); - - try - { - waitObj.wait(); - } - catch (InterruptedException t) - { - //Not expecting interruptions - Debug.stackTrace(t); - } - } - - private void initialiseSocketHashMaps() - { - initialiaseHashMaps(XmlConstants.SOCKET_NAME_CHAT); - initialiaseHashMaps(XmlConstants.SOCKET_NAME_LOBBY); - initialiaseHashMaps(XmlConstants.SOCKET_NAME_GAME); - } - - private void initialiaseHashMaps(String socketType) - { - hmWaitObjBySocketName.put(socketType, new Object()); - hmNotificationQueueBySocketName.put(socketType, new ArrayList()); - } - - public Object getNotificationWaitObject(String socketType) - { - return hmWaitObjBySocketName.get(socketType); - } -} diff --git a/server/src/main/java/server/EntropyServer.java b/server/src/main/java/server/EntropyServer.java index 782fb71..c71d885 100644 --- a/server/src/main/java/server/EntropyServer.java +++ b/server/src/main/java/server/EntropyServer.java @@ -1,5 +1,6 @@ package server; +import auth.UserConnection; import logging.LoggerUncaughtExceptionHandler; import object.*; import org.w3c.dom.Document; diff --git a/server/src/main/java/server/InactiveCheckRunnable.java b/server/src/main/java/server/InactiveCheckRunnable.java index 35c6fea..6725981 100644 --- a/server/src/main/java/server/InactiveCheckRunnable.java +++ b/server/src/main/java/server/InactiveCheckRunnable.java @@ -2,8 +2,8 @@ import java.util.List; +import auth.UserConnection; import object.ServerRunnable; -import object.UserConnection; import util.Debug; import static utils.InjectedThings.logger; diff --git a/server/src/main/java/server/ListenerRunnable.java b/server/src/main/java/server/ListenerRunnable.java index d710946..239fe31 100644 --- a/server/src/main/java/server/ListenerRunnable.java +++ b/server/src/main/java/server/ListenerRunnable.java @@ -1,7 +1,7 @@ package server; +import auth.UserConnection; import object.ServerRunnable; -import object.UserConnection; import util.Debug; import java.net.ServerSocket; diff --git a/server/src/main/java/server/MessageHandlerRunnable.java b/server/src/main/java/server/MessageHandlerRunnable.java index c02bc3c..fff33de 100644 --- a/server/src/main/java/server/MessageHandlerRunnable.java +++ b/server/src/main/java/server/MessageHandlerRunnable.java @@ -12,10 +12,10 @@ import javax.crypto.SecretKey; +import auth.UserConnection; import object.NotificationSocket; import object.Room; import object.ServerRunnable; -import object.UserConnection; import org.w3c.dom.Document; import org.w3c.dom.Element; diff --git a/server/src/main/java/server/NotificationRunnable.java b/server/src/main/java/server/NotificationRunnable.java index 58b1a69..4468c52 100644 --- a/server/src/main/java/server/NotificationRunnable.java +++ b/server/src/main/java/server/NotificationRunnable.java @@ -5,9 +5,9 @@ import java.net.SocketTimeoutException; import java.util.concurrent.atomic.AtomicInteger; +import auth.UserConnection; import object.NotificationSocket; import object.ServerRunnable; -import object.UserConnection; import org.w3c.dom.Document; diff --git a/server/src/main/java/util/EntropyThreadPoolExecutor.java b/server/src/main/java/util/EntropyThreadPoolExecutor.java index 07b6dfd..4b525f8 100644 --- a/server/src/main/java/util/EntropyThreadPoolExecutor.java +++ b/server/src/main/java/util/EntropyThreadPoolExecutor.java @@ -54,9 +54,9 @@ import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; +import auth.UserConnection; import object.ServerRunnable; import object.ServerThread; -import object.UserConnection; import utils.InjectedThings; /** diff --git a/server/src/main/java/util/LoggingUtil.kt b/server/src/main/java/util/LoggingUtil.kt index 459d230..0c6c886 100644 --- a/server/src/main/java/util/LoggingUtil.kt +++ b/server/src/main/java/util/LoggingUtil.kt @@ -1,45 +1,19 @@ package util -import `object`.ServerRunnable import `object`.ServerThread -import `object`.UserConnection import utils.InjectedThings.logger fun dumpServerThreads() { val sb = StringBuilder() val dumpTimeMillis = System.currentTimeMillis() - val backgroundThreads = mutableListOf() - val nonUscThreads = mutableListOf() - val hmUscToThreads = mutableMapOf>() - var loggerThread: Thread? = null - var idleWorkers = 0 + val threads = Thread.getAllStackTraces().keys + val serverThreads = threads.filterIsInstance() + val backgroundThreads = threads - serverThreads - val threads = Thread.getAllStackTraces() - val it: Iterator = threads.keys.iterator() - while (it.hasNext()) { - val t = it.next() - if (t !is ServerThread) { - if (t.name == "Logger") { - loggerThread = t - } else { - backgroundThreads.add(t) - } - - continue - } - - val r: ServerRunnable = t.runnable - val usc = r.userConnection - if (usc != null) { - val hashedThreads = hmUscToThreads.getOrPut(usc, ::mutableListOf) - hashedThreads.add(t) - } else if (r.getDetails() == "No current task") { - idleWorkers++ - } else { - nonUscThreads.add(t) - } - } + val nonUscThreads = serverThreads.filter { it.runnable.userConnection == null } + val uscThreads = serverThreads.filter { it.runnable.userConnection != null } + val idleCount = nonUscThreads.count { it.runnable.details == "No current task" } // Now dump the threads, grouped together sb.append("Threads @ ") @@ -50,43 +24,23 @@ fun dumpServerThreads() { sb.append("-----------------") sb.append("\n\n") - // Dump the non-usc threads first. These are "permanent" threads, so not interested in time - // running + // Dump the non-usc threads first. dumpServerThreadDetails(sb, nonUscThreads, -1) - dumpOrdinaryThreadDetails(sb, loggerThread) sb.append("\n\n") - - // Idle worker count, nothing interesting to log for these so don't log a line for each - if (idleWorkers > 0) { - sb.append("Idle workers: ") - sb.append(idleWorkers) - sb.append("\n\n") - } + sb.append("Idle workers: $idleCount\n\n") // Dump the usc threads if we have some - these are the most interesting - if (hmUscToThreads.isNotEmpty()) { - sb.append("Active workers\n") - sb.append("-----------------") - sb.append("\n\n") - - val itUsc: Iterator> = hmUscToThreads.values.iterator() - while (itUsc.hasNext()) { - val serverThreadsForUsc = itUsc.next() - dumpServerThreadDetails(sb, serverThreadsForUsc, dumpTimeMillis) - } - - sb.append("\n\n") - } + sb.append("Active workers\n") + sb.append("-----------------") + sb.append("\n\n") + dumpServerThreadDetails(sb, uscThreads, dumpTimeMillis) + sb.append("\n\n") // Dump basic info about other JVM threads sb.append("JVM threads\n") sb.append("-----------------") sb.append("\n\n") - - for (i in backgroundThreads.indices) { - val t = backgroundThreads[i] - dumpOrdinaryThreadDetails(sb, t) - } + backgroundThreads.forEach { dumpOrdinaryThreadDetails(sb, it) } logger.info("threads", sb.toString()) } @@ -96,13 +50,9 @@ private fun dumpServerThreadDetails( serverThreads: List, dumpTimeMillis: Long ) { - for (i in serverThreads.indices) { - val t: ServerThread = serverThreads[i] - t.dumpDetails(sb, dumpTimeMillis) - } + serverThreads.forEach { it.dumpDetails(sb, dumpTimeMillis) } } -private fun dumpOrdinaryThreadDetails(sb: StringBuilder, t: Thread?) { - val debugStr = t!!.name + " (" + t.state + ")\n" - sb.append(debugStr) +private fun dumpOrdinaryThreadDetails(sb: StringBuilder, t: Thread) { + sb.append("${t.name} (${t.state})\n") } diff --git a/server/src/main/java/util/XmlBuilderServer.java b/server/src/main/java/util/XmlBuilderServer.java index 2663f6d..8f48aac 100644 --- a/server/src/main/java/util/XmlBuilderServer.java +++ b/server/src/main/java/util/XmlBuilderServer.java @@ -1,5 +1,6 @@ package util; +import auth.UserConnection; import object.*; import org.w3c.dom.Document; import org.w3c.dom.Element; diff --git a/server/src/main/kotlin/auth/UserConnection.kt b/server/src/main/kotlin/auth/UserConnection.kt new file mode 100644 index 0000000..b2489ba --- /dev/null +++ b/server/src/main/kotlin/auth/UserConnection.kt @@ -0,0 +1,149 @@ +package auth + +import java.util.concurrent.atomic.AtomicInteger +import javax.crypto.SecretKey +import `object`.NotificationSocket +import org.w3c.dom.Document +import server.EntropyServer +import server.NotificationRunnable +import util.ColourGenerator +import util.Debug +import util.EncryptionUtil +import util.XmlConstants + +data class UserConnection(val ipAddress: String, val symmetricKey: SecretKey?) { + var username: String? = null + var colour: String? = null + var lastActive: Long = -1 + private set + + var mobile: Boolean = false + private set + + private val hmNotificationQueueBySocketName = HashMap>() + private val hmSocketBySocketName = HashMap() + private val hmWaitObjBySocketName = HashMap() + + /** When a connection is initiated */ + init { + initialiseSocketHashMaps() + } + + /** When the logon process is completed */ + fun update(username: String, mobile: Boolean) { + this.username = username + this.mobile = mobile + + colour = ColourGenerator.generateNextColour() + setLastActiveNow() + + Debug.append("New user connected: $username") + } + + fun setLastActiveNow() { + this.lastActive = System.currentTimeMillis() + } + + fun destroyNotificationSockets() { + val it: Iterator = hmSocketBySocketName.keys.iterator() + while (it.hasNext()) { + val socketType = it.next() + replaceNotificationSocket(socketType, null) + } + } + + /** + * Synchronize on the wait object so we wait for any current attempt to send a Notification to + * go into its wait() block. This way we ensure it gets our notify() call. If we sneak in here + * before it that's fine too, as when it tries to send there'll be a fresh new socket. + */ + fun replaceNotificationSocket(socketType: String, socket: NotificationSocket?) { + val notificationWaitObj = hmWaitObjBySocketName.getValue(socketType) + + synchronized(notificationWaitObj) { + val existingSocket = hmSocketBySocketName[socketType] + existingSocket?.closeResources() + + hmSocketBySocketName[socketType] = socket + notificationWaitObj.notify() + } + } + + fun getNotificationSocket(socketType: String): NotificationSocket? { + return hmSocketBySocketName[socketType] + } + + override fun toString(): String { + var desc = "$username @ $ipAddress" + + if (mobile) { + desc += " (mob)" + } + + if (symmetricKey != null) { + desc += ", " + EncryptionUtil.convertSecretKeyToString(symmetricKey) + } + + return desc + } + + fun sendNotificationInWorkerPool( + message: Document?, + server: EntropyServer, + socketName: String?, + counter: AtomicInteger? + ) { + val runnable = NotificationRunnable(server, message, this, counter, socketName) + server.executeInWorkerPool(runnable) + } + + fun addNotificationToQueue(socketType: String, message: Document) { + val notificationQueue = hmNotificationQueueBySocketName.getValue(socketType) + notificationQueue.add(message) + } + + fun getNotificationQueue(socketName: String): ArrayList { + return hmNotificationQueueBySocketName[socketName]!! + } + + fun getNextNotificationToSend(socketType: String): Document? { + val notificationQueue = hmNotificationQueueBySocketName.getValue(socketType) + val size = notificationQueue.size + return if (size > 0) { + notificationQueue.removeAt(0) + } else { + null + } + } + + fun getNotificationQueueSize(socketName: String): Int { + val notificationQueue = hmNotificationQueueBySocketName.getValue(socketName) + return notificationQueue.size + } + + fun waitForNewNotificationSocket(socketName: String) { + val waitObj = hmWaitObjBySocketName.getValue(socketName) + + try { + waitObj.wait() + } catch (t: InterruptedException) { + // Not expecting interruptions + Debug.stackTrace(t) + } + } + + private fun initialiseSocketHashMaps() { + initialiaseHashMaps(XmlConstants.SOCKET_NAME_CHAT) + initialiaseHashMaps(XmlConstants.SOCKET_NAME_LOBBY) + initialiaseHashMaps(XmlConstants.SOCKET_NAME_GAME) + } + + private fun initialiaseHashMaps(socketType: String) { + hmWaitObjBySocketName[socketType] = Object() + hmNotificationQueueBySocketName[socketType] = ArrayList() + } + + fun getNotificationWaitObject(socketType: String): Any? { + return hmWaitObjBySocketName[socketType] + } +} diff --git a/server/src/main/kotlin/plugins/Routing.kt b/server/src/main/kotlin/plugins/Routing.kt index 648c283..8d29151 100644 --- a/server/src/main/kotlin/plugins/Routing.kt +++ b/server/src/main/kotlin/plugins/Routing.kt @@ -1,6 +1,6 @@ package plugins -import dto.ClientErrorResponse +import http.dto.ClientErrorResponse import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.logging.toLogString diff --git a/server/src/main/kotlin/routes/dev/DevController.kt b/server/src/main/kotlin/routes/dev/DevController.kt index 60484f6..10918bd 100644 --- a/server/src/main/kotlin/routes/dev/DevController.kt +++ b/server/src/main/kotlin/routes/dev/DevController.kt @@ -1,7 +1,8 @@ package routes.dev -import dto.DevCommandRequest import http.Routes +import http.dto.DevCommandRequest +import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.request.* import io.ktor.server.response.* @@ -16,7 +17,7 @@ object DevController { private suspend fun doDevCommand(call: ApplicationCall) { val request = call.receive() - val response = devService.processCommand(request.command) - call.respond(response) + devService.processCommand(request.command) + call.respond(HttpStatusCode.NoContent) } } diff --git a/server/src/main/kotlin/routes/health/HealthCheckService.kt b/server/src/main/kotlin/routes/health/HealthCheckService.kt index 1261cce..8e3e436 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 dto.HealthCheckResponse +import http.dto.HealthCheckResponse import util.OnlineConstants class HealthCheckService { diff --git a/server/src/main/kotlin/store/UserConnectionStore.kt b/server/src/main/kotlin/store/UserConnectionStore.kt index ae42754..f09f42a 100644 --- a/server/src/main/kotlin/store/UserConnectionStore.kt +++ b/server/src/main/kotlin/store/UserConnectionStore.kt @@ -1,6 +1,6 @@ package store -import `object`.UserConnection +import auth.UserConnection class MemoryUserConnectionStore : MemoryStore() { override val name = "user_connections" diff --git a/server/src/main/kotlin/util/Globals.kt b/server/src/main/kotlin/util/Globals.kt index fa4a21f..8478d85 100644 --- a/server/src/main/kotlin/util/Globals.kt +++ b/server/src/main/kotlin/util/Globals.kt @@ -1,8 +1,8 @@ package util +import auth.UserConnection import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.TimeUnit -import `object`.UserConnection import store.MemoryUserConnectionStore import store.Store diff --git a/server/src/test/kotlin/routes/dev/DevControllerTest.kt b/server/src/test/kotlin/routes/dev/DevControllerTest.kt new file mode 100644 index 0000000..947ec1f --- /dev/null +++ b/server/src/test/kotlin/routes/dev/DevControllerTest.kt @@ -0,0 +1,34 @@ +package routes.dev + +import http.Routes +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 org.junit.jupiter.api.Test +import testCore.AbstractTest + +class DevControllerTest : AbstractTest() { + @Test + fun `Should respond to a health check request`() = testApplication { + val response = client.post(Routes.DEV_COMMAND, ::buildRequest) + + response.status shouldBe HttpStatusCode.NoContent + response.bodyAsText() shouldBe "" + + verifyLog("threads") + } + + private fun buildRequest(builder: HttpRequestBuilder) { + builder.contentType(ContentType.Application.Json) + builder.setBody( + """ + { + "command": "${DevCommand.DUMP_THREADS.value}" + } + """ + .trimIndent() + ) + } +} diff --git a/server/src/test/kotlin/routes/dev/DevServiceTest.kt b/server/src/test/kotlin/routes/dev/DevServiceTest.kt new file mode 100644 index 0000000..5712231 --- /dev/null +++ b/server/src/test/kotlin/routes/dev/DevServiceTest.kt @@ -0,0 +1,27 @@ +package routes.dev + +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import routes.ClientException +import testCore.AbstractTest + +class DevServiceTest : AbstractTest() { + @Test + fun `throws a client exception if supplied with an invalid commmand`() { + val service = DevService() + + val ex = shouldThrow { service.processCommand("invalid") } + ex.errorCode shouldBe "invalid.command" + } + + @Test + fun `should not raise an error for any valid commands`() { + val service = DevService() + + DevCommand.entries.forEach { command -> + shouldNotThrowAny { service.processCommand(command.value) } + } + } +} diff --git a/server/src/test/kotlin/routes/health/HealthCheckControllerTest.kt b/server/src/test/kotlin/routes/health/HealthCheckControllerTest.kt index daffc30..e02dba9 100644 --- a/server/src/test/kotlin/routes/health/HealthCheckControllerTest.kt +++ b/server/src/test/kotlin/routes/health/HealthCheckControllerTest.kt @@ -1,5 +1,6 @@ package routes.health +import http.Routes import io.kotest.matchers.shouldBe import io.ktor.client.request.* import io.ktor.client.statement.* @@ -13,7 +14,7 @@ import util.OnlineConstants class HealthCheckControllerTest : AbstractTest() { @Test fun `Should respond to a health check request`() = testApplication { - val response = client.get("/health-check") + val response = client.get(Routes.DEV_COMMAND) response.status shouldBe HttpStatusCode.OK response.bodyAsText() shouldMatchJson """ diff --git a/server/src/test/kotlin/store/AbstractStoreTest.kt b/server/src/test/kotlin/store/AbstractStoreTest.kt new file mode 100644 index 0000000..505d2b7 --- /dev/null +++ b/server/src/test/kotlin/store/AbstractStoreTest.kt @@ -0,0 +1,60 @@ +package store + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test + +abstract class AbstractStoreTest { + abstract fun makeStore(): Store + + abstract fun makeItemA(): T + + abstract fun makeItemB(): T + + @Test + fun `PUT should overwrite existing value`() { + val store = makeStore() + + store.put("1", makeItemA()) + store.put("1", makeItemB()) + + store.get("1") shouldBe makeItemB() + } + + @Test + fun `FIND should return null for non-existent key`() { + makeStore().find("invalid") shouldBe null + } + + @Test + fun `GET should throw an error for non-existent key`() { + shouldThrow { makeStore().get("invalid") } + } + + @Test + fun `GET and FIND should retrieve the appropriate record`() { + val store = makeStore() + + store.put("A", makeItemA()) + store.put("B", makeItemB()) + + store.find("A") shouldBe makeItemA() + store.find("B") shouldBe makeItemB() + + store.get("A") shouldBe makeItemA() + store.get("B") shouldBe makeItemB() + } + + @Test + fun `Should be able to retrieve all items`() { + val store = makeStore() + store.getAll().shouldBeEmpty() + + store.put("A", makeItemA()) + store.put("B", makeItemB()) + + store.getAll().shouldContainExactlyInAnyOrder(makeItemA(), makeItemB()) + } +} diff --git a/server/src/test/kotlin/store/MemoryUserConnectionStoreTest.kt b/server/src/test/kotlin/store/MemoryUserConnectionStoreTest.kt new file mode 100644 index 0000000..fe728a5 --- /dev/null +++ b/server/src/test/kotlin/store/MemoryUserConnectionStoreTest.kt @@ -0,0 +1,17 @@ +package store + +import auth.UserConnection +import io.mockk.mockk +import javax.crypto.SecretKey + +class MemoryUserConnectionStoreTest : AbstractStoreTest() { + + private val secretKeyA = mockk() + private val secretKeyB = mockk() + + override fun makeStore() = MemoryUserConnectionStore() + + override fun makeItemA() = UserConnection("1.2.3.A", secretKeyA) + + override fun makeItemB() = UserConnection("1.2.3.B", secretKeyB) +}