diff --git a/pom.xml b/pom.xml
index dcb85de..3d17b69 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,6 +41,7 @@
3.0.0
3.3.2
4.2
+ 5.9.3
4.0.4
10.0.15
diff --git a/webapp/pom.xml b/webapp/pom.xml
index e4aa80b..0508430 100644
--- a/webapp/pom.xml
+++ b/webapp/pom.xml
@@ -94,8 +94,23 @@
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+
+
+ org.junit
+ junit-bom
+ 5.9.3
+ pom
+ import
+
+
+
@@ -104,12 +119,6 @@
${kotlin.version}
test
-
- org.junit.jupiter
- junit-jupiter-engine
- 5.6.0
- test
-
org.jetbrains.kotlin
kotlin-stdlib-jdk8
@@ -123,7 +132,7 @@
org.jetbrains.kotlinx
kotlinx-datetime-jvm
- 0.3.3
+ 0.4.0
@@ -135,7 +144,7 @@
com.sun.mail
jakarta.mail
- 1.6.5
+ 1.6.7
@@ -147,7 +156,7 @@
io.github.microutils
kotlin-logging-jvm
- 2.1.23
+ 3.0.5
org.slf4j
@@ -231,6 +240,19 @@
jeasse-servlet3
1.2
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.jupiter.version}
+ test
+
+
+ org.mockito.kotlin
+ mockito-kotlin
+ 4.1.0
+ test
+
com.icegreen
diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt
index 17eb25e..b11395f 100644
--- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt
+++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt
@@ -42,13 +42,13 @@ interface ApiHandler {
}
fun getObjectPayload(request: HttpServletRequest): Json.Object {
- val json = request.getAttribute(PAYLOAD_KEY) as Json? ?: throw ApiException(HttpServletResponse.SC_BAD_REQUEST, "no payload")
+ val json = getPayload(request)
if (!json.isObject) badRequest("expecting a json object")
return json.asObject()
}
fun getArrayPayload(request: HttpServletRequest): Json.Array {
- val json = request.getAttribute(PAYLOAD_KEY) as Json? ?: throw ApiException(HttpServletResponse.SC_BAD_REQUEST, "no payload")
+ val json = getPayload(request)
if (!json.isArray) badRequest("expecting a json array")
return json.asArray()
}
diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt
index 5dc9d2d..f92370d 100644
--- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt
+++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt
@@ -21,7 +21,6 @@ object PlayerHandler: PairgothApiHandler {
val payload = getObjectPayload(request)
// player parsing (CB TODO - team handling, based on tournament type)
val player = Player.fromJson(payload)
- // CB TODO - handle concurrency
tournament.pairables[player.id] = player
// CB TODO - handle event broadcasting
return Json.Object("success" to true, "id" to player.id)
diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/TimeSystem.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/TimeSystem.kt
index 4508708..9d3e252 100644
--- a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/TimeSystem.kt
+++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/TimeSystem.kt
@@ -2,9 +2,10 @@ package org.jeudego.pairgoth.model
import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler
+import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
+import org.jeudego.pairgoth.model.TimeSystem.TimeSystemType.*
-
-sealed class TimeSystem(
+data class TimeSystem(
val type: TimeSystemType,
val mainTime: Int,
val increment: Int,
@@ -14,12 +15,12 @@ sealed class TimeSystem(
val stones: Int
) {
companion object {}
- enum class TimeSystemType { CANADIAN, STANDARD, FISHER, SUDDEN_DEATH }
+ enum class TimeSystemType { CANADIAN, STANDARD, FISCHER, SUDDEN_DEATH }
}
-class CanadianByoyomi(mainTime: Int, byoyomi: Int, stones: Int):
+fun CanadianByoyomi(mainTime: Int, byoyomi: Int, stones: Int) =
TimeSystem(
- type = TimeSystemType.CANADIAN,
+ type = CANADIAN,
mainTime = mainTime,
increment = 0,
byoyomi = byoyomi,
@@ -27,9 +28,9 @@ class CanadianByoyomi(mainTime: Int, byoyomi: Int, stones: Int):
stones = stones
)
-class StandardByoyomi(mainTime: Int, byoyomi: Int, periods: Int):
+fun StandardByoyomi(mainTime: Int, byoyomi: Int, periods: Int) =
TimeSystem(
- type = TimeSystemType.STANDARD,
+ type = STANDARD,
mainTime = mainTime,
increment = 0,
byoyomi = byoyomi,
@@ -37,9 +38,9 @@ class StandardByoyomi(mainTime: Int, byoyomi: Int, periods: Int):
stones = 1
)
-class FisherTime(mainTime: Int, increment: Int, maxTime: Int = Int.MAX_VALUE):
+fun FischerTime(mainTime: Int, increment: Int, maxTime: Int = Int.MAX_VALUE) =
TimeSystem(
- type = TimeSystemType.FISHER,
+ type = FISCHER,
mainTime = mainTime,
increment = increment,
maxTime = maxTime,
@@ -48,9 +49,9 @@ class FisherTime(mainTime: Int, increment: Int, maxTime: Int = Int.MAX_VALUE):
stones = 0
)
-class SuddenDeath(mainTime: Int):
+fun SuddenDeath(mainTime: Int) =
TimeSystem(
- type = TimeSystemType.SUDDEN_DEATH,
+ type = SUDDEN_DEATH,
mainTime = mainTime,
increment = 0,
byoyomi = 0,
@@ -61,32 +62,33 @@ class SuddenDeath(mainTime: Int):
// Serialization
fun TimeSystem.Companion.fromJson(json: Json.Object) =
- when (json.getString("type")?.uppercase() ?: ApiHandler.badRequest("missing timeSystem type")) {
+ when (json.getString("type")?.uppercase() ?: badRequest("missing timeSystem type")) {
"CANADIAN" -> CanadianByoyomi(
- mainTime = json.getInt("mainTime") ?: ApiHandler.badRequest("missing timeSystem mainTime"),
- byoyomi = json.getInt("byoyomi") ?: ApiHandler.badRequest("missing timeSystem byoyomi"),
- stones = json.getInt("stones") ?: ApiHandler.badRequest("missing timeSystem stones")
+ mainTime = json.getInt("mainTime") ?: badRequest("missing timeSystem mainTime"),
+ byoyomi = json.getInt("byoyomi") ?: badRequest("missing timeSystem byoyomi"),
+ stones = json.getInt("stones") ?: badRequest("missing timeSystem stones")
)
"STANDARD" -> StandardByoyomi(
- mainTime = json.getInt("mainTime") ?: ApiHandler.badRequest("missing timeSystem mainTime"),
- byoyomi = json.getInt("byoyomi") ?: ApiHandler.badRequest("missing timeSystem byoyomi"),
- periods = json.getInt("periods") ?: ApiHandler.badRequest("missing timeSystem periods")
+ mainTime = json.getInt("mainTime") ?: badRequest("missing timeSystem mainTime"),
+ byoyomi = json.getInt("byoyomi") ?: badRequest("missing timeSystem byoyomi"),
+ periods = json.getInt("periods") ?: badRequest("missing timeSystem periods")
)
- "FISHER" -> FisherTime(
- mainTime = json.getInt("mainTime") ?: ApiHandler.badRequest("missing timeSystem mainTime"),
- increment = json.getInt("increment") ?: ApiHandler.badRequest("missing timeSystem increment"),
- maxTime = json.getInt("increment") ?: Integer.MAX_VALUE
+ "FISCHER" -> FischerTime(
+ mainTime = json.getInt("mainTime") ?: badRequest("missing timeSystem mainTime"),
+ increment = json.getInt("increment") ?: badRequest("missing timeSystem increment"),
+ maxTime = json.getInt("maxTime") ?: Integer.MAX_VALUE
)
"SUDDEN_DEATH" -> SuddenDeath(
- mainTime = json.getInt("mainTime") ?: ApiHandler.badRequest("missing timeSystem mainTime"),
+ mainTime = json.getInt("mainTime") ?: badRequest("missing timeSystem mainTime"),
)
- else -> ApiHandler.badRequest("invalid or missing timeSystem type")
+ else -> badRequest("invalid or missing timeSystem type")
}
fun TimeSystem.toJson() = when (type) {
- TimeSystem.TimeSystemType.CANADIAN -> Json.Object("mainTime" to mainTime, "byoyomi" to byoyomi, "stones" to stones)
- TimeSystem.TimeSystemType.STANDARD -> Json.Object("mainTime" to mainTime, "byoyomi" to byoyomi, "periods" to periods)
- TimeSystem.TimeSystemType.FISHER -> Json.Object("mainTime" to mainTime, "increment" to increment, "maxTime" to maxTime)
- TimeSystem.TimeSystemType.SUDDEN_DEATH -> Json.Object("mainTime" to mainTime)
+ TimeSystem.TimeSystemType.CANADIAN -> Json.Object("type" to type.name, "mainTime" to mainTime, "byoyomi" to byoyomi, "stones" to stones)
+ TimeSystem.TimeSystemType.STANDARD -> Json.Object("type" to type.name, "mainTime" to mainTime, "byoyomi" to byoyomi, "periods" to periods)
+ TimeSystem.TimeSystemType.FISCHER ->
+ if (maxTime == Int.MAX_VALUE) Json.Object("type" to type.name, "mainTime" to mainTime, "increment" to increment)
+ else Json.Object("type" to type.name, "mainTime" to mainTime, "increment" to increment, "maxTime" to maxTime)
+ TimeSystem.TimeSystemType.SUDDEN_DEATH -> Json.Object("type" to type.name, "mainTime" to mainTime)
}
-
diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt
index 826a613..846c4c2 100644
--- a/webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt
+++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt
@@ -5,10 +5,6 @@ import org.jeudego.pairgoth.model.Tournament
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.E
-// CB TODO - handle concurrency:
-// - either with concurrent maps
-// - or with a thread isolation (better, covers more operations)
-
object Store {
private val _nextTournamentId = AtomicInteger()
private val _nextPlayerId = AtomicInteger()
diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt
index 64e0063..31ae69a 100644
--- a/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt
+++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt
@@ -12,6 +12,8 @@ import org.jeudego.pairgoth.util.toString
import org.slf4j.LoggerFactory
import java.io.IOException
import java.util.*
+import java.util.concurrent.locks.ReadWriteLock
+import java.util.concurrent.locks.ReentrantReadWriteLock
import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@@ -34,7 +36,18 @@ class ApiServlet : HttpServlet() {
doRequest(request, response)
}
+ private val lock: ReadWriteLock = ReentrantReadWriteLock()
private fun doRequest(request: HttpServletRequest, response: HttpServletResponse) {
+ val requestLock = if (request.method == "GET") lock.readLock() else lock.writeLock()
+ try {
+ requestLock.lock()
+ doProtectedRequest(request, response)
+ } finally {
+ requestLock.unlock()
+ }
+ }
+
+ private fun doProtectedRequest(request: HttpServletRequest, response: HttpServletResponse) {
val uri = request.requestURI
logger.logRequest(request, !uri.contains(".") && uri.length > 1)
diff --git a/webapp/src/test/kotlin/BasicTests.kt b/webapp/src/test/kotlin/BasicTests.kt
new file mode 100644
index 0000000..e10e0a9
--- /dev/null
+++ b/webapp/src/test/kotlin/BasicTests.kt
@@ -0,0 +1,51 @@
+package org.jeudego.pairgoth.test
+
+import com.republicate.kson.Json
+import org.junit.jupiter.api.MethodOrderer.Alphanumeric
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import org.junit.jupiter.api.TestMethodOrder
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+
+@TestMethodOrder(Alphanumeric::class)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class BasicTests() {
+
+ val aTournament = Json.Object(
+ "type" to "INDIVIDUAL",
+ "name" to "Mon Tournoi",
+ "shortName" to "mon-tournoi",
+ "startDate" to "2023-05-10",
+ "endDate" to "2023-05-12",
+ "country" to "FR",
+ "location" to "Marseille",
+ "online" to false,
+ "timeSystem" to Json.Object(
+ "type" to "FISCHER",
+ "mainTime" to 1200,
+ "increment" to 10
+ ),
+ "pairing" to Json.Object(
+ "type" to "ROUNDROBIN"
+ )
+ )
+
+ @Test
+ fun `001 create tournament`() {
+ val resp = TestAPI.post("/api/tour", aTournament)
+ assertTrue(resp.isObject, "Json object expected")
+ assertTrue(resp.asObject().getBoolean("success") == true, "expecting success")
+ }
+
+ @Test
+ fun `002 get tournament`() {
+ val resp = TestAPI.get("/api/tour/1")
+ assertTrue(resp.isObject, "Json object expected")
+ assertEquals(1, resp.asObject().getInt("id"), "First tournament should have id #1")
+ // filter out "id", and also "komi", "rules" and "gobanSize" which were provided by default
+ val cmp = Json.Object(*resp.asObject().entries.filter { it.key !in listOf("id", "komi", "rules", "gobanSize") }.map { Pair(it.key, it.value) }.toTypedArray())
+ assertEquals(aTournament.toString(), cmp.toString(), "tournament differs")
+ }
+}
diff --git a/webapp/src/test/kotlin/TestUtils.kt b/webapp/src/test/kotlin/TestUtils.kt
new file mode 100644
index 0000000..4436b5e
--- /dev/null
+++ b/webapp/src/test/kotlin/TestUtils.kt
@@ -0,0 +1,83 @@
+package org.jeudego.pairgoth.test
+
+import com.republicate.kson.Json
+import org.jeudego.pairgoth.api.ApiHandler
+import org.jeudego.pairgoth.web.ApiServlet
+import org.jeudego.pairgoth.web.WebappManager
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import java.io.BufferedReader
+import java.io.PrintWriter
+import java.io.StringReader
+import java.io.StringWriter
+import java.util.*
+import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.HttpServletResponse
+
+object TestAPI {
+
+ fun Any?.toUnit() = Unit
+
+ private val apiServlet = ApiServlet()
+
+ private fun testRequest(reqMethod: String, uri: String, payload: Json? = null): Json {
+
+ WebappManager.properties["webapp.env"] = "test"
+
+ // mock request
+ val myHeaderNames = if (reqMethod == "GET") emptyList() else listOf("Content-Type")
+ val selector = argumentCaptor()
+ val subSelector = argumentCaptor()
+ val reqPayload = argumentCaptor()
+ val myReader = payload?.let { BufferedReader(StringReader(payload.toString())) }
+ val req = mock {
+ on { method } doReturn reqMethod
+ on { requestURI } doReturn uri
+ on { setAttribute(eq(ApiHandler.SELECTOR_KEY), selector.capture()) } doAnswer {}
+ on { setAttribute(eq(ApiHandler.SUBSELECTOR_KEY), subSelector.capture()) } doAnswer {}
+ on { setAttribute(eq(ApiHandler.PAYLOAD_KEY), reqPayload.capture()) } doAnswer {}
+ on { getAttribute(ApiHandler.SELECTOR_KEY) } doAnswer { selector.lastValue }
+ on { getAttribute(ApiHandler.SUBSELECTOR_KEY) } doAnswer { subSelector.lastValue }
+ on { getAttribute(ApiHandler.PAYLOAD_KEY) } doAnswer { reqPayload.lastValue }
+ on { reader } doReturn myReader
+ on { scheme } doReturn "http"
+ on { localName } doReturn "pairgoth"
+ on { localPort } doReturn 80
+ on { contextPath } doReturn ""
+ on { contentType } doReturn if (reqMethod == "GET") null else "application/json; charset=UTF-8"
+ on { headerNames } doReturn Collections.enumeration(myHeaderNames)
+ on { getHeader(eq("Accept")) } doReturn "application/json"
+ }
+
+ // mock response
+ val buffer = StringWriter()
+ val errCode = argumentCaptor()
+ val errMessage = argumentCaptor()
+ val resp = mock {
+ on { writer } doAnswer { PrintWriter(buffer) }
+ on { sendError(errCode.capture(), errMessage.capture()) } doAnswer { throw Error("${errCode.lastValue} ${errMessage.lastValue}") }
+ }
+
+ when (reqMethod) {
+ "GET" -> apiServlet.doGet(req, resp)
+ "POST" -> apiServlet.doPost(req, resp)
+ "PUT" -> apiServlet.doPut(req, resp)
+ "DELETE" -> apiServlet.doDelete(req, resp)
+ }
+
+ return Json.parse(buffer.toString()) ?: throw Error("no response payload")
+ }
+
+ fun get(uri: String) = testRequest("GET", uri)
+ fun post(uri: String, payload: Json) = testRequest("POST", uri, payload)
+ fun put(uri: String, payload: Json) = testRequest("PUT", uri, payload)
+ fun delete(uri: String, payload: Json) = testRequest("DELETE", uri, payload)
+}
+
+fun expectSuccess() {
+
+}
\ No newline at end of file