Skip to content

Commit 4997743

Browse files
committed
be able to run public endpoint tests on jvm
1 parent ab386a6 commit 4997743

37 files changed

+1749
-1998
lines changed

README.md

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -388,33 +388,8 @@ APIs available only in `SpotifyClientApi` and `SpotifyImplicitGrantApi` instance
388388

389389
### Java
390390
This library has first-class support for Java! You have two choices when using this library: async-only with Kotlin
391-
suspend functions (using SpotifyContinuation), or by using the `SpotifyRestAction` class. Using `SpotifyRestAction`s are
392-
recommended.
393-
394-
#### What is the SpotifyRestAction class and how do I use it?
395-
Abstracting requests into a `SpotifyRestAction` class allows for a lot of flexibility in sending and receiving requests.
396-
This class includes options for asynchronous and blocking execution in all endpoints. However,
397-
due to this, you **must** call one of the provided methods in order for the call
398-
to execute! The `SpotifyRestAction` provides many methods and fields for use, including blocking and asynchronous ones. For example,
399-
- `hasRun()` tells you whether the rest action has been *started*
400-
- `hasCompleted()` tells you whether this rest action has been fully executed and *completed*
401-
- `complete()` blocks the current thread and returns the result
402-
- `suspendComplete(context: CoroutineContext = Dispatchers.Default)` switches to given context, invokes the supplier, and synchronously retrieves the result.
403-
- `suspendQueue()` suspends the coroutine, invokes the supplier asynchronously, and resumes with the result
404-
- `queue()` executes and immediately returns
405-
- `queue(consumer: (T) -> Unit)` executes the provided callback as soon as the request
406-
is asynchronously evaluated
407-
- `queueAfter(quantity: Int, timeUnit: TimeUnit, consumer: (T) -> Unit)` executes the
408-
provided callback after the provided time. As long as supplier execution is less than the provided
409-
time, this will likely be accurate within a few milliseconds.
410-
- `asFuture()` transforms the supplier into a `CompletableFuture` (only JVM)
411-
412-
Here's an example of how easy it is to use `spotify-web-api-kotlin` with a `SpotifyRestAction`:
413-
```java
414-
var api = SpotifyApiBuilderKt.spotifyAppApi(Const.clientId, Const.clientSecret).buildRestAction(true).complete();
415-
var album = api.getAlbums().getAlbumRestAction("spotify:album:0b23AHutIA1BOW0u1dZ6wM", null).complete();
416-
System.out.println("Album name is: " + album.getName());
417-
```
391+
suspend functions (using SpotifyContinuation).
392+
418393

419394
#### Integrating with Kotlin suspend functions via Java `Continuation`s
420395
Unfortunately, coroutines don't play very nicely with Java code. Fortunately, however, we provide a wrapper around Kotlin's

build.gradle.kts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,6 @@ kotlin {
156156
binaries.executable()
157157
}
158158

