test(api/rest): add captcha tests

This commit is contained in:
Cilly Leang 2026-02-16 22:32:43 +11:00
parent 46218aa3c7
commit 053b24a614
Signed by: cilly
GPG key ID: 6500251E087653C9
2 changed files with 68 additions and 3 deletions

View file

@ -0,0 +1,30 @@
package moe.lava.neon.tests.api
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import moe.lava.neon.api.ApiClient
import moe.lava.neon.api.endpoints.getExperiments
import moe.lava.neon.common.captcha.CaptchaResponse
class CaptchaTest : FunSpec({
val mock = DiscordApiMock()
val client = ApiClient(mock.engine, false)
val (captchaReq, captchaRes) = mock.generateCaptcha()
mock.isCaptchaEnabled = true
test("captcha should not be handled") {
val res = client.getExperiments().response
res.status.value shouldBe 400
}
test("captcha should be handled") {
client.setCaptchaHandler { req ->
captchaRes
.takeIf { req == captchaReq }
?: CaptchaResponse.Failed(Throwable())
}
val res = client.getExperiments().response
res.status.value shouldBe 200
}
})

View file

@ -15,11 +15,19 @@ import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.TextContent
import io.ktor.http.headersOf
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import moe.lava.neon.common.captcha.CaptchaRequest
import moe.lava.neon.common.captcha.CaptchaResponse
import moe.lava.neon.tests.api.mock.AuthResponse
@OptIn(ExperimentalSerializationApi::class)
private val JsonWithSnakecase = Json {
namingStrategy = JsonNamingStrategy.SnakeCase
}
private val JsonHeader = headersOf(HttpHeaders.ContentType, "application/json")
val idArb = Arb.long(1e18.toLong(), 1e20.toLong())
@ -29,21 +37,44 @@ val tokenArb = Arb.stringPattern("(mfa\\.[a-zA-Z0-9_-]{20,})|([a-zA-Z0-9_-]{23,2
class DiscordApiMock {
private val fingerprints = mutableListOf<String>()
private val logins = mutableMapOf<String, String>()
private var captcha: Pair<CaptchaRequest, CaptchaResponse.Success> = generateCaptcha()
var isCaptchaEnabled = false
fun createLogin(email: String, password: String) {
logins[email] = password
}
fun generateCaptcha(): Pair<CaptchaRequest, CaptchaResponse.Success> {
val req = CaptchaRequest(
listOf(Arb.string().single()),
Arb.string().single(),
Arb.string().single(),
Arb.string().single(),
Arb.string().single(),
Arb.string().single(),
true,
)
val res = CaptchaResponse.Success(Arb.string().single())
captcha = req to res
return req to res
}
val engine = MockEngine { req ->
if (!req.url.toString().startsWith("https://discord.com/api/v9")) {
return@MockEngine respondError(HttpStatusCode.NotFound)
}
if (isCaptchaEnabled) {
if (req.headers["X-Captcha-Key"] != captcha.second.token) {
return@MockEngine respondJson(JsonWithSnakecase.encodeToString(captcha.first), HttpStatusCode.BadRequest)
}
}
val path = req.url.encodedPath.replaceFirst("/api/v9", "")
return@MockEngine when (path) {
"/experiments" -> {
val fp = Arb.string(18..20, "123456789").single()
fingerprints.add(fp)
respond(AuthResponse.Experiments(fp), headers = JsonHeader)
respondJson(AuthResponse.Experiments(fp))
}
"/auth/login" -> {
val body = req.body as? TextContent
@ -58,13 +89,17 @@ class DiscordApiMock {
return@MockEngine badReq("Unknown credentials")
}
respond(AuthResponse.Login(idArb.next(), tokenArb.next()), headers = JsonHeader)
respondJson(AuthResponse.Login(idArb.next(), tokenArb.next()))
}
else -> respondError(HttpStatusCode.NotFound)
}
}
@Suppress("NOTHING_TO_INLINE")
inline fun MockRequestHandleScope.badReq(msg: String): HttpResponseData =
private inline fun MockRequestHandleScope.badReq(msg: String): HttpResponseData =
respondError(HttpStatusCode.BadRequest, content = "[Neon] $msg")
@Suppress("NOTHING_TO_INLINE")
private inline fun MockRequestHandleScope.respondJson(content: String, status: HttpStatusCode = HttpStatusCode.OK): HttpResponseData =
respond(content = content, status = status, headers = JsonHeader)
}