diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/VersionInterceptorTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/VersionInterceptorTest.kt new file mode 100644 index 00000000..c26abd4b --- /dev/null +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/VersionInterceptorTest.kt @@ -0,0 +1,74 @@ +package org.moire.ultrasonic.api.subsonic.interceptors + +import okhttp3.Interceptor +import okhttp3.mockwebserver.MockResponse +import org.amshove.kluent.`should contain` +import org.amshove.kluent.`should equal` +import org.junit.Test +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions +import org.moire.ultrasonic.api.subsonic.enqueueResponse +import kotlin.LazyThreadSafetyMode.NONE + +/** + * Integration test for [VersionInterceptor]. + */ +class VersionInterceptorTest : BaseInterceptorTest() { + private val initialProtocolVersion = SubsonicAPIVersions.V1_1_0 + private var updatedProtocolVersion = SubsonicAPIVersions.V1_1_0 + + override val interceptor: Interceptor by lazy(NONE) { + VersionInterceptor(initialProtocolVersion) { + updatedProtocolVersion = it + } + } + + @Test + fun `Should add initial protocol version to request`() { + mockWebServerRule.enqueueResponse("ping_ok.json") + val request = createRequest {} + + client.newCall(request).execute() + + val requestLine = mockWebServerRule.mockWebServer.takeRequest().requestLine + + requestLine `should contain` "v=${initialProtocolVersion.restApiVersion}" + } + + @Test + fun `Should update version from response`() { + mockWebServerRule.enqueueResponse("ping_ok.json") + + client.newCall(createRequest {}).execute() + + (interceptor as VersionInterceptor).protocolVersion `should equal` SubsonicAPIVersions.V1_13_0 + } + + @Test + fun `Should not update version if response json doesn't contain version`() { + mockWebServerRule.enqueueResponse("non_subsonic_response.json") + + client.newCall(createRequest {}).execute() + + (interceptor as VersionInterceptor).protocolVersion `should equal` initialProtocolVersion + } + + @Test + fun `Should not update version on non-json response`() { + mockWebServerRule.mockWebServer.enqueue(MockResponse() + .setBody("asdqwnekjnqwkjen") + .setHeader("Content-Type", "application/octet-stream")) + + client.newCall(createRequest {}).execute() + + (interceptor as VersionInterceptor).protocolVersion `should equal` initialProtocolVersion + } + + @Test + fun `Should notify notifier on version change`() { + mockWebServerRule.enqueueResponse("ping_ok.json") + + client.newCall(createRequest {}).execute() + + updatedProtocolVersion `should equal` SubsonicAPIVersions.V1_13_0 + } +} diff --git a/subsonic-api/src/integrationTest/resources/non_subsonic_response.json b/subsonic-api/src/integrationTest/resources/non_subsonic_response.json new file mode 100644 index 00000000..ffdfd2fa --- /dev/null +++ b/subsonic-api/src/integrationTest/resources/non_subsonic_response.json @@ -0,0 +1,3 @@ +{ + "none" : "some" +} \ No newline at end of file diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/VersionInterceptor.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/VersionInterceptor.kt new file mode 100644 index 00000000..f8469783 --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/VersionInterceptor.kt @@ -0,0 +1,77 @@ +package org.moire.ultrasonic.api.subsonic.interceptors + +import com.fasterxml.jackson.core.JsonFactory +import com.fasterxml.jackson.core.JsonParseException +import com.fasterxml.jackson.core.JsonToken +import okhttp3.Interceptor +import okhttp3.Interceptor.Chain +import okhttp3.Response +import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions +import java.io.IOException + +private const val DEFAULT_PEEK_BYTE_COUNT = 100L + +/** + * Special [Interceptor] that adds client supported version to request and tries to update it + * from server response. + * + * Optionally [notifier] will be invoked on version change. + * + * @author Yahor Berdnikau + */ +internal class VersionInterceptor( + internal var protocolVersion: SubsonicAPIVersions, + private val notifier: (SubsonicAPIVersions) -> Unit = {}) : Interceptor { + private val jsonFactory = JsonFactory() + + override fun intercept(chain: Chain): okhttp3.Response { + val originalRequest = chain.request() + + val newRequest = originalRequest.newBuilder() + .url(originalRequest + .url() + .newBuilder() + .addQueryParameter("v", protocolVersion.restApiVersion) + .build()) + .build() + + val response = chain.proceed(newRequest) + if (response.isSuccessful) { + val isJson = response.body()?.contentType()?.subtype()?.equals("json", true) ?: false + if (isJson) { + tryUpdateProtocolVersion(response) + } + } + + return response + } + + private fun tryUpdateProtocolVersion(response: Response) { + val content = response.peekBody(DEFAULT_PEEK_BYTE_COUNT) + .byteStream().bufferedReader().readText() + + try { + val jsonReader = jsonFactory.createParser(content) + jsonReader.nextToken() + if (jsonReader.currentToken == JsonToken.START_OBJECT) { + while (jsonReader.currentName != "version" && + jsonReader.currentToken != null) { + jsonReader.nextToken() + } + val versionStr = jsonReader.nextTextValue() + if (versionStr != null) { + try { + protocolVersion = SubsonicAPIVersions.fromApiVersion(versionStr) + notifier(protocolVersion) + } catch (e: IllegalArgumentException) { + // no-op + } + } + } + } catch (io: IOException) { + // no-op + } catch (parse: JsonParseException) { + // no-op + } + } +}