API still in progress: first unit tests
This commit is contained in:
@@ -94,8 +94,23 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit</groupId>
|
||||
<artifactId>junit-bom</artifactId>
|
||||
<version>5.9.3</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- main dependencies -->
|
||||
<dependency>
|
||||
@@ -104,12 +119,6 @@
|
||||
<version>${kotlin.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>5.6.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
@@ -123,7 +132,7 @@
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-datetime-jvm</artifactId>
|
||||
<version>0.3.3</version>
|
||||
<version>0.4.0</version>
|
||||
</dependency>
|
||||
<!-- servlets and mail APIs -->
|
||||
<dependency>
|
||||
@@ -135,7 +144,7 @@
|
||||
<dependency>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
<artifactId>jakarta.mail</artifactId>
|
||||
<version>1.6.5</version>
|
||||
<version>1.6.7</version>
|
||||
</dependency>
|
||||
<!-- auth -->
|
||||
<dependency>
|
||||
@@ -147,7 +156,7 @@
|
||||
<dependency>
|
||||
<groupId>io.github.microutils</groupId>
|
||||
<artifactId>kotlin-logging-jvm</artifactId>
|
||||
<version>2.1.23</version>
|
||||
<version>3.0.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
@@ -231,6 +240,19 @@
|
||||
<artifactId>jeasse-servlet3</artifactId>
|
||||
<version>1.2</version>
|
||||
</dependency>
|
||||
<!-- tests -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito.kotlin</groupId>
|
||||
<artifactId>mockito-kotlin</artifactId>
|
||||
<version>4.1.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- test emails -->
|
||||
<dependency>
|
||||
<groupId>com.icegreen</groupId>
|
||||
|
@@ -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()
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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()
|
||||
|
@@ -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)
|
||||
|
||||
|
51
webapp/src/test/kotlin/BasicTests.kt
Normal file
51
webapp/src/test/kotlin/BasicTests.kt
Normal file
@@ -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")
|
||||
}
|
||||
}
|
83
webapp/src/test/kotlin/TestUtils.kt
Normal file
83
webapp/src/test/kotlin/TestUtils.kt
Normal file
@@ -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<String>()
|
||||
val subSelector = argumentCaptor<String>()
|
||||
val reqPayload = argumentCaptor<String>()
|
||||
val myReader = payload?.let { BufferedReader(StringReader(payload.toString())) }
|
||||
val req = mock<HttpServletRequest> {
|
||||
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<Int>()
|
||||
val errMessage = argumentCaptor<String>()
|
||||
val resp = mock<HttpServletResponse> {
|
||||
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() {
|
||||
|
||||
}
|
Reference in New Issue
Block a user