test(api/rest): add captcha tests
This commit is contained in:
parent
46218aa3c7
commit
053b24a614
2 changed files with 68 additions and 3 deletions
|
|
@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -15,11 +15,19 @@ import io.ktor.http.HttpHeaders
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.http.content.TextContent
|
import io.ktor.http.content.TextContent
|
||||||
import io.ktor.http.headersOf
|
import io.ktor.http.headersOf
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonNamingStrategy
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
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
|
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")
|
private val JsonHeader = headersOf(HttpHeaders.ContentType, "application/json")
|
||||||
|
|
||||||
val idArb = Arb.long(1e18.toLong(), 1e20.toLong())
|
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 {
|
class DiscordApiMock {
|
||||||
private val fingerprints = mutableListOf<String>()
|
private val fingerprints = mutableListOf<String>()
|
||||||
private val logins = mutableMapOf<String, String>()
|
private val logins = mutableMapOf<String, String>()
|
||||||
|
private var captcha: Pair<CaptchaRequest, CaptchaResponse.Success> = generateCaptcha()
|
||||||
|
|
||||||
|
var isCaptchaEnabled = false
|
||||||
|
|
||||||
fun createLogin(email: String, password: String) {
|
fun createLogin(email: String, password: String) {
|
||||||
logins[email] = password
|
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 ->
|
val engine = MockEngine { req ->
|
||||||
if (!req.url.toString().startsWith("https://discord.com/api/v9")) {
|
if (!req.url.toString().startsWith("https://discord.com/api/v9")) {
|
||||||
return@MockEngine respondError(HttpStatusCode.NotFound)
|
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", "")
|
val path = req.url.encodedPath.replaceFirst("/api/v9", "")
|
||||||
return@MockEngine when (path) {
|
return@MockEngine when (path) {
|
||||||
"/experiments" -> {
|
"/experiments" -> {
|
||||||
val fp = Arb.string(18..20, "123456789").single()
|
val fp = Arb.string(18..20, "123456789").single()
|
||||||
fingerprints.add(fp)
|
fingerprints.add(fp)
|
||||||
respond(AuthResponse.Experiments(fp), headers = JsonHeader)
|
respondJson(AuthResponse.Experiments(fp))
|
||||||
}
|
}
|
||||||
"/auth/login" -> {
|
"/auth/login" -> {
|
||||||
val body = req.body as? TextContent
|
val body = req.body as? TextContent
|
||||||
|
|
@ -58,13 +89,17 @@ class DiscordApiMock {
|
||||||
return@MockEngine badReq("Unknown credentials")
|
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)
|
else -> respondError(HttpStatusCode.NotFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
@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")
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue