API still in progress: first unit tests

This commit is contained in:
Claude Brisson
2023-05-16 07:02:42 +02:00
parent 30a9bad259
commit e347183b56
9 changed files with 212 additions and 45 deletions

View File

@@ -41,6 +41,7 @@
<maven.surefire.plugin.version>3.0.0</maven.surefire.plugin.version>
<maven.war.plugin.version>3.3.2</maven.war.plugin.version>
<license.plugin.version>4.2</license.plugin.version>
<junit.jupiter.version>5.9.3</junit.jupiter.version>
<servlet.api.version>4.0.4</servlet.api.version>
<jetty.version>10.0.15</jetty.version>

View File

@@ -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>

View File

@@ -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()
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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()

View File

@@ -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)

View 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")
}
}

View 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() {
}