Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BWA-86] Debug Menu #2 - config service #274

Merged
merged 21 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
15316a3
[BWA-86] Add Retrofit and all needed dependencies to be able to do seโ€ฆ
andrebispo5 Nov 4, 2024
0413cb6
[BWA-86] Add tests to network layer
andrebispo5 Nov 4, 2024
0a412a5
[BWA-86] lint
andrebispo5 Nov 4, 2024
3246c88
[BWA-86] lint
andrebispo5 Nov 4, 2024
2e587da
[BWA-86] Fetch server config data api and store it locally
andrebispo5 Nov 5, 2024
4572af3
[BWA-86] Add retrofit libs
andrebispo5 Nov 5, 2024
38e1325
[BWA-86] Add fake server config repository for tests
andrebispo5 Nov 5, 2024
6fb26c3
[BWA-86] Add missing test classes
andrebispo5 Nov 5, 2024
8aca95b
[BWA-86] lint
andrebispo5 Nov 5, 2024
a291a8f
[BWA-86] Fix retrofit libs
andrebispo5 Nov 5, 2024
0a99d65
Merge branch 'main' into bwa-86/network-layer-retrofit
andrebispo5 Nov 12, 2024
c855dae
Merge branch 'bwa-86/network-layer-retrofit' into bwa-86/config-service
andrebispo5 Nov 12, 2024
6df2149
[BWA-86] Add kdoc for ConfigService
andrebispo5 Nov 12, 2024
dd661ad
[BWA-86] Omit ResultCall from coverage
andrebispo5 Nov 12, 2024
88f51c2
Merge branch 'bwa-86/network-layer-retrofit' into bwa-86/config-service
andrebispo5 Nov 12, 2024
7348997
[BWA-86] Remove server default endpoints
andrebispo5 Nov 12, 2024
53754af
Merge branch 'bwa-86/network-layer-retrofit' into bwa-86/config-service
andrebispo5 Nov 12, 2024
c36310d
[BWA-86] Fix import for retrofit create
andrebispo5 Nov 12, 2024
e7bb0dc
[BWA-86] Remove identity and events related code from network layer
andrebispo5 Nov 18, 2024
a46e96f
Merge branch 'bwa-86/network-layer-retrofit' into bwa-86/config-service
andrebispo5 Nov 18, 2024
72273a2
Merge branch 'main' into bwa-86/config-service
andrebispo5 Nov 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,9 @@ dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.square.okhttp)
implementation(libs.square.okhttp.logging)
implementation(platform(libs.square.retrofit.bom))
implementation(libs.square.retrofit)
implementation(libs.square.retrofit.kotlinx.serialization)
implementation(libs.zxing.zxing.core)

