test(api/rest): add basic endpoint tests
This commit is contained in:
parent
c7fb2817fc
commit
46218aa3c7
9 changed files with 253 additions and 8 deletions
|
|
@ -2,8 +2,10 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.androidMultiplatformLibrary)
|
alias(libs.plugins.androidMultiplatformLibrary)
|
||||||
|
alias(libs.plugins.kotest)
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
|
alias(libs.plugins.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
|
@ -30,12 +32,28 @@ kotlin {
|
||||||
}
|
}
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
implementation(libs.kotlin.test)
|
implementation(libs.kotlin.test)
|
||||||
|
implementation(libs.kotest.assertions)
|
||||||
|
implementation(libs.kotest.framework)
|
||||||
|
implementation(libs.kotest.property)
|
||||||
|
implementation(libs.ktor.client.mock)
|
||||||
}
|
}
|
||||||
jvmMain.dependencies {
|
jvmMain.dependencies {
|
||||||
implementation(libs.ktor.client.okhttp)
|
implementation(libs.ktor.client.okhttp)
|
||||||
}
|
}
|
||||||
|
jvmTest.dependencies {
|
||||||
|
implementation(libs.kotest.runner.junit5)
|
||||||
|
}
|
||||||
androidMain.dependencies {
|
androidMain.dependencies {
|
||||||
implementation(libs.ktor.client.okhttp)
|
implementation(libs.ktor.client.okhttp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.named<Test>("jvmTest") {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
//tasks.withType<Test>().configureEach {
|
||||||
|
// logger.lifecycle("UP-TO-DATE check for $name is disabled, forcing it to run.")
|
||||||
|
// outputs.upToDateWhen { false }
|
||||||
|
//}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package moe.lava.neon.api
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.engine.HttpClientEngine
|
||||||
import io.ktor.client.plugins.HttpSend
|
import io.ktor.client.plugins.HttpSend
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.client.plugins.cookies.HttpCookies
|
import io.ktor.client.plugins.cookies.HttpCookies
|
||||||
|
|
@ -52,6 +53,7 @@ class ApiClient internal constructor(
|
||||||
plugin(HttpSend).intercept { req ->
|
plugin(HttpSend).intercept { req ->
|
||||||
logger.d { "Intercepting ${req.url.buildString()}" }
|
logger.d { "Intercepting ${req.url.buildString()}" }
|
||||||
val call = execute(req)
|
val call = execute(req)
|
||||||
|
logger.d { "recv ${call.response.bodyAsText()}" }
|
||||||
if (call.response.status.value != 400) return@intercept call
|
if (call.response.status.value != 400) return@intercept call
|
||||||
logger.d { "Found 400 response: ${call.response.bodyAsText()}" }
|
logger.d { "Found 400 response: ${call.response.bodyAsText()}" }
|
||||||
val captchaRequest = runCatching { call.response.body<CaptchaRequest>() }
|
val captchaRequest = runCatching { call.response.body<CaptchaRequest>() }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package moe.lava.neon.tests.api
|
||||||
|
|
||||||
|
import io.kotest.property.Arb
|
||||||
|
import io.kotest.property.arbitrary.long
|
||||||
|
import io.kotest.property.arbitrary.next
|
||||||
|
import io.kotest.property.arbitrary.single
|
||||||
|
import io.kotest.property.arbitrary.string
|
||||||
|
import io.kotest.property.arbitrary.stringPattern
|
||||||
|
import io.ktor.client.engine.mock.MockEngine
|
||||||
|
import io.ktor.client.engine.mock.MockRequestHandleScope
|
||||||
|
import io.ktor.client.engine.mock.respond
|
||||||
|
import io.ktor.client.engine.mock.respondError
|
||||||
|
import io.ktor.client.request.HttpResponseData
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.http.content.TextContent
|
||||||
|
import io.ktor.http.headersOf
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import moe.lava.neon.tests.api.mock.AuthResponse
|
||||||
|
|
||||||
|
private val JsonHeader = headersOf(HttpHeaders.ContentType, "application/json")
|
||||||
|
|
||||||
|
val idArb = Arb.long(1e18.toLong(), 1e20.toLong())
|
||||||
|
// Good enough
|
||||||
|
val tokenArb = Arb.stringPattern("(mfa\\.[a-zA-Z0-9_-]{20,})|([a-zA-Z0-9_-]{23,28}\\.[a-zA-Z0-9_-]{6,7}\\.[a-zA-Z0-9_-]{38})")
|
||||||
|
|
||||||
|
class DiscordApiMock {
|
||||||
|
private val fingerprints = mutableListOf<String>()
|
||||||
|
private val logins = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
fun createLogin(email: String, password: String) {
|
||||||
|
logins[email] = password
|
||||||
|
}
|
||||||
|
|
||||||
|
val engine = MockEngine { req ->
|
||||||
|
if (!req.url.toString().startsWith("https://discord.com/api/v9")) {
|
||||||
|
return@MockEngine respondError(HttpStatusCode.NotFound)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
"/auth/login" -> {
|
||||||
|
val body = req.body as? TextContent
|
||||||
|
?: return@MockEngine badReq("No body")
|
||||||
|
val json = Json.parseToJsonElement(body.text).jsonObject
|
||||||
|
val login = json["login"]?.jsonPrimitive?.content
|
||||||
|
?: return@MockEngine badReq("No login")
|
||||||
|
val password = json["password"]?.jsonPrimitive?.content
|
||||||
|
?: return@MockEngine badReq("No password")
|
||||||
|
|
||||||
|
if (logins[login] != password) {
|
||||||
|
return@MockEngine badReq("Unknown credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(AuthResponse.Login(idArb.next(), tokenArb.next()), headers = JsonHeader)
|
||||||
|
}
|
||||||
|
else -> respondError(HttpStatusCode.NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
inline fun MockRequestHandleScope.badReq(msg: String): HttpResponseData =
|
||||||
|
respondError(HttpStatusCode.BadRequest, content = "[Neon] $msg")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
package moe.lava.neon.tests.api
|
||||||
|
|
||||||
|
import io.kotest.assertions.throwables.shouldNotThrowAny
|
||||||
|
import io.kotest.assertions.withClue
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.core.spec.style.funSpec
|
||||||
|
import io.kotest.core.spec.style.scopes.FunSpecContainerScope
|
||||||
|
import io.kotest.matchers.booleans.shouldBeTrue
|
||||||
|
import io.kotest.matchers.collections.shouldBeOneOf
|
||||||
|
import io.kotest.matchers.collections.shouldContainAllInAnyOrder
|
||||||
|
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.ktor.client.statement.request
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import moe.lava.neon.api.ApiResponse
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
|
|
||||||
|
fun <T> endpointTestFactory(
|
||||||
|
endpoint: String,
|
||||||
|
testName: String?,
|
||||||
|
response: suspend () -> ApiResponse<T>,
|
||||||
|
also: suspend FunSpecContainerScope.(response: ApiResponse<T>) -> Unit,
|
||||||
|
) = funSpec {
|
||||||
|
context(testName ?: endpoint) {
|
||||||
|
val res = response()
|
||||||
|
val req = res.response.request
|
||||||
|
|
||||||
|
test("has correct base url") {
|
||||||
|
req.url.toString().startsWith("https://discord.com/api/v9/") shouldBe true
|
||||||
|
}
|
||||||
|
test("uses correct endpoint") {
|
||||||
|
req.url.encodedPath.replace("/api/v9/", "") shouldBe endpoint
|
||||||
|
}
|
||||||
|
test("has valid super props") {
|
||||||
|
val props = req.headers["X-Super-Properties"]
|
||||||
|
props.shouldNotBeNull()
|
||||||
|
val decoded = withClue("should be decodable") {
|
||||||
|
shouldNotThrowAny {
|
||||||
|
Base64.decode(props).decodeToString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val parsed = withClue("should be parsable") {
|
||||||
|
shouldNotThrowAny {
|
||||||
|
Json.parseToJsonElement(decoded).jsonObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withClue("has props") {
|
||||||
|
parsed.keys shouldContainAllInAnyOrder setOf(
|
||||||
|
"os",
|
||||||
|
"browser",
|
||||||
|
"browser_user_agent",
|
||||||
|
"browser_version",
|
||||||
|
"client_build_number",
|
||||||
|
"release_channel",
|
||||||
|
"system_locale",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val userAgent = withClue("has valid user agent") {
|
||||||
|
val agent = parsed["browser_user_agent"]?.jsonPrimitive
|
||||||
|
agent?.isString.shouldBeTrue()
|
||||||
|
agent.content
|
||||||
|
}
|
||||||
|
withClue("has matching user agent") {
|
||||||
|
userAgent shouldBeOneOf setOf(req.headers["User-Agent"], "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test("has correct body") {
|
||||||
|
shouldNotThrowAny { res.body() }
|
||||||
|
}
|
||||||
|
also(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> FunSpec.withFactory(
|
||||||
|
testName: String? = null,
|
||||||
|
endpoint: String,
|
||||||
|
response: suspend () -> ApiResponse<T>,
|
||||||
|
also: suspend FunSpecContainerScope.(response: ApiResponse<T>) -> Unit = {},
|
||||||
|
) {
|
||||||
|
include(endpointTestFactory(endpoint, testName, response, also))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package moe.lava.neon.tests.api.endpoints
|
||||||
|
|
||||||
|
import io.kotest.assertions.throwables.shouldNotThrowAny
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.ktor.client.statement.request
|
||||||
|
import io.ktor.http.content.TextContent
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import moe.lava.neon.api.ApiClient
|
||||||
|
import moe.lava.neon.api.endpoints.getExperiments
|
||||||
|
import moe.lava.neon.api.endpoints.login
|
||||||
|
import moe.lava.neon.tests.api.DiscordApiMock
|
||||||
|
import moe.lava.neon.tests.api.withFactory
|
||||||
|
|
||||||
|
class AuthTest : FunSpec({
|
||||||
|
val mock = DiscordApiMock()
|
||||||
|
val client = ApiClient(mock.engine, false)
|
||||||
|
|
||||||
|
var fp: String? = null
|
||||||
|
withFactory(
|
||||||
|
testName = "get experiments",
|
||||||
|
endpoint = "experiments",
|
||||||
|
response = client::getExperiments
|
||||||
|
) { res ->
|
||||||
|
val body = res.body()
|
||||||
|
fp = body.fingerprint
|
||||||
|
}
|
||||||
|
|
||||||
|
val email = "hello@example.com"
|
||||||
|
val password = "supersecurepassword"
|
||||||
|
mock.createLogin(email, password)
|
||||||
|
withFactory(
|
||||||
|
testName = "login with real creds",
|
||||||
|
endpoint = "auth/login",
|
||||||
|
response = { client.login(email, password, fp!!) }
|
||||||
|
) { res ->
|
||||||
|
val req = res.response.request
|
||||||
|
val headers = req.headers
|
||||||
|
test("has correct fingerprint") {
|
||||||
|
headers["X-Fingerprint"] shouldBe fp
|
||||||
|
}
|
||||||
|
context("has correct body") {
|
||||||
|
val body = shouldNotThrowAny { Json.parseToJsonElement((req.content as TextContent).text).jsonObject }
|
||||||
|
test("has correct login") {
|
||||||
|
body["login"]?.jsonPrimitive?.content shouldBe email
|
||||||
|
}
|
||||||
|
test("has correct password") {
|
||||||
|
body["password"]?.jsonPrimitive?.content shouldBe password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package moe.lava.neon.tests.api.mock
|
||||||
|
|
||||||
|
object AuthResponse {
|
||||||
|
fun Experiments(fp: String) = """{"fingerprint":"$fp","assignments":[[3643362751,0,1,-1,0,4342,0,0,null,null,null],[1428438599,0,1,-1,2,3832,0,0,null,null,null],[1567199723,0,1,-1,1,1775,0,0,null,null,null],[4179344527,3,1,-1,0,3584,0,0,null,null,null],[1814483290,0,1,-1,0,6281,0,0,null,null,null],[4221006726,0,1,-1,0,4318,0,0,null,null,null],[1398673921,1,1,-1,0,4649,0,0,null,null,null],[1034661306,0,1,-1,0,1792,0,0,null,null,null],[3124003316,1,1,-1,0,1427,0,0,null,null,null],[2676348506,0,1,-1,0,4412,0,0,null,null,null],[4136574802,0,1,-1,1,4337,0,0,null,null,null],[4049571159,0,1,-1,0,504,0,0,null,null,null],[2539540256,0,1,-1,3,25,0,0,null,null,null],[1549543958,2,1,-1,0,6992,0,0,null,null,null],[1333727,0,1,-1,0,4978,0,0,null,null,null],[3029387945,1,1,-1,0,8670,0,0,null,null,null],[738080167,0,1,-1,1,9441,0,0,null,null,null],[3283745071,0,1,-1,1,9135,0,0,null,null,null],[373531156,0,3,-1,0,7954,0,0,null,null,null],[1617749743,0,1,-1,0,8281,0,0,null,null,null],[288968706,0,1,-1,3,7867,0,0,null,null,null],[2091202574,0,1,-1,0,8443,0,0,null,null,null],[4265918989,0,1,-1,0,2580,0,0,null,null,null],[1083932689,0,1,-1,0,6167,0,0,null,null,null],[1884426471,0,1,-1,0,5258,0,0,null,null,null],[2180379513,0,1,-1,4,5727,0,0,null,null,null],[759064140,0,1,-1,0,6050,0,0,null,null,null],[1680860120,0,1,-1,1,6633,0,0,null,null,null],[151550492,0,1,-1,2,102,0,1,null,null,null],[2054293512,0,0,-1,0,193,0,1,null,null,null],[2848826960,0,1,-1,0,7093,0,0,null,null,null],[3775594731,3,1,-1,0,1418,0,0,null,null,null],[878040044,1,1,-1,0,6738,0,1,null,null,null],[2990331215,0,1,-1,0,2786,0,0,null,null,null],[996399186,2,1,-1,0,5255,0,0,null,null,null],[3173338335,0,0,-1,0,178,0,1,null,null,null],[640084831,3,1,-1,0,5284,0,0,null,null,null],[1112953678,0,1,-1,0,3640,0,0,null,null,null],[4285324985,0,1,-1,0,8618,0,0,null,null,null],[4206392105,4,1,-1,0,5758,0,0,null,null,null],[1714347921,0,1,-1,0,2255,0,0,null,null,null],[3936291300,3,1,-1,0,3408,0,0,null,null,null],[2660711063,0,1,-1,0,8317,0,0,null,null,null],[1644303758,0,1,-1,0,2365,0,1,null,null,null],[114771571,0,1,-1,0,796,0,0,null,null,null],[437074334,5,1,-1,2,2482,0,1,null,null,null],[1978990512,3,1,-1,0,9101,0,0,null,null,null],[3378028029,3,2,-1,0,1997,0,1,null,null,null],[1046173986,0,1,-1,0,9264,0,0,null,null,null],[1757800499,1,1,-1,0,9654,0,0,null,null,null],[2849514387,0,1,-1,0,275,0,1,null,null,null],[2613104049,0,1,-1,0,8339,0,1,null,null,null],[2482010813,0,1,-1,0,4372,0,1,null,null,null],[1778984745,0,1,-1,0,1685,0,1,null,null,null],[2870923171,0,1,-1,0,101,0,0,null,null,null],[1598219105,1,2,-1,0,3159,0,1,null,null,null],[641666131,1,1,-1,0,2501,0,0,null,null,null]]}"""
|
||||||
|
fun Login(userId: Long, token: String) = """{"user_id":"$userId","token":"$token","user_settings":{"locale":"en-US","theme":"dark"}}"""
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,14 @@ internal data class PlatformProps(
|
||||||
internal expect val platformSuperProps: PlatformProps
|
internal expect val platformSuperProps: PlatformProps
|
||||||
|
|
||||||
object ApiConstants {
|
object ApiConstants {
|
||||||
val superProps = Base64.encode(Json.encodeToString(SuperProperties()).encodeToByteArray())
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
val json = Json {
|
||||||
|
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
encodeDefaults = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val superProps = Base64.encode(json.encodeToString(SuperProperties()).encodeToByteArray())
|
||||||
val baseHeaders = mapOf(
|
val baseHeaders = mapOf(
|
||||||
"X-Debug-Options" to "bugReporterEnabled",
|
"X-Debug-Options" to "bugReporterEnabled",
|
||||||
"X-Discord-Locale" to "en-US",
|
"X-Discord-Locale" to "en-US",
|
||||||
|
|
@ -34,13 +41,6 @@ object ApiConstants {
|
||||||
const val userAgent = "Discord-Android/311020;RNA"
|
const val userAgent = "Discord-Android/311020;RNA"
|
||||||
const val gatewayUserAgent = "okhttp/4.12.0"
|
const val gatewayUserAgent = "okhttp/4.12.0"
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
val json = Json {
|
|
||||||
namingStrategy = JsonNamingStrategy.SnakeCase
|
|
||||||
ignoreUnknownKeys = true
|
|
||||||
encodeDefaults = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Desktop uses separate properties
|
// TODO: Desktop uses separate properties
|
||||||
@Suppress("PropertyName")
|
@Suppress("PropertyName")
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ plugins {
|
||||||
alias(libs.plugins.composeMultiplatform) apply false
|
alias(libs.plugins.composeMultiplatform) apply false
|
||||||
alias(libs.plugins.composeCompiler) apply false
|
alias(libs.plugins.composeCompiler) apply false
|
||||||
alias(libs.plugins.koinCompiler) apply false
|
alias(libs.plugins.koinCompiler) apply false
|
||||||
|
alias(libs.plugins.kotest) apply false
|
||||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||||
alias(libs.plugins.kotlinSerialization) apply false
|
alias(libs.plugins.kotlinSerialization) apply false
|
||||||
|
alias(libs.plugins.ksp) apply false
|
||||||
alias(libs.plugins.sqldelight) apply false
|
alias(libs.plugins.sqldelight) apply false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,11 @@ junit = "4.13.2"
|
||||||
kermit = "2.0.8"
|
kermit = "2.0.8"
|
||||||
koin-bom = "4.2.0-RC1"
|
koin-bom = "4.2.0-RC1"
|
||||||
koin-plugin = "0.3.0"
|
koin-plugin = "0.3.0"
|
||||||
|
kotest = "6.1.2"
|
||||||
kotlin = "2.3.0"
|
kotlin = "2.3.0"
|
||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
kotlinx-serialization = "1.10.0"
|
kotlinx-serialization = "1.10.0"
|
||||||
|
ksp = "2.3.4"
|
||||||
ktor = "3.4.0"
|
ktor = "3.4.0"
|
||||||
material3 = "1.11.0-alpha02"
|
material3 = "1.11.0-alpha02"
|
||||||
material3-adaptive = "1.3.0-alpha04"
|
material3-adaptive = "1.3.0-alpha04"
|
||||||
|
|
@ -62,6 +64,10 @@ koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" }
|
||||||
koin-compose-navigation3 = { module = "io.insert-koin:koin-compose-navigation3" }
|
koin-compose-navigation3 = { module = "io.insert-koin:koin-compose-navigation3" }
|
||||||
koin-core = { module = "io.insert-koin:koin-core" }
|
koin-core = { module = "io.insert-koin:koin-core" }
|
||||||
koin-test = { module = "io.insert-koin:koin-test" }
|
koin-test = { module = "io.insert-koin:koin-test" }
|
||||||
|
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
|
||||||
|
kotest-framework = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" }
|
||||||
|
kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" }
|
||||||
|
kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||||
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
||||||
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
|
|
@ -69,6 +75,7 @@ kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serializa
|
||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||||
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
||||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||||
|
ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
|
||||||
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
|
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
|
||||||
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
|
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
|
||||||
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||||
|
|
@ -81,6 +88,8 @@ composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "k
|
||||||
composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "compose-hot-reload" }
|
composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "compose-hot-reload" }
|
||||||
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
||||||
koinCompiler = { id = "io.insert-koin.compiler.plugin", version.ref = "koin-plugin" }
|
koinCompiler = { id = "io.insert-koin.compiler.plugin", version.ref = "koin-plugin" }
|
||||||
|
kotest = { id = "io.kotest", version.ref = "kotest" }
|
||||||
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||||
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
|
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue