From a75b60135ad4e481a460fd43afb79bfb9a7d67a7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 13 Jun 2026 18:17:42 -0400 Subject: [PATCH] fix: adapt Blossom interceptor test to OkHttp 5 Interceptor.Chain OkHttp 5.4.0 expanded Interceptor.Chain from ~10 to ~40 members, breaking the test's hand-rolled `object : Interceptor.Chain` fake. Replace it with a java.lang.reflect.Proxy that implements only request()/proceed() and throws for the rest, so it survives future interface growth without dozens of empty stubs. A real OkHttpClient can't be used here because Platform init fails outside Robolectric (android.util.Log.isLoggable is unavailable). Co-Authored-By: Claude Opus 4.8 (1M context) --- ...ocalBlossomCacheRedirectInterceptorTest.kt | 120 +++++++++--------- 1 file changed, 57 insertions(+), 63 deletions(-) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/LocalBlossomCacheRedirectInterceptorTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/LocalBlossomCacheRedirectInterceptorTest.kt index 199f089e43..8149863821 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/LocalBlossomCacheRedirectInterceptorTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/LocalBlossomCacheRedirectInterceptorTest.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.service.okhttp -import okhttp3.Connection import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor import okhttp3.Protocol @@ -29,6 +28,7 @@ import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.Assert.assertEquals import org.junit.Test +import java.lang.reflect.Proxy class LocalBlossomCacheRedirectInterceptorTest { private val sha = "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553" @@ -37,7 +37,7 @@ class LocalBlossomCacheRedirectInterceptorTest { fun bridgeOffPassesThrough() { val interceptor = LocalBlossomCacheRedirectInterceptor { false } val captured = mutableListOf() - val response = interceptor.intercept(fakeChain("https://blossom.example.com/$sha.jpg", captured)) + val response = run(interceptor, "https://blossom.example.com/$sha.jpg", captured) assertEquals("https://blossom.example.com/$sha.jpg", captured.single()) response.close() } @@ -46,7 +46,7 @@ class LocalBlossomCacheRedirectInterceptorTest { fun bridgeOnRewritesShaInPath() { val interceptor = LocalBlossomCacheRedirectInterceptor { true } val captured = mutableListOf() - val response = interceptor.intercept(fakeChain("https://blossom.example.com/$sha.jpg", captured)) + val response = run(interceptor, "https://blossom.example.com/$sha.jpg", captured) assertEquals( "http://127.0.0.1:24242/$sha.jpg?xs=https%3A%2F%2Fblossom.example.com", captured.single(), @@ -58,7 +58,7 @@ class LocalBlossomCacheRedirectInterceptorTest { fun bridgeOnPreservesNonHexUrls() { val interceptor = LocalBlossomCacheRedirectInterceptor { true } val captured = mutableListOf() - val response = interceptor.intercept(fakeChain("https://example.com/avatar.jpg", captured)) + val response = run(interceptor, "https://example.com/avatar.jpg", captured) assertEquals("https://example.com/avatar.jpg", captured.single()) response.close() } @@ -67,7 +67,7 @@ class LocalBlossomCacheRedirectInterceptorTest { fun bridgeOnSkipsLocalhost() { val interceptor = LocalBlossomCacheRedirectInterceptor { true } val captured = mutableListOf() - val response = interceptor.intercept(fakeChain("http://127.0.0.1:24242/$sha.jpg", captured)) + val response = run(interceptor, "http://127.0.0.1:24242/$sha.jpg", captured) assertEquals("http://127.0.0.1:24242/$sha.jpg", captured.single()) response.close() } @@ -76,7 +76,7 @@ class LocalBlossomCacheRedirectInterceptorTest { fun bridgeOnHandlesNonStandardPort() { val interceptor = LocalBlossomCacheRedirectInterceptor { true } val captured = mutableListOf() - val response = interceptor.intercept(fakeChain("https://blossom.example.com:8443/$sha.png", captured)) + val response = run(interceptor, "https://blossom.example.com:8443/$sha.png", captured) assertEquals( "http://127.0.0.1:24242/$sha.png?xs=https%3A%2F%2Fblossom.example.com%3A8443", captured.single(), @@ -88,7 +88,7 @@ class LocalBlossomCacheRedirectInterceptorTest { fun bridgeOnHandlesUppercaseSha() { val interceptor = LocalBlossomCacheRedirectInterceptor { true } val captured = mutableListOf() - val response = interceptor.intercept(fakeChain("https://blossom.example.com/${sha.uppercase()}.jpg", captured)) + val response = run(interceptor, "https://blossom.example.com/${sha.uppercase()}.jpg", captured) assertEquals( "http://127.0.0.1:24242/$sha.jpg?xs=https%3A%2F%2Fblossom.example.com", captured.single(), @@ -100,7 +100,7 @@ class LocalBlossomCacheRedirectInterceptorTest { fun bridgeOnFallsBackToBinExtension() { val interceptor = LocalBlossomCacheRedirectInterceptor { true } val captured = mutableListOf() - val response = interceptor.intercept(fakeChain("https://blossom.example.com/$sha", captured)) + val response = run(interceptor, "https://blossom.example.com/$sha", captured) assertEquals( "http://127.0.0.1:24242/$sha.bin?xs=https%3A%2F%2Fblossom.example.com", captured.single(), @@ -112,7 +112,7 @@ class LocalBlossomCacheRedirectInterceptorTest { fun bridgeOnHandlesShaInDeeperPathSegment() { val interceptor = LocalBlossomCacheRedirectInterceptor { true } val captured = mutableListOf() - val response = interceptor.intercept(fakeChain("https://nostr.build/i/cache/$sha.webp", captured)) + val response = run(interceptor, "https://nostr.build/i/cache/$sha.webp", captured) assertEquals( "http://127.0.0.1:24242/$sha.webp?xs=https%3A%2F%2Fnostr.build%2Fi%2Fcache", captured.single(), @@ -124,7 +124,7 @@ class LocalBlossomCacheRedirectInterceptorTest { fun bridgeOnPreservesNostrBuildPathPrefix() { val interceptor = LocalBlossomCacheRedirectInterceptor { true } val captured = mutableListOf() - val response = interceptor.intercept(fakeChain("https://cdn.nostr.build/i/$sha.jpg", captured)) + val response = run(interceptor, "https://cdn.nostr.build/i/$sha.jpg", captured) assertEquals( "http://127.0.0.1:24242/$sha.jpg?xs=https%3A%2F%2Fcdn.nostr.build%2Fi", captured.single(), @@ -138,11 +138,10 @@ class LocalBlossomCacheRedirectInterceptorTest { val interceptor = LocalBlossomCacheRedirectInterceptor { true } val captured = mutableListOf() val response = - interceptor.intercept( - fakeChain( - "https://share.yabu.me/84b0c46ab699ac35eb2ca286470b85e081db2087cdef63932236c397417782f5/28fa4d999af6ae3e4e11bfc2727130ef1b3a13cc0f981e5a93c3996cb2f524e5.webp", - captured, - ), + run( + interceptor, + "https://share.yabu.me/84b0c46ab699ac35eb2ca286470b85e081db2087cdef63932236c397417782f5/28fa4d999af6ae3e4e11bfc2727130ef1b3a13cc0f981e5a93c3996cb2f524e5.webp", + captured, ) assertEquals( "http://127.0.0.1:24242/28fa4d999af6ae3e4e11bfc2727130ef1b3a13cc0f981e5a93c3996cb2f524e5.webp?xs=https%3A%2F%2Fshare.yabu.me%2F84b0c46ab699ac35eb2ca286470b85e081db2087cdef63932236c397417782f5", @@ -158,21 +157,19 @@ class LocalBlossomCacheRedirectInterceptorTest { val interceptor = LocalBlossomCacheRedirectInterceptor { true } val captured = mutableListOf() val url = "https://example.com/$sha/avatar.jpg" - val response = interceptor.intercept(fakeChain(url, captured)) + val response = run(interceptor, url, captured) assertEquals(url, captured.single()) response.close() } @Test - fun bridgeOnSkipsWhenLastSegmentHasNonHexPrefixBeforeSha() { - // nostr.build /i/ layout: _.. The hex inside the - // filename isn't a Blossom blob — rewriting to the local cache and + fun bridgeOnSkipsNostrBuildResizedVariants() { // sending xs=https://nostr.build/i would 404 because the real blob // is at /i/nostr.build_.jpg, not /i/. val interceptor = LocalBlossomCacheRedirectInterceptor { true } val captured = mutableListOf() val url = "https://nostr.build/i/nostr.build_$sha.jpg" - val response = interceptor.intercept(fakeChain(url, captured)) + val response = run(interceptor, url, captured) assertEquals(url, captured.single()) response.close() } @@ -183,55 +180,52 @@ class LocalBlossomCacheRedirectInterceptorTest { val interceptor = LocalBlossomCacheRedirectInterceptor { true } val captured = mutableListOf() val url = "https://example.com/${sha}_thumb.jpg" - val response = interceptor.intercept(fakeChain(url, captured)) + val response = run(interceptor, url, captured) assertEquals(url, captured.single()) response.close() } + /** + * Runs [interceptor] against [url] and returns its response. The chain is a + * dynamic proxy that only implements [Interceptor.Chain.request] and + * [Interceptor.Chain.proceed]; `proceed` records the URL it is asked to + * fetch — i.e. the URL after any rewrite [interceptor] applied — and returns + * a synthetic 200 without touching the network. A proxy is used instead of + * an `object :` literal because OkHttp's `Interceptor.Chain` declares ~40 + * members (client-config getters and `withX` reconfigurers) that this test + * never exercises and that would otherwise need empty stubs. + */ + private fun run( + interceptor: Interceptor, + url: String, + captured: MutableList, + ): Response = interceptor.intercept(fakeChain(url, captured)) + private fun fakeChain( url: String, captured: MutableList, - ): Interceptor.Chain = - object : Interceptor.Chain { - private val request = Request.Builder().url(url.toHttpUrl()).build() - - override fun request(): Request = request - - override fun proceed(request: Request): Response { - captured.add(request.url.toString()) - return Response - .Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(200) - .message("OK") - .body("".toResponseBody(null)) - .build() + ): Interceptor.Chain { + val request = Request.Builder().url(url.toHttpUrl()).build() + return Proxy.newProxyInstance( + Interceptor.Chain::class.java.classLoader, + arrayOf(Interceptor.Chain::class.java), + ) { _, method, args -> + when (method.name) { + "request" -> request + "proceed" -> { + val proceeded = args[0] as Request + captured.add(proceeded.url.toString()) + Response + .Builder() + .request(proceeded) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body("".toResponseBody(null)) + .build() + } + else -> throw UnsupportedOperationException(method.name) } - - override fun connection(): Connection? = null - - override fun call() = throw UnsupportedOperationException() - - override fun connectTimeoutMillis(): Int = 0 - - override fun readTimeoutMillis(): Int = 0 - - override fun writeTimeoutMillis(): Int = 0 - - override fun withConnectTimeout( - timeout: Int, - unit: java.util.concurrent.TimeUnit, - ): Interceptor.Chain = this - - override fun withReadTimeout( - timeout: Int, - unit: java.util.concurrent.TimeUnit, - ): Interceptor.Chain = this - - override fun withWriteTimeout( - timeout: Int, - unit: java.util.concurrent.TimeUnit, - ): Interceptor.Chain = this - } + } as Interceptor.Chain + } }