Skip to content

Commit a3ecc12

Browse files
committed
allow android tests, add request interception so that we can automatically build a mocked api for non-jvm environment tests
1 parent 4997743 commit a3ecc12

File tree

8 files changed

+117
-46
lines changed

8 files changed

+117
-46
lines changed

src/androidTest/kotlin/com/adamratzman/spotify/CommonImpl.kt

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,62 @@ import kotlinx.coroutines.CoroutineScope
1010
import kotlinx.coroutines.asCoroutineDispatcher
1111
import kotlinx.coroutines.runBlocking
1212

13-
public fun setFinalStatic(field: Field, newValue: Any?) {
13+
private fun setFinalStatic(field: Field, newValue: Any?) {
1414
field.isAccessible = true
1515
val modifiersField = Field::class.java.getDeclaredField("modifiers")
1616
modifiersField.isAccessible = true
1717
modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv())
1818
field.set(null, newValue)
1919
}
2020

21-
public actual fun getEnvironmentVariable(name: String): String? {
21+
private fun getEnvironmentVariable(name: String): String? {
2222
setFinalStatic(VERSION::class.java.getField("SDK_INT"), 26)
2323
return System.getenv(name) ?: System.getProperty(name)
2424
}
2525

26-
public actual fun Exception.stackTrace() {
27-
println(this.stackTrace.joinToString("\n") { it.toString() })
28-
this.printStackTrace()
29-
}
26+
actual fun getTestClientId(): String? = getEnvironmentVariable("SPOTIFY_CLIENT_ID")
27+
actual fun getTestClientSecret(): String? = getEnvironmentVariable("SPOTIFY_CLIENT_SECRET")
28+
actual fun getTestRedirectUri(): String? = getEnvironmentVariable("SPOTIFY_REDIRECT_URI")
29+
actual fun getTestTokenString(): String? = getEnvironmentVariable("SPOTIFY_TOKEN_STRING")
30+
actual fun isHttpLoggingEnabled(): Boolean = getEnvironmentVariable("SPOTIFY_LOG_HTTP") == "true"
31+
actual fun arePlayerTestsEnabled(): Boolean = getEnvironmentVariable("SPOTIFY_ENABLE_PLAYER_TESTS")?.toBoolean() == true
32+
actual fun areLivePkceTestsEnabled(): Boolean = getEnvironmentVariable("VERBOSE_TEST_ENABLED")?.toBoolean() ?: false
33+
34+
actual suspend fun buildSpotifyApi(): GenericSpotifyApi? {
35+
val clientId = getTestClientId()
36+
val clientSecret = getTestClientSecret()
37+
val tokenString = getTestTokenString()
38+
val logHttp = isHttpLoggingEnabled()
39+
40+
return when {
41+
tokenString?.isNotBlank() == true -> {
42+
spotifyClientApi {
43+
credentials {
44+
this.clientId = clientId
45+
this.clientSecret = clientSecret
46+
this.redirectUri = getTestRedirectUri()
47+
}
48+
authorization {
49+
this.tokenString = tokenString
50+
}
51+
options {
52+
this.enableDebugMode = logHttp
53+
}
54+
}.build()
55+
}
3056

31-
public val testCoroutineContext: CoroutineContext =
32-
Executors.newSingleThreadExecutor().asCoroutineDispatcher()
57+
clientId?.isNotBlank() == true -> {
58+
spotifyAppApi {
59+
credentials {
60+
this.clientId = clientId
61+
this.clientSecret = clientSecret
62+
}
63+
options {
64+
this.enableDebugMode = logHttp
65+
}
66+
}.build()
67+
}
3368

34-
public actual fun runBlockingTest(block: suspend CoroutineScope.() -> Unit): Unit = runBlocking(testCoroutineContext) { this.block() }
69+
else -> null
70+
}
71+
}

src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApi.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import com.adamratzman.spotify.endpoints.pub.ShowApi
2424
import com.adamratzman.spotify.endpoints.pub.TrackApi
2525
import com.adamratzman.spotify.endpoints.pub.UserApi
2626
import com.adamratzman.spotify.http.CacheState
27-
import com.adamratzman.spotify.http.HttpConnection
27+
import com.adamratzman.spotify.http.HttpRequest
2828
import com.adamratzman.spotify.http.HttpHeader
2929
import com.adamratzman.spotify.http.HttpRequestMethod
3030
import com.adamratzman.spotify.http.HttpResponse
@@ -339,7 +339,7 @@ public sealed class SpotifyApi<T : SpotifyApi<T, B>, B : ISpotifyApiBuilder<T, B
339339
json: Json = api?.spotifyApiOptions?.json ?: Json.Default
340340
): Token {
341341
val response = executeTokenRequest(
342-
HttpConnection(
342+
HttpRequest(
343343
"https://accounts.spotify.com/api/token",
344344
HttpRequestMethod.POST,
345345
mapOf("grant_type" to "client_credentials"),
@@ -696,11 +696,11 @@ public suspend fun getCredentialedToken(
696696
): Token = SpotifyApi.getCredentialedToken(clientId, clientSecret, api, json)
697697

698698
internal suspend fun executeTokenRequest(
699-
httpConnection: HttpConnection,
699+
httpRequest: HttpRequest,
700700
clientId: String,
701701
clientSecret: String
702702
): HttpResponse {
703-
return httpConnection.execute(
703+
return httpRequest.execute(
704704
listOf(
705705
HttpHeader(
706706
"Authorization",
@@ -738,7 +738,7 @@ public suspend fun refreshSpotifyClientToken(
738738
val response = if (!usesPkceAuth) {
739739
require(clientSecret != null) { "The client secret is not set" }
740740
executeTokenRequest(
741-
HttpConnection(
741+
HttpRequest(
742742
"https://accounts.spotify.com/api/token",
743743
HttpRequestMethod.POST,
744744
getDefaultClientApiTokenBody(),
@@ -749,7 +749,7 @@ public suspend fun refreshSpotifyClientToken(
749749
), clientId, clientSecret
750750
)
751751
} else {
752-
HttpConnection(
752+
HttpRequest(
753753
"https://accounts.spotify.com/api/token",
754754
HttpRequestMethod.POST,
755755
getDefaultClientApiTokenBody(),

src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
package com.adamratzman.spotify
33

44
import com.adamratzman.spotify.SpotifyApi.Companion.getCredentialedToken
5-
import com.adamratzman.spotify.http.HttpConnection
5+
import com.adamratzman.spotify.http.HttpRequest
66
import com.adamratzman.spotify.http.HttpRequestMethod
7+
import com.adamratzman.spotify.http.HttpResponse
78
import com.adamratzman.spotify.models.Token
89
import com.adamratzman.spotify.models.serialization.nonstrictJson
910
import com.adamratzman.spotify.models.serialization.toObject
@@ -891,7 +892,7 @@ public class SpotifyClientApiBuilder(
891892
require(clientId != null && clientSecret != null && redirectUri != null) { "You need to specify a valid clientId, clientSecret, and redirectUri in the credentials block!" }
892893

893894
val response = executeTokenRequest(
894-
HttpConnection(
895+
HttpRequest(
895896
"https://accounts.spotify.com/api/token",
896897
HttpRequestMethod.POST,
897898
mapOf(
@@ -929,7 +930,7 @@ public class SpotifyClientApiBuilder(
929930
authorization.authorizationCode != null && authorization.pkceCodeVerifier != null -> try {
930931
require(clientId != null && redirectUri != null) { "You need to specify a valid clientId and redirectUri in the credentials block!" }
931932

932-
val response = HttpConnection(
933+
val response = HttpRequest(
933934
"https://accounts.spotify.com/api/token",
934935
HttpRequestMethod.POST,
935936
mapOf(
@@ -1136,6 +1137,7 @@ public class SpotifyUserAuthorization(
11361137
* to avoid retrying at all, or set to null to keep retrying until success.
11371138
* @param enableDebugMode Whether to enable debug mode (false by default). With debug mode, all response JSON will be outputted to console.
11381139
* @param afterTokenRefresh An optional block to execute after token refresh has been completed.
1140+
* @param httpResponseSubscriber An optional suspending method to subscribe to successful http responses.
11391141
*/
11401142
public data class SpotifyApiOptions(
11411143
public var useCache: Boolean = true,
@@ -1154,5 +1156,6 @@ public data class SpotifyApiOptions(
11541156
public var proxyBaseUrl: String? = null,
11551157
public var retryOnInternalServerErrorTimes: Int? = 5,
11561158
public var enableDebugMode: Boolean = false,
1159+
public var httpResponseSubscriber: (suspend (request: HttpRequest, response: HttpResponse) -> Unit)? = null,
11571160
public var afterTokenRefresh: (suspend (GenericSpotifyApi) -> Unit)? = null
11581161
)

src/commonMain/kotlin/com.adamratzman.spotify/http/Endpoints.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ public abstract class SpotifyEndpoint(public val api: GenericSpotifyApi) {
206206
body: String? = null,
207207
method: HttpRequestMethod = HttpRequestMethod.GET,
208208
contentType: String? = null
209-
) = HttpConnection(
209+
) = HttpRequest(
210210
url,
211211
method,
212212
null,

src/commonMain/kotlin/com.adamratzman.spotify/http/HttpConnection.kt renamed to src/commonMain/kotlin/com.adamratzman.spotify/http/HttpRequest.kt

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import com.adamratzman.spotify.models.ErrorResponse
1111
import com.adamratzman.spotify.models.SpotifyRatelimitedException
1212
import com.adamratzman.spotify.models.serialization.nonstrictJson
1313
import com.adamratzman.spotify.models.serialization.toObject
14+
import com.soywiz.klogger.Console
15+
import com.soywiz.korio.async.launch
1416
import io.ktor.client.HttpClient
1517
import io.ktor.client.plugins.ResponseException
1618
import io.ktor.client.request.HttpRequestBuilder
@@ -24,6 +26,7 @@ import io.ktor.http.HttpMethod
2426
import io.ktor.http.content.ByteArrayContent
2527
import io.ktor.utils.io.core.toByteArray
2628
import kotlinx.coroutines.CancellationException
29+
import kotlinx.coroutines.currentCoroutineContext
2730
import kotlinx.coroutines.delay
2831
import kotlinx.serialization.Serializable
2932

@@ -40,10 +43,12 @@ public data class HttpHeader(val key: String, val value: String)
4043
@Serializable
4144
public data class HttpResponse(val responseCode: Int, val body: String, val headers: List<HttpHeader>)
4245

46+
public typealias HttpConnection = HttpRequest
47+
4348
/**
4449
* Provides a fast, easy, and slim way to execute and retrieve HTTP GET, POST, PUT, and DELETE requests
4550
*/
46-
public class HttpConnection constructor(
51+
public class HttpRequest constructor(
4752
public val url: String,
4853
public val method: HttpRequestMethod,
4954
public val bodyMap: Map<*, *>?,
@@ -59,28 +64,30 @@ public class HttpConnection constructor(
5964
}
6065

6166
public fun buildRequest(additionalHeaders: List<HttpHeader>?): HttpRequestBuilder = HttpRequestBuilder().apply {
62-
url(this@HttpConnection.url)
63-
method = this@HttpConnection.method.externalMethod
67+
url(this@HttpRequest.url)
68+
method = this@HttpRequest.method.externalMethod
6469

6570
setBody(
66-
when (this@HttpConnection.method) {
71+
when (this@HttpRequest.method) {
6772
HttpRequestMethod.DELETE -> {
6873
bodyString.toByteArrayContent() ?: body
6974
}
75+
7076
HttpRequestMethod.PUT, HttpRequestMethod.POST -> {
7177
val contentString = if (contentType == ContentType.Application.FormUrlEncoded) {
7278
bodyMap?.map { "${it.key}=${it.value}" }?.joinToString("&") ?: bodyString
7379
} else bodyString
7480

7581
contentString.toByteArrayContent() ?: ByteArrayContent("".toByteArray(), contentType)
7682
}
83+
7784
else -> body
7885
}
7986
)
8087

8188
// let additionalHeaders overwrite headers
82-
val allHeaders = if (additionalHeaders == null) this@HttpConnection.headers
83-
else this@HttpConnection.headers.filter { oldHeaders -> oldHeaders.key !in additionalHeaders.map { it.key } } + additionalHeaders
89+
val allHeaders = if (additionalHeaders == null) this@HttpRequest.headers
90+
else this@HttpRequest.headers.filter { oldHeaders -> oldHeaders.key !in additionalHeaders.map { it.key } } + additionalHeaders
8491

8592
allHeaders.forEach { (key, value) ->
8693
header(key, value)
@@ -92,7 +99,7 @@ public class HttpConnection constructor(
9299
retryIfInternalServerErrorLeft: Int? = SpotifyApiOptions().retryOnInternalServerErrorTimes // default
93100
): HttpResponse {
94101
val httpRequest = buildRequest(additionalHeaders)
95-
if (api?.spotifyApiOptions?.enableDebugMode == true) println("DEBUG MODE: Request: $this")
102+
if (api?.spotifyApiOptions?.enableDebugMode == true) Console.debug("Request: $this")
96103
try {
97104
return httpClient.request(httpRequest).let { response ->
98105
val respCode = response.status.value
@@ -119,7 +126,7 @@ public class HttpConnection constructor(
119126
}
120127

121128
val body: String = response.bodyAsText()
122-
if (api?.spotifyApiOptions?.enableDebugMode == true) println("DEBUG MODE: $body")
129+
if (api?.spotifyApiOptions?.enableDebugMode == true) Console.debug("Request body: $body")
123130

124131
if (respCode == 401 && body.contains("access token") && api?.spotifyApiOptions?.automaticRefresh == true) {
125132
api.refreshToken()
@@ -133,7 +140,7 @@ public class HttpConnection constructor(
133140
)
134141
}
135142

136-
return HttpResponse(
143+
val response = HttpResponse(
137144
responseCode = respCode,
138145
body = body,
139146
headers = response.headers.entries().map { (key, value) ->
@@ -143,12 +150,20 @@ public class HttpConnection constructor(
143150
)
144151
}
145152
)
153+
154+
api?.spotifyApiOptions?.httpResponseSubscriber?.let { subscriber ->
155+
launch(currentCoroutineContext()) {
156+
subscriber(this, response)
157+
}
158+
}
159+
160+
return response
146161
}
147162
} catch (e: CancellationException) {
148163
throw e
149164
} catch (e: ResponseException) {
150165
val errorBody = e.response.bodyAsText()
151-
if (api?.spotifyApiOptions?.enableDebugMode == true) println("DEBUG MODE: $errorBody")
166+
if (api?.spotifyApiOptions?.enableDebugMode == true) Console.debug("Error body: $errorBody")
152167
try {
153168
val respCode = e.response.status.value
154169

src/commonTest/kotlin/com.adamratzman/spotify/Common.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ expect fun areLivePkceTestsEnabled(): Boolean
1313
expect fun arePlayerTestsEnabled(): Boolean
1414
expect fun getTestClientId(): String?
1515
expect fun getTestClientSecret(): String?
16+
expect fun getTestRedirectUri(): String?
17+
expect fun getTestTokenString(): String?
18+
expect fun isHttpLoggingEnabled(): Boolean
19+
1620
expect suspend fun buildSpotifyApi(): GenericSpotifyApi?
1721

1822
suspend inline fun <reified T : Throwable> assertFailsWithSuspend(crossinline block: suspend () -> Unit) {

src/commonTest/kotlin/com.adamratzman/spotify/utilities/HttpConnectionTests.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
package com.adamratzman.spotify.utilities
55

6-
import com.adamratzman.spotify.http.HttpConnection
6+
import com.adamratzman.spotify.http.HttpRequest
77
import com.adamratzman.spotify.http.HttpRequestMethod
88
import kotlin.test.Test
99
import kotlin.test.assertEquals
@@ -18,7 +18,7 @@ import kotlinx.serialization.json.jsonPrimitive
1818
class HttpConnectionTests {
1919
@Test
2020
fun testGetRequest() = runTestOnDefaultDispatcher {
21-
val (response, body) = HttpConnection(
21+
val (response, body) = HttpRequest(
2222
"https://httpbin.org/get?query=string",
2323
HttpRequestMethod.GET,
2424
null,
@@ -44,7 +44,7 @@ class HttpConnectionTests {
4444

4545
@Test
4646
fun testPostRequest() = runTestOnDefaultDispatcher {
47-
val (response, body) = HttpConnection(
47+
val (response, body) = HttpRequest(
4848
"https://httpbin.org/post?query=string",
4949
HttpRequestMethod.POST,
5050
null,
@@ -72,7 +72,7 @@ class HttpConnectionTests {
7272

7373
@Test
7474
fun testDeleteRequest() = runTestOnDefaultDispatcher {
75-
val (response, _) = HttpConnection(
75+
val (response, _) = HttpRequest(
7676
"https://httpbin.org/delete?query=string",
7777
HttpRequestMethod.DELETE,
7878
null,

0 commit comments

Comments
 (0)