diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/BaseInterceptorTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/BaseInterceptorTest.kt new file mode 100644 index 00000000..ebdeaf68 --- /dev/null +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/BaseInterceptorTest.kt @@ -0,0 +1,35 @@ +package org.moire.ultrasonic.api.subsonic.interceptors + +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import org.junit.Before +import org.junit.Rule +import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule + +/** + * Base class for testing [okhttp3.Interceptor] implementations. + */ +abstract class BaseInterceptorTest { + @Rule @JvmField val mockWebServerRule = MockWebServerRule() + + lateinit var client: OkHttpClient + + abstract val interceptor: Interceptor + + @Before + fun setUp() { + client = OkHttpClient.Builder().addInterceptor(interceptor).build() + } + + /** + * Creates [Request] to use with [mockWebServerRule]. + * + * @param additionalParams passes [Request.Builder] to add additionally required + * params to the [Request]. + */ + fun createRequest(additionalParams: (Request.Builder) -> Unit): Request = Request.Builder() + .url(mockWebServerRule.mockWebServer.url("/")) + .also { additionalParams(it) } + .build() +} \ No newline at end of file diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/RangeHeaderInterceptorTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/RangeHeaderInterceptorTest.kt new file mode 100644 index 00000000..95f46e04 --- /dev/null +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/RangeHeaderInterceptorTest.kt @@ -0,0 +1,59 @@ +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 to` +import org.amshove.kluent.`should not contain` +import org.junit.Test + +/** + * Unit test for [RangeHeaderInterceptor]. + */ +class RangeHeaderInterceptorTest : BaseInterceptorTest() { + + override val interceptor: Interceptor + get() = RangeHeaderInterceptor() + + @Test + fun `Should update uppercase range header`() { + mockWebServerRule.mockWebServer.enqueue(MockResponse()) + val offset = 111 + val request = createRequest { + it.addHeader("Range", "$offset") + } + + client.newCall(request).execute() + + val executedRequest = mockWebServerRule.mockWebServer.takeRequest() + executedRequest.headers.names() `should contain` "Range" + executedRequest.headers["Range"]!! `should equal to` "bytes=$offset-" + } + + @Test + fun `Should not add range header if request doens't contain it`() { + mockWebServerRule.mockWebServer.enqueue(MockResponse()) + val request = createRequest { } + + client.newCall(request).execute() + + val executedRequest = mockWebServerRule.mockWebServer.takeRequest() + executedRequest.headers.names() `should not contain` "Range" + executedRequest.headers.names() `should not contain` "range" + } + + @Test + fun `Should update lowercase range header`() { + mockWebServerRule.mockWebServer.enqueue(MockResponse()) + val offset = 51233 + val request = createRequest { + it.addHeader("range", "$offset") + } + + client.newCall(request).execute() + + val executedRequest = mockWebServerRule.mockWebServer.takeRequest() + executedRequest.headers.names() `should contain` "Range" + executedRequest.headers["Range"]!! `should equal to` "bytes=$offset-" + } +} \ No newline at end of file diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt index 11fc9041..f0c44510 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt @@ -8,6 +8,7 @@ import okhttp3.HttpUrl import okhttp3.OkHttpClient import okhttp3.ResponseBody import okhttp3.logging.HttpLoggingInterceptor +import org.moire.ultrasonic.api.subsonic.interceptors.RangeHeaderInterceptor import org.moire.ultrasonic.api.subsonic.response.StreamResponse import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse import retrofit2.Response @@ -51,6 +52,7 @@ class SubsonicAPIClient(baseUrl: String, .build() chain.proceed(originalRequest.newBuilder().url(newUrl).build()) } + .addInterceptor(RangeHeaderInterceptor()) .also { if (debug) { it.addLogging() diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/RangeHeaderInterceptor.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/RangeHeaderInterceptor.kt new file mode 100644 index 00000000..3690b0d7 --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/RangeHeaderInterceptor.kt @@ -0,0 +1,25 @@ +package org.moire.ultrasonic.api.subsonic.interceptors + +import okhttp3.Interceptor +import okhttp3.Interceptor.Chain +import okhttp3.Response + +/** + * Modifies request "Range" header to be according to HTTP standard. + * + * See [range rfc](https://tools.ietf.org/html/rfc7233). + */ +internal class RangeHeaderInterceptor : Interceptor { + override fun intercept(chain: Chain): Response { + val originalRequest = chain.request() + val headers = originalRequest.headers() + return if (headers.names().contains("Range")) { + val offset = "bytes=${headers["Range"]}-" + chain.proceed(originalRequest.newBuilder() + .removeHeader("Range").addHeader("Range", offset) + .build()) + } else { + chain.proceed(originalRequest) + } + } +} \ No newline at end of file