API still in progress: first unit tests
This commit is contained in:
@@ -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