Updated on 2026-08-14
This commit is contained in:
parent
6f82ba15b7
commit
80c206e13b
12 changed files with 468 additions and 51 deletions
|
|
@ -5,6 +5,8 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.datasource.api.auth.AuthApi
|
||||
import com.tangem.datasource.api.auth.qualifier.SessionAuthAuthenticator
|
||||
import com.tangem.datasource.api.auth.qualifier.SessionAuthInterceptor
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.lib.auth.AuthFeatureToggles
|
||||
import com.tangem.lib.auth.devicekey.DeviceKeyManager
|
||||
|
|
@ -14,6 +16,7 @@ import com.tangem.lib.auth.dpop.DpopProofFactory
|
|||
import com.tangem.lib.auth.dpop.internal.DefaultDpopProofFactory
|
||||
import com.tangem.lib.auth.dpop.internal.DisabledDpopProofFactory
|
||||
import com.tangem.lib.auth.http.DpopAuthorizationInterceptor
|
||||
import com.tangem.lib.auth.http.SessionAuthenticator
|
||||
import com.tangem.lib.auth.nonce.AuthNonceDecryptor
|
||||
import com.tangem.lib.auth.nonce.internal.DefaultAuthNonceDecryptor
|
||||
import com.tangem.lib.auth.nonce.internal.DisabledAuthNonceDecryptor
|
||||
|
|
@ -35,6 +38,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Interceptor
|
||||
import java.security.KeyStore
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -43,6 +48,16 @@ import javax.inject.Singleton
|
|||
@InstallIn(SingletonComponent::class)
|
||||
internal object AuthModule {
|
||||
|
||||
/**
|
||||
* Exposes the backend-authentication feature toggle as a plain `Boolean` so that callers
|
||||
* in `core:datasource` (which can't depend on `libs:auth` for layering reasons) can gate
|
||||
* session-auth wiring without importing [AuthFeatureToggles].
|
||||
*/
|
||||
@Provides
|
||||
@Named("isBackendAuthenticationEnabled")
|
||||
fun provideIsBackendAuthenticationEnabled(authFeatureToggles: AuthFeatureToggles): Boolean =
|
||||
authFeatureToggles.isBackendAuthenticationEnabled
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDeviceKeyManager(
|
||||
|
|
@ -150,8 +165,15 @@ internal object AuthModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDpopAuthorizationInterceptor(
|
||||
store: SessionTokensStore,
|
||||
proofFactory: DpopProofFactory,
|
||||
): DpopAuthorizationInterceptor = DpopAuthorizationInterceptor(store, proofFactory)
|
||||
@SessionAuthInterceptor
|
||||
fun provideDpopAuthorizationInterceptor(store: SessionTokensStore, proofFactory: DpopProofFactory): Interceptor {
|
||||
return DpopAuthorizationInterceptor(store, proofFactory)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@SessionAuthAuthenticator
|
||||
fun provideSessionAuthenticator(refresher: SessionTokenRefresher, proofFactory: DpopProofFactory): Authenticator {
|
||||
return SessionAuthenticator(refresher, proofFactory)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,17 @@
|
|||
package com.tangem.lib.auth.http
|
||||
|
||||
import com.tangem.datasource.api.auth.RequiresDpopProof
|
||||
import com.tangem.datasource.api.auth.RequiresSessionAuth
|
||||
import com.tangem.lib.auth.dpop.DpopProofFactory
|
||||
import com.tangem.lib.auth.session.SessionTokensStore
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import retrofit2.Invocation
|
||||
|
||||
/**
|
||||
* Adds [RFC 9449](https://www.rfc-editor.org/rfc/rfc9449) DPoP headers to requests whose
|
||||
* Retrofit method is marked with [RequiresSessionAuth]:
|
||||
* Retrofit method is marked with [RequiresDpopProof] or the umbrella [RequiresSessionAuth]:
|
||||
* - `Authorization: DPoP <access-token>` — present if [SessionTokensStore] holds an access token.
|
||||
* - `DPoP: <proof-jwt>` — freshly generated for every annotated request; `ath` claim is set if
|
||||
* the access token is present.
|
||||
|
|
@ -32,7 +31,7 @@ class DpopAuthorizationInterceptor(
|
|||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val original = chain.request()
|
||||
|
||||
if (!original.requiresSessionAuth()) return chain.proceed(original)
|
||||
if (!original.requiresDpopProof()) return chain.proceed(original)
|
||||
|
||||
val accessToken = runBlocking { store.get().getOrNull()?.accessToken }
|
||||
if (accessToken == null) {
|
||||
|
|
@ -43,7 +42,7 @@ class DpopAuthorizationInterceptor(
|
|||
}
|
||||
|
||||
val proof = runBlocking {
|
||||
proofFactory.create(original.method, original.url.toString(), accessToken)
|
||||
proofFactory.create(original.method, original.htuUrl(), accessToken)
|
||||
}.getOrNull()
|
||||
|
||||
if (proof == null) {
|
||||
|
|
@ -51,20 +50,6 @@ class DpopAuthorizationInterceptor(
|
|||
return chain.proceed(original)
|
||||
}
|
||||
|
||||
return chain.proceed(
|
||||
original.newBuilder()
|
||||
.header(HEADER_AUTHORIZATION, "$DPOP_SCHEME $accessToken")
|
||||
.header(HEADER_DPOP, proof)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun Request.requiresSessionAuth(): Boolean =
|
||||
tag(Invocation::class.java)?.method()?.isAnnotationPresent(RequiresSessionAuth::class.java) == true
|
||||
|
||||
private companion object {
|
||||
const val HEADER_AUTHORIZATION = "Authorization"
|
||||
const val HEADER_DPOP = "DPoP"
|
||||
const val DPOP_SCHEME = "DPoP"
|
||||
return chain.proceed(original.withDpopHeaders(accessToken, proof))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.lib.auth.http
|
||||
|
||||
import com.tangem.datasource.api.auth.RequiresDpopProof
|
||||
import com.tangem.datasource.api.auth.RequiresSessionAuth
|
||||
import com.tangem.datasource.api.auth.RequiresSessionRefresh
|
||||
import okhttp3.Request
|
||||
import retrofit2.Invocation
|
||||
|
||||
internal const val HEADER_AUTHORIZATION = "Authorization"
|
||||
internal const val HEADER_DPOP = "DPoP"
|
||||
internal const val DPOP_SCHEME = "DPoP"
|
||||
|
||||
/**
|
||||
* `true` when the Retrofit method behind this request opts into outgoing DPoP proof headers —
|
||||
* either explicitly via [RequiresDpopProof] or transitively via the umbrella [RequiresSessionAuth].
|
||||
*/
|
||||
internal fun Request.requiresDpopProof(): Boolean =
|
||||
hasMethodAnnotation<RequiresDpopProof>() || hasMethodAnnotation<RequiresSessionAuth>()
|
||||
|
||||
/**
|
||||
* `true` when the Retrofit method behind this request opts into automatic session-token refresh
|
||||
* on 401/403 — either explicitly via [RequiresSessionRefresh] or transitively via [RequiresSessionAuth].
|
||||
*/
|
||||
internal fun Request.requiresSessionRefresh(): Boolean =
|
||||
hasMethodAnnotation<RequiresSessionRefresh>() || hasMethodAnnotation<RequiresSessionAuth>()
|
||||
|
||||
/** Returns a copy of this request with `Authorization: DPoP <token>` and `DPoP: <proof>` headers set. */
|
||||
internal fun Request.withDpopHeaders(accessToken: String, proof: String): Request = newBuilder()
|
||||
.header(HEADER_AUTHORIZATION, "$DPOP_SCHEME $accessToken")
|
||||
.header(HEADER_DPOP, proof)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Target URI for the DPoP `htu` claim — full URL stripped of query and fragment per RFC 9449 §4.2.
|
||||
* Callers must pass this (not the raw `url.toString()`) to `DpopProofFactory.create` so the contract
|
||||
* is honoured at the call site rather than relying on defensive stripping inside any one factory impl.
|
||||
*/
|
||||
internal fun Request.htuUrl(): String = url.toString().substringBefore('#').substringBefore('?')
|
||||
|
||||
private inline fun <reified A : Annotation> Request.hasMethodAnnotation(): Boolean =
|
||||
tag(Invocation::class.java)?.method()?.isAnnotationPresent(A::class.java) == true
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.lib.auth.http
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.datasource.api.auth.RequiresSessionRefresh
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
|
||||
import com.tangem.lib.auth.dpop.DpopProofFactory
|
||||
import com.tangem.lib.auth.session.SessionTokenRefresher
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
|
||||
/**
|
||||
* OkHttp [Authenticator] that reacts to 401/403 by rotating session tokens via
|
||||
* [SessionTokenRefresher] and retrying the original request with a fresh DPoP proof.
|
||||
*
|
||||
* Returns `null` (giving up) when:
|
||||
* - the response code is not 401/403;
|
||||
* - the Retrofit method is **not** annotated with [RequiresSessionRefresh] (or the umbrella
|
||||
* [RequiresSessionAuth]) — keeps public endpoints and refresh-flow endpoints themselves
|
||||
* (annotated with `@RequiresDpopProof` only) from triggering token rotation on incidental 401s;
|
||||
* - the request was already retried once (`response.priorResponse != null`);
|
||||
* - the refresher fails (revoked session, network error, etc.).
|
||||
*
|
||||
* This guarantees at most one retry per call site — OkHttp will not loop on persistent 401s.
|
||||
*/
|
||||
class SessionAuthenticator(
|
||||
private val refresher: SessionTokenRefresher,
|
||||
private val proofFactory: DpopProofFactory,
|
||||
) : Authenticator {
|
||||
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
if (response.code != Code.UNAUTHORIZED.numericCode && response.code != Code.FORBIDDEN.numericCode) return null
|
||||
if (response.priorResponse != null) return null
|
||||
if (!response.request.requiresSessionRefresh()) return null
|
||||
|
||||
val refreshed = runBlocking { refresher.refresh() }.getOrElse { error ->
|
||||
TangemLogger.e("Session refresh failed ($error); surfacing original ${response.code}")
|
||||
return null
|
||||
}
|
||||
|
||||
val request = response.request
|
||||
val proof = runBlocking {
|
||||
proofFactory.create(request.method, request.htuUrl(), refreshed.accessToken)
|
||||
}.getOrElse {
|
||||
TangemLogger.e("DPoP proof generation failed after refresh; cannot retry request")
|
||||
return null
|
||||
}
|
||||
|
||||
return request.withDpopHeaders(refreshed.accessToken, proof)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,9 @@ package com.tangem.lib.auth.http
|
|||
import arrow.core.None
|
||||
import arrow.core.Some
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.auth.RequiresDpopProof
|
||||
import com.tangem.datasource.api.auth.RequiresSessionAuth
|
||||
import com.tangem.datasource.api.auth.RequiresSessionRefresh
|
||||
import com.tangem.lib.auth.dpop.DpopProofFactory
|
||||
import com.tangem.lib.auth.session.SessionTokens
|
||||
import com.tangem.lib.auth.session.SessionTokensStore
|
||||
|
|
@ -47,12 +49,12 @@ class DpopAuthorizationInterceptorTest {
|
|||
)
|
||||
|
||||
@Test
|
||||
fun `annotated request gets Authorization and DPoP headers`() {
|
||||
fun `@RequiresDpopProof method gets Authorization and DPoP headers`() {
|
||||
coEvery { store.get() } returns Some(storedTokens)
|
||||
coEvery { proofFactory.create(any(), any(), "old-access") } returns Some("proof-jwt")
|
||||
|
||||
val proceeded = slot<Request>()
|
||||
val chain = chain(request(annotated = true), proceeded)
|
||||
val chain = chain(request(dpop = true), proceeded)
|
||||
|
||||
interceptor.intercept(chain)
|
||||
|
||||
|
|
@ -60,12 +62,38 @@ class DpopAuthorizationInterceptorTest {
|
|||
assertThat(proceeded.captured.header("DPoP")).isEqualTo("proof-jwt")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `@RequiresSessionAuth (umbrella) method gets headers — covers proof path transitively`() {
|
||||
coEvery { store.get() } returns Some(storedTokens)
|
||||
coEvery { proofFactory.create(any(), any(), "old-access") } returns Some("proof-jwt")
|
||||
|
||||
val proceeded = slot<Request>()
|
||||
val chain = chain(request(sessionAuth = true), proceeded)
|
||||
|
||||
interceptor.intercept(chain)
|
||||
|
||||
assertThat(proceeded.captured.header("Authorization")).isEqualTo("DPoP old-access")
|
||||
assertThat(proceeded.captured.header("DPoP")).isEqualTo("proof-jwt")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `@RequiresSessionRefresh-only method does NOT get DPoP headers`() {
|
||||
val proceeded = slot<Request>()
|
||||
val chain = chain(request(sessionRefresh = true), proceeded)
|
||||
|
||||
interceptor.intercept(chain)
|
||||
|
||||
assertThat(proceeded.captured.header("Authorization")).isNull()
|
||||
assertThat(proceeded.captured.header("DPoP")).isNull()
|
||||
coVerify(exactly = 0) { proofFactory.create(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `annotated request without access token passes through unmodified`() {
|
||||
coEvery { store.get() } returns None
|
||||
|
||||
val proceeded = slot<Request>()
|
||||
val chain = chain(request(annotated = true), proceeded)
|
||||
val chain = chain(request(dpop = true), proceeded)
|
||||
|
||||
interceptor.intercept(chain)
|
||||
|
||||
|
|
@ -76,9 +104,8 @@ class DpopAuthorizationInterceptorTest {
|
|||
|
||||
@Test
|
||||
fun `unannotated request passes through unchanged — proof factory never invoked`() {
|
||||
val original = request(annotated = false)
|
||||
val proceeded = slot<Request>()
|
||||
val chain = chain(original, proceeded)
|
||||
val chain = chain(request(), proceeded)
|
||||
|
||||
interceptor.intercept(chain)
|
||||
|
||||
|
|
@ -105,7 +132,7 @@ class DpopAuthorizationInterceptorTest {
|
|||
coEvery { proofFactory.create(any(), any(), any()) } returns None
|
||||
|
||||
val proceeded = slot<Request>()
|
||||
val chain = chain(request(annotated = true), proceeded)
|
||||
val chain = chain(request(dpop = true), proceeded)
|
||||
|
||||
interceptor.intercept(chain)
|
||||
|
||||
|
|
@ -113,15 +140,21 @@ class DpopAuthorizationInterceptorTest {
|
|||
assertThat(proceeded.captured.header("DPoP")).isNull()
|
||||
}
|
||||
|
||||
private fun request(annotated: Boolean): Request {
|
||||
private fun request(
|
||||
dpop: Boolean = false,
|
||||
sessionRefresh: Boolean = false,
|
||||
sessionAuth: Boolean = false,
|
||||
): Request {
|
||||
val builder = Request.Builder().url("https://example.com/api/v1/foo")
|
||||
builder.tag(Invocation::class.java, invocationWithAnnotation(annotated))
|
||||
builder.tag(Invocation::class.java, invocationWith(dpop, sessionRefresh, sessionAuth))
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun invocationWithAnnotation(annotated: Boolean): Invocation {
|
||||
private fun invocationWith(dpop: Boolean, sessionRefresh: Boolean, sessionAuth: Boolean): Invocation {
|
||||
val method = mockk<Method>()
|
||||
every { method.isAnnotationPresent(RequiresSessionAuth::class.java) } returns annotated
|
||||
every { method.isAnnotationPresent(RequiresDpopProof::class.java) } returns dpop
|
||||
every { method.isAnnotationPresent(RequiresSessionRefresh::class.java) } returns sessionRefresh
|
||||
every { method.isAnnotationPresent(RequiresSessionAuth::class.java) } returns sessionAuth
|
||||
val invocation = mockk<Invocation>()
|
||||
every { invocation.method() } returns method
|
||||
return invocation
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
package com.tangem.lib.auth.http
|
||||
|
||||
import arrow.core.None
|
||||
import arrow.core.Some
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.auth.RequiresDpopProof
|
||||
import com.tangem.datasource.api.auth.RequiresSessionAuth
|
||||
import com.tangem.datasource.api.auth.RequiresSessionRefresh
|
||||
import com.tangem.lib.auth.dpop.DpopProofFactory
|
||||
import com.tangem.lib.auth.session.AuthError
|
||||
import com.tangem.lib.auth.session.SessionRefreshError
|
||||
import com.tangem.lib.auth.session.SessionTokenRefresher
|
||||
import com.tangem.lib.auth.session.SessionTokens
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.datetime.Instant
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import retrofit2.Invocation
|
||||
import java.lang.reflect.Method
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class SessionAuthenticatorTest {
|
||||
|
||||
private val refresher: SessionTokenRefresher = mockk()
|
||||
private val proofFactory: DpopProofFactory = mockk()
|
||||
|
||||
private val authenticator = SessionAuthenticator(refresher, proofFactory)
|
||||
|
||||
private val refreshedTokens = SessionTokens(
|
||||
accessToken = "new-access",
|
||||
accessTokenExpiresAt = Instant.fromEpochSeconds(1_700_000_000),
|
||||
refreshToken = "rt-2",
|
||||
refreshTokenExpiresAt = Instant.fromEpochSeconds(1_700_003_600),
|
||||
walletIds = emptyList(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `401 on @RequiresSessionRefresh triggers refresh and retries with new headers`() {
|
||||
coEvery { refresher.refresh() } returns refreshedTokens.right()
|
||||
coEvery { proofFactory.create(any(), any(), "new-access") } returns Some("fresh-proof")
|
||||
|
||||
val retried = authenticator.authenticate(
|
||||
route = null,
|
||||
response = response(code = 401, sessionRefresh = true),
|
||||
)
|
||||
|
||||
assertThat(retried).isNotNull()
|
||||
assertThat(retried!!.header("Authorization")).isEqualTo("DPoP new-access")
|
||||
assertThat(retried.header("DPoP")).isEqualTo("fresh-proof")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `401 on @RequiresSessionAuth (umbrella) triggers refresh — covers refresh path transitively`() {
|
||||
coEvery { refresher.refresh() } returns refreshedTokens.right()
|
||||
coEvery { proofFactory.create(any(), any(), "new-access") } returns Some("fresh-proof")
|
||||
|
||||
val retried = authenticator.authenticate(
|
||||
route = null,
|
||||
response = response(code = 401, sessionAuth = true),
|
||||
)
|
||||
|
||||
assertThat(retried).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `401 on @RequiresDpopProof-only method does NOT trigger refresh — prevents recursion`() {
|
||||
val retried = authenticator.authenticate(
|
||||
route = null,
|
||||
response = response(code = 401, dpop = true),
|
||||
)
|
||||
|
||||
assertThat(retried).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `403 also triggers refresh`() {
|
||||
coEvery { refresher.refresh() } returns refreshedTokens.right()
|
||||
coEvery { proofFactory.create(any(), any(), "new-access") } returns Some("fresh-proof")
|
||||
|
||||
val retried = authenticator.authenticate(
|
||||
route = null,
|
||||
response = response(code = 403, sessionRefresh = true),
|
||||
)
|
||||
|
||||
assertThat(retried).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `other 4xx codes are passed through`() {
|
||||
val retried = authenticator.authenticate(
|
||||
route = null,
|
||||
response = response(code = 404, sessionRefresh = true),
|
||||
)
|
||||
|
||||
assertThat(retried).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `prior response present means we already retried — give up`() {
|
||||
val first = response(code = 401, sessionRefresh = true)
|
||||
val second = response(code = 401, sessionRefresh = true, priorResponse = first)
|
||||
|
||||
val retried = authenticator.authenticate(route = null, response = second)
|
||||
|
||||
assertThat(retried).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unannotated request 401 is passed through without refresh`() {
|
||||
val retried = authenticator.authenticate(route = null, response = response(code = 401))
|
||||
|
||||
assertThat(retried).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refresh failure gives up`() {
|
||||
coEvery { refresher.refresh() } returns SessionRefreshError.Api(AuthError.NetworkError).left()
|
||||
|
||||
val retried = authenticator.authenticate(
|
||||
route = null,
|
||||
response = response(code = 401, sessionRefresh = true),
|
||||
)
|
||||
|
||||
assertThat(retried).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `proof generation None result gives up`() {
|
||||
coEvery { refresher.refresh() } returns refreshedTokens.right()
|
||||
coEvery { proofFactory.create(any(), any(), any()) } returns None
|
||||
|
||||
val retried = authenticator.authenticate(
|
||||
route = null,
|
||||
response = response(code = 401, sessionRefresh = true),
|
||||
)
|
||||
|
||||
assertThat(retried).isNull()
|
||||
}
|
||||
|
||||
private fun response(
|
||||
code: Int,
|
||||
dpop: Boolean = false,
|
||||
sessionRefresh: Boolean = false,
|
||||
sessionAuth: Boolean = false,
|
||||
priorResponse: Response? = null,
|
||||
): Response {
|
||||
val builder = Request.Builder().url("https://example.com/api/v1/foo")
|
||||
builder.tag(Invocation::class.java, invocationWith(dpop, sessionRefresh, sessionAuth))
|
||||
val request = builder.build()
|
||||
return Response.Builder()
|
||||
.request(request)
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(code)
|
||||
.message("test")
|
||||
.apply { if (priorResponse != null) priorResponse(priorResponse) }
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun invocationWith(dpop: Boolean, sessionRefresh: Boolean, sessionAuth: Boolean): Invocation {
|
||||
val method = mockk<Method>()
|
||||
every { method.isAnnotationPresent(RequiresDpopProof::class.java) } returns dpop
|
||||
every { method.isAnnotationPresent(RequiresSessionRefresh::class.java) } returns sessionRefresh
|
||||
every { method.isAnnotationPresent(RequiresSessionAuth::class.java) } returns sessionAuth
|
||||
val invocation = mockk<Invocation>()
|
||||
every { invocation.method() } returns method
|
||||
return invocation
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue