BIT-725, BIT-328: Add base URL interceptors and dynamically change environments (#160)

This commit is contained in:
Brian Yencho
2023-10-25 14:24:02 -05:00
committed by GitHub
parent 62fe0ea901
commit 95d7eaf144
18 changed files with 676 additions and 78 deletions

View File

@@ -29,7 +29,6 @@ class IdentityServiceTest : BaseServiceTest() {
private val identityService = IdentityServiceImpl(
api = identityApi,
json = Json,
baseUrl = server.url("/").toString(),
deviceModelProvider = deviceModelProvider,
)

View File

@@ -0,0 +1,36 @@
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
import okhttp3.Request
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Test
class BaseUrlInterceptorTest {
private val baseUrlInterceptor = BaseUrlInterceptor()
@Test
fun `intercept with a null base URL should proceed with the original request`() {
val request = Request.Builder().url("http://www.fake.com/").build()
val chain = FakeInterceptorChain(request)
val response = baseUrlInterceptor.intercept(chain)
assertEquals(request, response.request)
assertEquals("http", response.request.url.scheme)
assertEquals("www.fake.com", response.request.url.host)
}
@Test
fun `intercept with a non-null base URL should update the base URL used by the request`() {
baseUrlInterceptor.baseUrl = "https://api.bitwarden.com"
val request = Request.Builder().url("http://www.fake.com/").build()
val chain = FakeInterceptorChain(request)
val response = baseUrlInterceptor.intercept(chain)
assertNotEquals(request, response.request)
assertEquals("https", response.request.url.scheme)
assertEquals("api.bitwarden.com", response.request.url.host)
}
}

View File

@@ -0,0 +1,101 @@
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class BaseUrlInterceptorsTest {
private val baseUrlInterceptors = BaseUrlInterceptors()
@Test
fun `the default environment should be US and all interceptors should have the correct URLs`() {
assertEquals(
Environment.Us,
baseUrlInterceptors.environment,
)
assertEquals(
"https://vault.bitwarden.com/api",
baseUrlInterceptors.apiInterceptor.baseUrl,
)
assertEquals(
"https://vault.bitwarden.com/identity",
baseUrlInterceptors.identityInterceptor.baseUrl,
)
assertEquals(
"https://vault.bitwarden.com/events",
baseUrlInterceptors.eventsInterceptor.baseUrl,
)
}
@Suppress("MaxLineLength")
@Test
fun `setting the environment should update all the interceptors correctly for a non-blank base URL`() {
baseUrlInterceptors.environment = Environment.Eu
assertEquals(
"https://vault.bitwarden.eu/api",
baseUrlInterceptors.apiInterceptor.baseUrl,
)
assertEquals(
"https://vault.bitwarden.eu/identity",
baseUrlInterceptors.identityInterceptor.baseUrl,
)
assertEquals(
"https://vault.bitwarden.eu/events",
baseUrlInterceptors.eventsInterceptor.baseUrl,
)
}
@Suppress("MaxLineLength")
@Test
fun `setting the environment should update all the interceptors correctly for a blank base URL and all URLs filled`() {
baseUrlInterceptors.environment = Environment.SelfHosted(
environmentUrlData = EnvironmentUrlDataJson(
base = " ",
api = "https://api.com",
identity = "https://identity.com",
events = "https://events.com",
),
)
assertEquals(
"https://api.com",
baseUrlInterceptors.apiInterceptor.baseUrl,
)
assertEquals(
"https://identity.com",
baseUrlInterceptors.identityInterceptor.baseUrl,
)
assertEquals(
"https://events.com",
baseUrlInterceptors.eventsInterceptor.baseUrl,
)
}
@Suppress("MaxLineLength")
@Test
fun `setting the environment should update all the interceptors correctly for a blank base URL and some or all URLs absent`() {
baseUrlInterceptors.environment = Environment.SelfHosted(
environmentUrlData = EnvironmentUrlDataJson(
base = " ",
api = "",
identity = "",
icon = " ",
),
)
assertEquals(
"https://api.bitwarden.com",
baseUrlInterceptors.apiInterceptor.baseUrl,
)
assertEquals(
"https://identity.bitwarden.com",
baseUrlInterceptors.identityInterceptor.baseUrl,
)
assertEquals(
"https://events.bitwarden.com",
baseUrlInterceptors.eventsInterceptor.baseUrl,
)
}
}

View File

@@ -0,0 +1,155 @@
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import okhttp3.Interceptor
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import retrofit2.Retrofit
import retrofit2.create
import retrofit2.http.GET
class RetrofitsTest {
private val authTokenInterceptor = mockk<AuthTokenInterceptor> {
mockIntercept { isAuthInterceptorCalled = true }
}
private val baseUrlInterceptors = mockk<BaseUrlInterceptors> {
every { apiInterceptor } returns mockk {
mockIntercept { isApiInterceptorCalled = true }
}
every { identityInterceptor } returns mockk {
mockIntercept { isIdentityInterceptorCalled = true }
}
every { eventsInterceptor } returns mockk {
mockIntercept { isEventsInterceptorCalled = true }
}
}
private val json = Json
private val server = MockWebServer()
private val retrofits = RetrofitsImpl(
authTokenInterceptor = authTokenInterceptor,
baseUrlInterceptors = baseUrlInterceptors,
json = json,
)
private var isAuthInterceptorCalled = false
private var isApiInterceptorCalled = false
private var isIdentityInterceptorCalled = false
private var isEventsInterceptorCalled = false
@Before
fun setUp() {
server.start()
}
@After
fun tearDown() {
server.shutdown()
}
@Test
fun `authenticatedApiRetrofit should invoke the correct interceptors`() = runBlocking {
val testApi = retrofits
.authenticatedApiRetrofit
.createMockRetrofit()
.create<TestApi>()
server.enqueue(MockResponse().setBody("""{}"""))
testApi.test()
assertTrue(isAuthInterceptorCalled)
assertTrue(isApiInterceptorCalled)
assertFalse(isIdentityInterceptorCalled)
assertFalse(isEventsInterceptorCalled)
}
@Test
fun `unauthenticatedApiRetrofit should invoke the correct interceptors`() = runBlocking {
val testApi = retrofits
.unauthenticatedApiRetrofit
.createMockRetrofit()
.create<TestApi>()
server.enqueue(MockResponse().setBody("""{}"""))
testApi.test()
assertFalse(isAuthInterceptorCalled)
assertTrue(isApiInterceptorCalled)
assertFalse(isIdentityInterceptorCalled)
assertFalse(isEventsInterceptorCalled)
}
@Test
fun `unauthenticatedIdentityRetrofit should invoke the correct interceptors`() = runBlocking {
val testApi = retrofits
.unauthenticatedIdentityRetrofit
.createMockRetrofit()
.create<TestApi>()
server.enqueue(MockResponse().setBody("""{}"""))
testApi.test()
assertFalse(isAuthInterceptorCalled)
assertFalse(isApiInterceptorCalled)
assertTrue(isIdentityInterceptorCalled)
assertFalse(isEventsInterceptorCalled)
}
@Test
fun `staticRetrofitBuilder should invoke the correct interceptors`() = runBlocking {
val testApi = retrofits
.staticRetrofitBuilder
.baseUrl(server.url("/").toString())
.build()
.createMockRetrofit()
.create<TestApi>()
server.enqueue(MockResponse().setBody("""{}"""))
testApi.test()
assertFalse(isAuthInterceptorCalled)
assertFalse(isApiInterceptorCalled)
assertFalse(isIdentityInterceptorCalled)
assertFalse(isEventsInterceptorCalled)
}
private fun Retrofit.createMockRetrofit(): Retrofit =
this
.newBuilder()
.baseUrl(server.url("/").toString())
.build()
}
interface TestApi {
@GET("/test")
suspend fun test(): JsonObject
}
/**
* Mocks the given [Interceptor] such that the [Interceptor.intercept] is a no-op but triggers the
* [isCalledCallback].
*/
private fun Interceptor.mockIntercept(isCalledCallback: () -> Unit) {
val chainSlot = slot<Interceptor.Chain>()
every { intercept(capture(chainSlot)) } answers {
isCalledCallback()
val chain = chainSlot.captured
chain.proceed(chain.request())
}
}

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.repository
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import io.mockk.every
import io.mockk.mockk
@@ -28,6 +29,7 @@ class NetworkConfigRepositoryTest {
}
private val authTokenInterceptor = AuthTokenInterceptor()
private val baseUrlInterceptors = BaseUrlInterceptors()
private lateinit var networkConfigRepository: NetworkConfigRepository
@@ -37,6 +39,7 @@ class NetworkConfigRepositoryTest {
authRepository = authRepository,
authTokenInterceptor = authTokenInterceptor,
environmentRepository = environmentRepository,
baseUrlInterceptors = baseUrlInterceptors,
dispatcher = UnconfinedTestDispatcher(),
)
}
@@ -55,4 +58,19 @@ class NetworkConfigRepositoryTest {
mutableAuthStateFlow.value = AuthState.Unauthenticated
assertNull(authTokenInterceptor.authToken)
}
@Test
fun `changes in the Environment should update the BaseUrlInterceptors`() {
mutableEnvironmentStateFlow.value = Environment.Us
assertEquals(
Environment.Us,
baseUrlInterceptors.environment,
)
mutableEnvironmentStateFlow.value = Environment.Eu
assertEquals(
Environment.Eu,
baseUrlInterceptors.environment,
)
}
}

View File

@@ -0,0 +1,30 @@
package com.x8bit.bitwarden.data.platform.util
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
class StringExtensionsTest {
@Test
fun `orNullIfBlank returns null for a null String`() {
assertNull((null as String?).orNullIfBlank())
}
@Test
fun `orNullIfBlank returns null for an empty String`() {
assertNull("".orNullIfBlank())
}
@Test
fun `orNullIfBlank returns null for a blank String`() {
assertNull(" ".orNullIfBlank())
}
@Test
fun `orNullIfBlank returns the original value for a non-blank String`() {
assertEquals(
"test",
"test".orNullIfBlank(),
)
}
}