159-
// val hostOs = System.getProperty("os.name")
160-
// val isMainHost = hostOs.contains("mac", true)
161-
// val isMingwX64 = hostOs.startsWith("Windows")
162-
163159
macosX64 {
164160
mavenPublication {
165161
setupPom(artifactId)
@@ -216,8 +212,6 @@ kotlin {
216212
}
217213

218214
targets {
219-
val kotlinxDatetimeVersion = "0.3.1"
220-
221215
sourceSets {
222216
val kotlinxDatetimeVersion = "0.4.0"
223217
val serializationVersion = "1.3.3"
@@ -227,7 +221,7 @@ kotlin {
227221
val korlibsVersion = "2.2.0"
228222
val androidSpotifyAuthVersion = "1.2.5"
229223
val androidCryptoVersion = "1.0.0"
230-
val coroutineMTVersion = "1.6.0-native-mt"
224+
val coroutineMTVersion = "1.6.4"
231225

232226
val commonMain by getting {
233227
dependencies {
@@ -240,6 +234,7 @@ kotlin {
240234

241235
val commonTest by getting {
242236
dependencies {
237+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
243238
implementation(kotlin("test-common"))
244239
implementation(kotlin("test-annotations-common"))
245240
}

gradle.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ android.useAndroidX=true
2020
android.enableJetifier=true
2121
kotlin.mpp.enableGranularSourceSetsMetadata=true
2222
kotlinVersion=1.7.10
23-
androidBuildToolsVersion=7.0.0
23+
androidBuildToolsVersion=7.0.0
24+
versions.kotlinxdatetime=0.4.0

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,28 +75,28 @@ public abstract class SpotifyEndpoint(public val api: GenericSpotifyApi) {
7575
}
7676
}
7777

78-
internal suspend fun get(url: String): String {
78+
internal open suspend fun get(url: String): String {
7979
return execute<String>(url)
8080
}
8181

8282
internal suspend fun getNullable(url: String): String? {
83-
return execute<String?>(url, retryOnNull = false)
83+
return execute(url, retryOnNull = false)
8484
}
8585

86-
internal suspend fun post(url: String, body: String? = null, contentType: String? = null): String {
87-
return execute<String>(url, body, HttpRequestMethod.POST, contentType = contentType)
86+
internal open suspend fun post(url: String, body: String? = null, contentType: String? = null): String {
87+
return execute(url, body, HttpRequestMethod.POST, contentType = contentType)
8888
}
8989

90-
internal suspend fun put(url: String, body: String? = null, contentType: String? = null): String {
91-
return execute<String>(url, body, HttpRequestMethod.PUT, contentType = contentType)
90+
internal open suspend fun put(url: String, body: String? = null, contentType: String? = null): String {
91+
return execute(url, body, HttpRequestMethod.PUT, contentType = contentType)
9292
}
9393

9494
internal suspend fun delete(
9595
url: String,
9696
body: String? = null,
9797
contentType: String? = null
9898
): String {
99-
return execute<String>(url, body, HttpRequestMethod.DELETE, contentType = contentType)
99+
return execute(url, body, HttpRequestMethod.DELETE, contentType = contentType)
100100
}
101101

102102
@Suppress("UNCHECKED_CAST")

src/commonMain/kotlin/com.adamratzman.spotify/utils/Concurrency.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ public enum class TimeUnit(public val multiplier: Long) {
88
MILLISECONDS(1), SECONDS(1000), MINUTES(60000);
99

1010
public fun toMillis(duration: Long): Long = duration * multiplier
11-
}
11+
}
Lines changed: 18 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,19 @@
11
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2021; Original author: Adam Ratzman */
22
package com.adamratzman.spotify
33

4-
import kotlin.native.concurrent.ThreadLocal
54
import kotlin.test.assertTrue
65
import kotlinx.coroutines.CoroutineScope
7-
8-
val clientId = getEnvironmentVariable("SPOTIFY_CLIENT_ID")
9-
val clientSecret = getEnvironmentVariable("SPOTIFY_CLIENT_SECRET")
10-
val redirectUri = getEnvironmentVariable("SPOTIFY_REDIRECT_URI")
11-
val tokenString = getEnvironmentVariable("SPOTIFY_TOKEN_STRING")
12-
13-
// https://github.com/Kotlin/kotlinx.coroutines/issues/1996#issuecomment-728562784
14-
expect fun runBlockingTest(block: suspend CoroutineScope.() -> Unit)
15-
16-
@ThreadLocal
17-
var instantiationCompleted: Boolean = false
18-
19-
@ThreadLocal
20-
private lateinit var apiBacking: GenericSpotifyApi
21-
22-
// https://github.com/Kotlin/kotlinx.coroutines/issues/706#issuecomment-429922811
23-
suspend fun buildSpotifyApi() = when {
24-
tokenString?.isNotBlank() == true -> {
25-
spotifyClientApi {
26-
credentials {
27-
clientId = com.adamratzman.spotify.clientId
28-
clientSecret = com.adamratzman.spotify.clientSecret
29-
redirectUri = com.adamratzman.spotify.redirectUri
30-
}
31-
authorization {
32-
tokenString = com.adamratzman.spotify.tokenString
33-
}
34-
}.build().also { instantiationCompleted = true; apiBacking = it }
35-
}
36-
clientId?.isNotBlank() == true -> {
37-
spotifyAppApi {
38-
credentials {
39-
clientId = com.adamratzman.spotify.clientId
40-
clientSecret = com.adamratzman.spotify.clientSecret
41-
}
42-
}.build().also {
43-
instantiationCompleted = true; apiBacking = it
44-
}
45-
}
46-
else -> null.also { instantiationCompleted = true }
47-
}?.also { if (getEnvironmentVariable("SPOTIFY_LOG_HTTP") == "true") it.spotifyApiOptions.enableDebugMode = true }
48-
49-
fun buildSpotifyApiSync() = when {
50-
tokenString?.isNotBlank() == true -> {
51-
spotifyClientApi {
52-
credentials {
53-
clientId = com.adamratzman.spotify.clientId
54-
clientSecret = com.adamratzman.spotify.clientSecret
55-
redirectUri = com.adamratzman.spotify.redirectUri
56-
}
57-
authorization {
58-
tokenString = com.adamratzman.spotify.tokenString
59-
}
60-
}.buildRestAction().complete().also { instantiationCompleted = true; apiBacking = it }
61-
}
62-
clientId?.isNotBlank() == true -> {
63-
spotifyAppApi {
64-
credentials {
65-
clientId = com.adamratzman.spotify.clientId
66-
clientSecret = com.adamratzman.spotify.clientSecret
67-
}
68-
}.buildRestAction().complete().also {
69-
instantiationCompleted = true; apiBacking = it
70-
}
71-
}
72-
else -> null.also { instantiationCompleted = true }
73-
}?.also { if (getEnvironmentVariable("SPOTIFY_LOG_HTTP") == "true") it.spotifyApiOptions.enableDebugMode = true }
74-
75-
expect fun getEnvironmentVariable(name: String): String?
76-
77-
expect fun Exception.stackTrace()
6+
import kotlinx.coroutines.Dispatchers
7+
import kotlinx.coroutines.ExperimentalCoroutinesApi
8+
import kotlinx.coroutines.test.TestResult
9+
import kotlinx.coroutines.test.runTest
10+
import kotlinx.coroutines.withContext
11+
12+
expect fun areLivePkceTestsEnabled(): Boolean
13+
expect fun arePlayerTestsEnabled(): Boolean
14+
expect fun getTestClientId(): String?
15+
expect fun getTestClientSecret(): String?
16+
expect suspend fun buildSpotifyApi(): GenericSpotifyApi?
7817

7918
suspend inline fun <reified T : Throwable> assertFailsWithSuspend(crossinline block: suspend () -> Unit) {
8019
val noExceptionMessage = "Expected ${T::class.simpleName} exception to be thrown, but no exception was thrown."
@@ -89,3 +28,10 @@ suspend inline fun <reified T : Throwable> assertFailsWithSuspend(crossinline bl
8928
)
9029
}
9130
}
31+
32+
@OptIn(ExperimentalCoroutinesApi::class)
33+
fun <T> runTestOnDefaultDispatcher(block: suspend CoroutineScope.() -> T): TestResult = runTest {
34+
withContext(Dispatchers.Default) {
35+
block()
36+
}
37+
}
Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,29 @@
11
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2021; Original author: Adam Ratzman */
2+
@file:OptIn(ExperimentalCoroutinesApi::class)
3+
24
package com.adamratzman.spotify
35

6+
import kotlinx.coroutines.ExperimentalCoroutinesApi
7+
48
abstract class AbstractTest<T : GenericSpotifyApi> {
5-
var api: T? = null
9+
lateinit var api: T
10+
var apiInitialized: Boolean = false
611

7-
open fun testPrereq(): Boolean {
8-
val result = api != null
9-
if (!result) println("Prereq failed in ${this::class.simpleName}.")
10-
return result
11-
}
12+
suspend inline fun <reified Z : T> buildApi() {
13+
if (apiInitialized) return
1214

13-
suspend inline fun <reified Z : T> build(): Boolean {
14-
return try {
15-
val f = buildSpotifyApi()
16-
@Suppress("UNCHECKED_CAST")
17-
(f as? T)?.let { if (f is Z) api = it }
18-
api != null
19-
} catch (cce: Exception) {
20-
cce.printStackTrace()
21-
false
15+
val api = buildSpotifyApi()
16+
if (api != null && api is Z) {
17+
this.api = api
18+
apiInitialized = true
2219
}
2320
}
2421

25-
fun buildSync(): Boolean {
26-
return try {
27-
@Suppress("UNCHECKED_CAST")
28-
(buildSpotifyApiSync() as? T)?.let { api = it }
29-
api != null
30-
} catch (cce: ClassCastException) {
22+
suspend fun isApiInitialized(): Boolean {
23+
return if (apiInitialized) true
24+
else {
25+
println("Api is not initialized. buildSpotifyApi returns ${buildSpotifyApi()}")
3126
false
3227
}
3328
}
34-
}
35-
36-
typealias GenericSpotifyApiTest = AbstractTest<GenericSpotifyApi>
37-
typealias SpotifyClientApiTest = AbstractTest<SpotifyClientApi>
29+
}
Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,41 @@
11
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2021; Original author: Adam Ratzman */
2+
@file:OptIn(ExperimentalCoroutinesApi::class)
3+
24
package com.adamratzman.spotify.priv
35

46
import com.adamratzman.spotify.AbstractTest
57
import com.adamratzman.spotify.SpotifyClientApi
68
import com.adamratzman.spotify.SpotifyException.BadRequestException
7-
import com.adamratzman.spotify.runBlockingTest
89
import kotlin.test.Test
910
import kotlin.test.assertEquals
1011
import kotlin.test.assertFailsWith
1112
import kotlin.test.assertNotNull
1213
import kotlin.test.assertNull
14+
import kotlinx.coroutines.ExperimentalCoroutinesApi
15+
import com.adamratzman.spotify.runTestOnDefaultDispatcher
1316

1417
class ClientEpisodeApiTest : AbstractTest<SpotifyClientApi>() {
1518
@Test
16-
fun testGetEpisode() {
17-
return runBlockingTest {
18-
super.build<SpotifyClientApi>()
19-
if (!testPrereq()) return@runBlockingTest else api
19+
fun testGetEpisode() = runTestOnDefaultDispatcher {
20+
buildApi<SpotifyClientApi>()
21+
if (!isApiInitialized()) return@runTestOnDefaultDispatcher
2022

21-
assertNull(api!!.episodes.getEpisode("nonexistant episode"))
22-
assertNotNull(api!!.episodes.getEpisode("3lMZTE81Pbrp0U12WZe27l"))
23-
}
23+
assertNull(api.episodes.getEpisode("nonexistant episode"))
24+
assertNotNull(api.episodes.getEpisode("3lMZTE81Pbrp0U12WZe27l"))
2425
}
2526

2627
@Test
27-
fun testGetEpisodes() {
28-
return runBlockingTest {
29-
super.build<SpotifyClientApi>()
30-
if (!testPrereq()) return@runBlockingTest else api!!
28+
fun testGetEpisodes() = runTestOnDefaultDispatcher {
29+
buildApi<SpotifyClientApi>()
30+
if (!isApiInitialized()) return@runTestOnDefaultDispatcher
3131

32-
assertFailsWith<BadRequestException> { api!!.episodes.getEpisodes("hi", "dad") }
33-
assertFailsWith<BadRequestException> {
34-
api!!.episodes.getEpisodes("1cfOhXP4GQCd5ZFHoSF8gg", "j").map { it?.name }
35-
}
36-
assertEquals(
37-
listOf("The Great Inflation (Classic)"),
38-
api!!.episodes.getEpisodes("3lMZTE81Pbrp0U12WZe27l").map { it?.name }
39-
)
32+
assertFailsWith<BadRequestException> { api.episodes.getEpisodes("hi", "dad") }
33+
assertFailsWith<BadRequestException> {
34+
api.episodes.getEpisodes("1cfOhXP4GQCd5ZFHoSF8gg", "j").map { it?.name }
4035
}
36+
assertEquals(
37+
listOf("The Great Inflation (Classic)"),
38+
api.episodes.getEpisodes("3lMZTE81Pbrp0U12WZe27l").map { it?.name }
39+
)
4140
}
4241
}

0 commit comments

Comments
 (0)