// For now we are restricted to running Compose tests for debug builds only
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.bitwarden.authenticator.data.auth.datasource.disk.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Represents URLs for various Bitwarden domains.
*
* @property base The overall base URL.
* @property api Separate base URL for the "/api" domain (if applicable).
* @property identity Separate base URL for the "/identity" domain (if applicable).
* @property icon Separate base URL for the icon domain (if applicable).
* @property notifications Separate base URL for the notifications domain (if applicable).
* @property webVault Separate base URL for the web vault domain (if applicable).
* @property events Separate base URL for the events domain (if applicable).
*/
@Serializable
data class EnvironmentUrlDataJson(
@SerialName("base")
val base: String,

@SerialName("api")
val api: String? = null,

@SerialName("identity")
val identity: String? = null,

@SerialName("icons")
val icon: String? = null,

@SerialName("notifications")
val notifications: String? = null,

@SerialName("webVault")
val webVault: String? = null,

@SerialName("events")
val events: String? = null,
) {
@Suppress("UndocumentedPublicClass")
companion object {
/**
* Default [EnvironmentUrlDataJson] for the US region.
*/
val DEFAULT_US: EnvironmentUrlDataJson =
EnvironmentUrlDataJson(base = "https://vault.bitwarden.com")

/**
* Default [EnvironmentUrlDataJson] for the EU region.
*/
val DEFAULT_EU: EnvironmentUrlDataJson =
EnvironmentUrlDataJson(base = "https://vault.bitwarden.eu")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.bitwarden.authenticator.data.platform.datasource.disk

import com.bitwarden.authenticator.data.platform.datasource.disk.model.ServerConfig
import kotlinx.coroutines.flow.Flow

/**
* Primary access point for server configuration-related disk information.
*/
interface ConfigDiskSource {

/**
* The currently persisted [ServerConfig] (or `null` if not set).
*/
var serverConfig: ServerConfig?

/**
* Emits updates that track [ServerConfig]. This will replay the last known value,
* if any.
*/
val serverConfigFlow: Flow<ServerConfig?>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.bitwarden.authenticator.data.platform.datasource.disk

import android.content.SharedPreferences
import com.bitwarden.authenticator.data.platform.datasource.disk.BaseDiskSource.Companion.BASE_KEY
import com.bitwarden.authenticator.data.platform.datasource.disk.model.ServerConfig
import com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
import com.bitwarden.authenticator.data.platform.util.decodeFromStringOrNull
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

private const val SERVER_CONFIGURATIONS = "$BASE_KEY:serverConfigurations"

/**
* Primary implementation of [ConfigDiskSource].
*/
class ConfigDiskSourceImpl(
sharedPreferences: SharedPreferences,
private val json: Json,
) : BaseDiskSource(sharedPreferences = sharedPreferences),
ConfigDiskSource {

override var serverConfig: ServerConfig?
get() = getString(key = SERVER_CONFIGURATIONS)?.let { json.decodeFromStringOrNull(it) }
set(value) {
putString(
key = SERVER_CONFIGURATIONS,
value = value?.let { json.encodeToString(it) },
)
mutableServerConfigFlow.tryEmit(value)
}

override val serverConfigFlow: Flow<ServerConfig?>
get() = mutableServerConfigFlow.onSubscription { emit(serverConfig) }

private val mutableServerConfigFlow = bufferedMutableSharedFlow<ServerConfig?>(replay = 1)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.bitwarden.authenticator.data.platform.datasource.disk.di

import android.content.SharedPreferences
import com.bitwarden.authenticator.data.platform.datasource.di.UnencryptedPreferences
import com.bitwarden.authenticator.data.platform.datasource.disk.ConfigDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.ConfigDiskSourceImpl
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagDiskSourceImpl
import com.bitwarden.authenticator.data.platform.datasource.disk.SettingsDiskSource
Expand All @@ -20,6 +22,17 @@ import javax.inject.Singleton
*/
object PlatformDiskModule {

@Provides
@Singleton
fun provideConfigDiskSource(
@UnencryptedPreferences sharedPreferences: SharedPreferences,
json: Json,
): ConfigDiskSource =
ConfigDiskSourceImpl(
sharedPreferences = sharedPreferences,
json = json,
)

@Provides
@Singleton
fun provideSettingsDiskSource(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.bitwarden.authenticator.data.platform.datasource.disk.model

import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* A higher-level wrapper around [ConfigResponseJson] that provides a timestamp
* to check if a sync is necessary
*
* @property lastSync The [Long] of the last sync.
* @property serverData The raw [ConfigResponseJson] that contains specific data of the
* server configuration
*/
@Serializable
data class ServerConfig(
@SerialName("lastSync")
val lastSync: Long,

@SerialName("serverData")
val serverData: ConfigResponseJson,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.bitwarden.authenticator.data.platform.datasource.network.api

import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
import retrofit2.http.GET

/**
* This interface defines the API service for fetching configuration data.
*/
interface ConfigApi {

@GET("config")
suspend fun getConfig(): Result<ConfigResponseJson>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
@file:OmitFromCoverage

package com.bitwarden.authenticator.data.platform.datasource.network.core

import com.bitwarden.authenticator.data.platform.annotation.OmitFromCoverage
import com.bitwarden.authenticator.data.platform.util.asFailure
import com.bitwarden.authenticator.data.platform.util.asSuccess
import okhttp3.Request
import okio.IOException
import okio.Timeout
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.lang.reflect.Type

/**
* The integer code value for a "No Content" response.
*/
private const val NO_CONTENT_RESPONSE_CODE: Int = 204

/**
* A [Call] for wrapping a network request into a [Result].
*/
@Suppress("TooManyFunctions")
class ResultCall<T>(
private val backingCall: Call<T>,
private val successType: Type,
) : Call<Result<T>> {
override fun cancel(): Unit = backingCall.cancel()

override fun clone(): Call<Result<T>> = ResultCall(backingCall, successType)

override fun enqueue(callback: Callback<Result<T>>): Unit = backingCall.enqueue(
object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
callback.onResponse(this@ResultCall, Response.success(response.toResult()))
}

override fun onFailure(call: Call<T>, t: Throwable) {
callback.onResponse(this@ResultCall, Response.success(t.toFailure()))
}
},
)

@Suppress("TooGenericExceptionCaught")
override fun execute(): Response<Result<T>> =
try {
Response.success(
backingCall
.execute()
.toResult(),
)
} catch (ioException: IOException) {
Response.success(ioException.toFailure())
} catch (runtimeException: RuntimeException) {
Response.success(runtimeException.toFailure())
}

override fun isCanceled(): Boolean = backingCall.isCanceled

override fun isExecuted(): Boolean = backingCall.isExecuted

override fun request(): Request = backingCall.request()

override fun timeout(): Timeout = backingCall.timeout()

/**
* Synchronously send the request and return its response as a [Result].
*/
fun executeForResult(): Result<T> = requireNotNull(execute().body())

private fun Throwable.toFailure(): Result<T> =
this
.also {
// We rebuild the URL without query params, we do not want to log those
val url = backingCall.request().url.toUrl().run { "$protocol://$authority$path" }
}
.asFailure()

private fun Response<T>.toResult(): Result<T> =
if (!this.isSuccessful) {
HttpException(this).toFailure()
} else {
val body = this.body()
@Suppress("UNCHECKED_CAST")
when {
// We got a nonnull T as the body, just return it.
body != null -> body.asSuccess()
// We expected the body to be null since the successType is Unit, just return Unit.
successType == Unit::class.java -> (Unit as T).asSuccess()
// We allow null for 204's, just return null.
this.code() == NO_CONTENT_RESPONSE_CODE -> (null as T).asSuccess()
// All other null bodies result in an error.
else -> IllegalStateException("Unexpected null body!").toFailure()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.bitwarden.authenticator.data.platform.datasource.network.core

import retrofit2.Call
import retrofit2.CallAdapter
import java.lang.reflect.Type

/**
* A [CallAdapter] for wrapping network requests into [kotlin.Result].
*/
class ResultCallAdapter<T>(
private val successType: Type,
) : CallAdapter<T, Call<Result<T>>> {

override fun responseType(): Type = successType
override fun adapt(call: Call<T>): Call<Result<T>> = ResultCall(call, successType)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.bitwarden.authenticator.data.platform.datasource.network.core

import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type

/**
* A [CallAdapter.Factory] for wrapping network requests into [kotlin.Result].
*/
class ResultCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit,
): CallAdapter<*, *>? {
check(returnType is ParameterizedType) { "$returnType must be parameterized" }
val containerType = getParameterUpperBound(0, returnType)

if (getRawType(containerType) != Result::class.java) return null
check(containerType is ParameterizedType) { "$containerType must be parameterized" }

val requestType = getParameterUpperBound(0, containerType)

return if (getRawType(returnType) == Call::class.java) {
ResultCallAdapter<Any>(successType = requestType)
} else {
null
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package com.bitwarden.authenticator.data.platform.datasource.network.di

import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.HeadersInterceptor
import com.bitwarden.authenticator.data.platform.datasource.network.retrofit.Retrofits
import com.bitwarden.authenticator.data.platform.datasource.network.retrofit.RetrofitsImpl
import com.bitwarden.authenticator.data.platform.datasource.network.serializer.ZonedDateTimeSerializer
import com.bitwarden.authenticator.data.platform.datasource.network.service.ConfigService
import com.bitwarden.authenticator.data.platform.datasource.network.service.ConfigServiceImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import retrofit2.create
import javax.inject.Singleton

@Module
Expand All @@ -17,6 +24,29 @@ import javax.inject.Singleton
* It initializes and configures the networking components.
*/
object PlatformNetworkModule {
@Provides
@Singleton
fun providesConfigService(
retrofits: Retrofits,
): ConfigService = ConfigServiceImpl(retrofits.unauthenticatedApiRetrofit.create())

@Provides
@Singleton
fun providesHeadersInterceptor(): HeadersInterceptor = HeadersInterceptor()

@Provides
@Singleton
fun provideRetrofits(
baseUrlInterceptors: BaseUrlInterceptors,
headersInterceptor: HeadersInterceptor,
json: Json,
): Retrofits =
RetrofitsImpl(
baseUrlInterceptors = baseUrlInterceptors,
headersInterceptor = headersInterceptor,
json = json,
)

@Provides
@Singleton
fun providesJson(): Json = Json {
Expand Down
Loading