Merge pull request #115 from kirmanak/authorization
Use API tokens to authenticate application
This commit is contained in:
@@ -15,8 +15,8 @@ plugins {
|
|||||||
android {
|
android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "gq.kirmanak.mealient"
|
applicationId = "gq.kirmanak.mealient"
|
||||||
versionCode = 24
|
versionCode = 25
|
||||||
versionName = "0.3.9"
|
versionName = "0.3.10"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import android.app.Application
|
|||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import gq.kirmanak.mealient.architecture.configuration.BuildConfiguration
|
import gq.kirmanak.mealient.architecture.configuration.BuildConfiguration
|
||||||
|
import gq.kirmanak.mealient.data.migration.MigrationDetector
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
@@ -16,9 +21,15 @@ class App : Application() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var buildConfiguration: BuildConfiguration
|
lateinit var buildConfiguration: BuildConfiguration
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var migrationDetector: MigrationDetector
|
||||||
|
|
||||||
|
private val appCoroutineScope = CoroutineScope(Dispatchers.Main + Job())
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
logger.v { "onCreate() called" }
|
logger.v { "onCreate() called" }
|
||||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||||
|
appCoroutineScope.launch { migrationDetector.executeMigrations() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,6 @@ interface AuthDataSource {
|
|||||||
* Tries to acquire authentication token using the provided credentials
|
* Tries to acquire authentication token using the provided credentials
|
||||||
*/
|
*/
|
||||||
suspend fun authenticate(username: String, password: String): String
|
suspend fun authenticate(username: String, password: String): String
|
||||||
|
|
||||||
|
suspend fun createApiToken(name: String): String
|
||||||
}
|
}
|
||||||
@@ -10,9 +10,5 @@ interface AuthRepo {
|
|||||||
|
|
||||||
suspend fun getAuthHeader(): String?
|
suspend fun getAuthHeader(): String?
|
||||||
|
|
||||||
suspend fun requireAuthHeader(): String
|
|
||||||
|
|
||||||
suspend fun logout()
|
suspend fun logout()
|
||||||
|
|
||||||
suspend fun invalidateAuthHeader()
|
|
||||||
}
|
}
|
||||||
@@ -9,12 +9,4 @@ interface AuthStorage {
|
|||||||
suspend fun setAuthHeader(authHeader: String?)
|
suspend fun setAuthHeader(authHeader: String?)
|
||||||
|
|
||||||
suspend fun getAuthHeader(): String?
|
suspend fun getAuthHeader(): String?
|
||||||
|
|
||||||
suspend fun setEmail(email: String?)
|
|
||||||
|
|
||||||
suspend fun getEmail(): String?
|
|
||||||
|
|
||||||
suspend fun setPassword(password: String?)
|
|
||||||
|
|
||||||
suspend fun getPassword(): String?
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,9 @@ import gq.kirmanak.mealient.data.auth.AuthDataSource
|
|||||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
||||||
import gq.kirmanak.mealient.data.baseurl.ServerVersion
|
import gq.kirmanak.mealient.data.baseurl.ServerVersion
|
||||||
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
|
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
|
||||||
|
import gq.kirmanak.mealient.datasource.v0.models.CreateApiTokenRequestV0
|
||||||
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
|
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -15,14 +17,20 @@ class AuthDataSourceImpl @Inject constructor(
|
|||||||
private val v1Source: MealieDataSourceV1,
|
private val v1Source: MealieDataSourceV1,
|
||||||
) : AuthDataSource {
|
) : AuthDataSource {
|
||||||
|
|
||||||
|
private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion()
|
||||||
|
|
||||||
|
private suspend fun getUrl(): String = serverInfoRepo.requireUrl()
|
||||||
|
|
||||||
override suspend fun authenticate(
|
override suspend fun authenticate(
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
): String {
|
): String = when (getVersion()) {
|
||||||
val baseUrl = serverInfoRepo.requireUrl()
|
ServerVersion.V0 -> v0Source.authenticate(getUrl(), username, password)
|
||||||
return when (serverInfoRepo.getVersion()) {
|
ServerVersion.V1 -> v1Source.authenticate(getUrl(), username, password)
|
||||||
ServerVersion.V0 -> v0Source.authenticate(baseUrl, username, password)
|
}
|
||||||
ServerVersion.V1 -> v1Source.authenticate(baseUrl, username, password)
|
|
||||||
}
|
override suspend fun createApiToken(name: String): String = when (getVersion()) {
|
||||||
|
ServerVersion.V0 -> v0Source.createApiToken(getUrl(), CreateApiTokenRequestV0(name))
|
||||||
|
ServerVersion.V1 -> v1Source.createApiToken(getUrl(), CreateApiTokenRequestV1(name)).token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ package gq.kirmanak.mealient.data.auth.impl
|
|||||||
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||||
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
import gq.kirmanak.mealient.datasource.AuthenticationProvider
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -15,7 +15,7 @@ class AuthRepoImpl @Inject constructor(
|
|||||||
private val authStorage: AuthStorage,
|
private val authStorage: AuthStorage,
|
||||||
private val authDataSource: AuthDataSource,
|
private val authDataSource: AuthDataSource,
|
||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
) : AuthRepo {
|
) : AuthRepo, AuthenticationProvider {
|
||||||
|
|
||||||
override val isAuthorizedFlow: Flow<Boolean>
|
override val isAuthorizedFlow: Flow<Boolean>
|
||||||
get() = authStorage.authHeaderFlow.map { it != null }
|
get() = authStorage.authHeaderFlow.map { it != null }
|
||||||
@@ -23,34 +23,20 @@ class AuthRepoImpl @Inject constructor(
|
|||||||
override suspend fun authenticate(email: String, password: String) {
|
override suspend fun authenticate(email: String, password: String) {
|
||||||
logger.v { "authenticate() called with: email = $email, password = $password" }
|
logger.v { "authenticate() called with: email = $email, password = $password" }
|
||||||
val token = authDataSource.authenticate(email, password)
|
val token = authDataSource.authenticate(email, password)
|
||||||
val header = AUTH_HEADER_FORMAT.format(token)
|
authStorage.setAuthHeader(AUTH_HEADER_FORMAT.format(token))
|
||||||
authStorage.setAuthHeader(header)
|
val apiToken = authDataSource.createApiToken(API_TOKEN_NAME)
|
||||||
authStorage.setEmail(email)
|
authStorage.setAuthHeader(AUTH_HEADER_FORMAT.format(apiToken))
|
||||||
authStorage.setPassword(password)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAuthHeader(): String? = authStorage.getAuthHeader()
|
override suspend fun getAuthHeader(): String? = authStorage.getAuthHeader()
|
||||||
|
|
||||||
override suspend fun requireAuthHeader(): String = checkNotNull(getAuthHeader()) {
|
|
||||||
"Auth header is null when it was required"
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun logout() {
|
override suspend fun logout() {
|
||||||
logger.v { "logout() called" }
|
logger.v { "logout() called" }
|
||||||
authStorage.setEmail(null)
|
|
||||||
authStorage.setPassword(null)
|
|
||||||
authStorage.setAuthHeader(null)
|
authStorage.setAuthHeader(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun invalidateAuthHeader() {
|
|
||||||
logger.v { "invalidateAuthHeader() called" }
|
|
||||||
val email = authStorage.getEmail() ?: return
|
|
||||||
val password = authStorage.getPassword() ?: return
|
|
||||||
runCatchingExceptCancel { authenticate(email, password) }
|
|
||||||
.onFailure { logout() } // Clear all known values to avoid reusing them
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val AUTH_HEADER_FORMAT = "Bearer %s"
|
private const val AUTH_HEADER_FORMAT = "Bearer %s"
|
||||||
|
private const val API_TOKEN_NAME = "Mealient"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,14 +32,6 @@ class AuthStorageImpl @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun getAuthHeader(): String? = getString(AUTH_HEADER_KEY)
|
override suspend fun getAuthHeader(): String? = getString(AUTH_HEADER_KEY)
|
||||||
|
|
||||||
override suspend fun setEmail(email: String?) = putString(EMAIL_KEY, email)
|
|
||||||
|
|
||||||
override suspend fun getEmail(): String? = getString(EMAIL_KEY)
|
|
||||||
|
|
||||||
override suspend fun setPassword(password: String?) = putString(PASSWORD_KEY, password)
|
|
||||||
|
|
||||||
override suspend fun getPassword(): String? = getString(PASSWORD_KEY)
|
|
||||||
|
|
||||||
private suspend fun putString(
|
private suspend fun putString(
|
||||||
key: String,
|
key: String,
|
||||||
value: String?
|
value: String?
|
||||||
@@ -57,11 +49,5 @@ class AuthStorageImpl @Inject constructor(
|
|||||||
companion object {
|
companion object {
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
const val AUTH_HEADER_KEY = "authHeader"
|
const val AUTH_HEADER_KEY = "authHeader"
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
const val EMAIL_KEY = "email"
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
const val PASSWORD_KEY = "password"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package gq.kirmanak.mealient.data.configuration
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.BuildConfig
|
||||||
|
import gq.kirmanak.mealient.architecture.configuration.BuildConfiguration
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class BuildConfigurationImpl @Inject constructor() : BuildConfiguration {
|
||||||
|
|
||||||
|
@get:JvmName("_isDebug")
|
||||||
|
private val isDebug by lazy { BuildConfig.DEBUG }
|
||||||
|
|
||||||
|
private val versionCode by lazy { BuildConfig.VERSION_CODE }
|
||||||
|
|
||||||
|
override fun isDebug(): Boolean = isDebug
|
||||||
|
|
||||||
|
override fun versionCode(): Int = versionCode
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package gq.kirmanak.mealient.data.migration
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
|
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
||||||
|
import gq.kirmanak.mealient.datastore.DataStoreModule.Companion.ENCRYPTED
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class From24AuthMigrationExecutor @Inject constructor(
|
||||||
|
@Named(ENCRYPTED) private val sharedPreferences: SharedPreferences,
|
||||||
|
private val authRepo: AuthRepo,
|
||||||
|
private val logger: Logger,
|
||||||
|
) : MigrationExecutor {
|
||||||
|
|
||||||
|
override val migratingFrom: Int = 24
|
||||||
|
|
||||||
|
override suspend fun executeMigration() {
|
||||||
|
logger.v { "executeMigration() was called" }
|
||||||
|
val email = sharedPreferences.getString(EMAIL_KEY, null)
|
||||||
|
val password = sharedPreferences.getString(PASSWORD_KEY, null)
|
||||||
|
if (email != null && password != null) {
|
||||||
|
runCatchingExceptCancel { authRepo.authenticate(email, password) }
|
||||||
|
.onFailure { logger.e(it) { "API token creation failed" } }
|
||||||
|
.onSuccess { logger.i { "Created API token during migration" } }
|
||||||
|
}
|
||||||
|
sharedPreferences.edit {
|
||||||
|
remove(EMAIL_KEY)
|
||||||
|
remove(PASSWORD_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val EMAIL_KEY = "email"
|
||||||
|
private const val PASSWORD_KEY = "password"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package gq.kirmanak.mealient.data.migration
|
||||||
|
|
||||||
|
interface MigrationDetector {
|
||||||
|
|
||||||
|
suspend fun executeMigrations()
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package gq.kirmanak.mealient.data.migration
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.architecture.configuration.BuildConfiguration
|
||||||
|
import gq.kirmanak.mealient.data.storage.PreferencesStorage
|
||||||
|
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class MigrationDetectorImpl @Inject constructor(
|
||||||
|
private val preferencesStorage: PreferencesStorage,
|
||||||
|
private val migrationExecutors: Set<@JvmSuppressWildcards MigrationExecutor>,
|
||||||
|
private val buildConfiguration: BuildConfiguration,
|
||||||
|
private val logger: Logger,
|
||||||
|
) : MigrationDetector {
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun executeMigrations() {
|
||||||
|
val key = preferencesStorage.lastExecutedMigrationVersionKey
|
||||||
|
|
||||||
|
val lastVersion = preferencesStorage.getValue(key) ?: VERSION_BEFORE_MIGRATION_IMPLEMENTED
|
||||||
|
val currentVersion = buildConfiguration.versionCode()
|
||||||
|
logger.i { "Last migration version is $lastVersion, current is $currentVersion" }
|
||||||
|
|
||||||
|
if (lastVersion != currentVersion) {
|
||||||
|
migrationExecutors
|
||||||
|
.filter { it.migratingFrom >= lastVersion }
|
||||||
|
.forEach { executor ->
|
||||||
|
runCatchingExceptCancel { executor.executeMigration() }
|
||||||
|
.onFailure { logger.e(it) { "Migration executor failed: $executor" } }
|
||||||
|
.onSuccess { logger.i { "Migration executor succeeded: $executor" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preferencesStorage.storeValues(Pair(key, currentVersion))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val VERSION_BEFORE_MIGRATION_IMPLEMENTED = 24
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package gq.kirmanak.mealient.data.migration
|
||||||
|
|
||||||
|
interface MigrationExecutor {
|
||||||
|
|
||||||
|
val migratingFrom: Int
|
||||||
|
|
||||||
|
suspend fun executeMigration()
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package gq.kirmanak.mealient.data.network
|
|||||||
|
|
||||||
import gq.kirmanak.mealient.data.add.AddRecipeDataSource
|
import gq.kirmanak.mealient.data.add.AddRecipeDataSource
|
||||||
import gq.kirmanak.mealient.data.add.AddRecipeInfo
|
import gq.kirmanak.mealient.data.add.AddRecipeInfo
|
||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
|
||||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
||||||
import gq.kirmanak.mealient.data.baseurl.ServerVersion
|
import gq.kirmanak.mealient.data.baseurl.ServerVersion
|
||||||
import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo
|
import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo
|
||||||
@@ -10,8 +9,6 @@ import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
|||||||
import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo
|
import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo
|
||||||
import gq.kirmanak.mealient.data.share.ParseRecipeDataSource
|
import gq.kirmanak.mealient.data.share.ParseRecipeDataSource
|
||||||
import gq.kirmanak.mealient.data.share.ParseRecipeURLInfo
|
import gq.kirmanak.mealient.data.share.ParseRecipeURLInfo
|
||||||
import gq.kirmanak.mealient.datasource.NetworkError
|
|
||||||
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
|
||||||
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
|
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
|
||||||
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
|
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
|
||||||
import gq.kirmanak.mealient.extensions.toFullRecipeInfo
|
import gq.kirmanak.mealient.extensions.toFullRecipeInfo
|
||||||
@@ -20,85 +17,53 @@ import gq.kirmanak.mealient.extensions.toV0Request
|
|||||||
import gq.kirmanak.mealient.extensions.toV1CreateRequest
|
import gq.kirmanak.mealient.extensions.toV1CreateRequest
|
||||||
import gq.kirmanak.mealient.extensions.toV1Request
|
import gq.kirmanak.mealient.extensions.toV1Request
|
||||||
import gq.kirmanak.mealient.extensions.toV1UpdateRequest
|
import gq.kirmanak.mealient.extensions.toV1UpdateRequest
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class MealieDataSourceWrapper @Inject constructor(
|
class MealieDataSourceWrapper @Inject constructor(
|
||||||
private val serverInfoRepo: ServerInfoRepo,
|
private val serverInfoRepo: ServerInfoRepo,
|
||||||
private val authRepo: AuthRepo,
|
|
||||||
private val v0Source: MealieDataSourceV0,
|
private val v0Source: MealieDataSourceV0,
|
||||||
private val v1Source: MealieDataSourceV1,
|
private val v1Source: MealieDataSourceV1,
|
||||||
private val logger: Logger,
|
|
||||||
) : AddRecipeDataSource, RecipeDataSource, ParseRecipeDataSource {
|
) : AddRecipeDataSource, RecipeDataSource, ParseRecipeDataSource {
|
||||||
|
|
||||||
override suspend fun addRecipe(
|
private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion()
|
||||||
recipe: AddRecipeInfo,
|
|
||||||
): String = makeCall { token, url, version ->
|
private suspend fun getUrl(): String = serverInfoRepo.requireUrl()
|
||||||
when (version) {
|
|
||||||
ServerVersion.V0 -> v0Source.addRecipe(url, token, recipe.toV0Request())
|
override suspend fun addRecipe(recipe: AddRecipeInfo): String = when (getVersion()) {
|
||||||
|
ServerVersion.V0 -> v0Source.addRecipe(getUrl(), recipe.toV0Request())
|
||||||
ServerVersion.V1 -> {
|
ServerVersion.V1 -> {
|
||||||
val slug = v1Source.createRecipe(url, token, recipe.toV1CreateRequest())
|
val slug = v1Source.createRecipe(getUrl(), recipe.toV1CreateRequest())
|
||||||
v1Source.updateRecipe(url, token, slug, recipe.toV1UpdateRequest())
|
v1Source.updateRecipe(getUrl(), slug, recipe.toV1UpdateRequest())
|
||||||
slug
|
slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun requestRecipes(
|
override suspend fun requestRecipes(
|
||||||
start: Int,
|
start: Int,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
): List<RecipeSummaryInfo> = makeCall { token, url, version ->
|
): List<RecipeSummaryInfo> = when (getVersion()) {
|
||||||
when (version) {
|
|
||||||
ServerVersion.V0 -> {
|
ServerVersion.V0 -> {
|
||||||
v0Source.requestRecipes(url, token, start, limit).map { it.toRecipeSummaryInfo() }
|
v0Source.requestRecipes(getUrl(), start, limit).map { it.toRecipeSummaryInfo() }
|
||||||
}
|
}
|
||||||
ServerVersion.V1 -> {
|
ServerVersion.V1 -> {
|
||||||
// Imagine start is 30 and limit is 15. It means that we already have page 1 and 2, now we need page 3
|
// Imagine start is 30 and limit is 15. It means that we already have page 1 and 2, now we need page 3
|
||||||
val page = start / limit + 1
|
val page = start / limit + 1
|
||||||
v1Source.requestRecipes(url, token, page, limit).map { it.toRecipeSummaryInfo() }
|
v1Source.requestRecipes(getUrl(), page, limit).map { it.toRecipeSummaryInfo() }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun requestRecipeInfo(
|
override suspend fun requestRecipeInfo(slug: String): FullRecipeInfo = when (getVersion()) {
|
||||||
slug: String,
|
ServerVersion.V0 -> v0Source.requestRecipeInfo(getUrl(), slug).toFullRecipeInfo()
|
||||||
): FullRecipeInfo = makeCall { token, url, version ->
|
ServerVersion.V1 -> v1Source.requestRecipeInfo(getUrl(), slug).toFullRecipeInfo()
|
||||||
when (version) {
|
|
||||||
ServerVersion.V0 -> v0Source.requestRecipeInfo(url, token, slug).toFullRecipeInfo()
|
|
||||||
ServerVersion.V1 -> v1Source.requestRecipeInfo(url, token, slug).toFullRecipeInfo()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun parseRecipeFromURL(
|
override suspend fun parseRecipeFromURL(
|
||||||
parseRecipeURLInfo: ParseRecipeURLInfo,
|
parseRecipeURLInfo: ParseRecipeURLInfo,
|
||||||
): String = makeCall { token, url, version ->
|
): String = when (getVersion()) {
|
||||||
when (version) {
|
ServerVersion.V0 -> v0Source.parseRecipeFromURL(getUrl(), parseRecipeURLInfo.toV0Request())
|
||||||
ServerVersion.V0 -> {
|
ServerVersion.V1 -> v1Source.parseRecipeFromURL(getUrl(), parseRecipeURLInfo.toV1Request())
|
||||||
v0Source.parseRecipeFromURL(url, token, parseRecipeURLInfo.toV0Request())
|
|
||||||
}
|
|
||||||
ServerVersion.V1 -> {
|
|
||||||
v1Source.parseRecipeFromURL(url, token, parseRecipeURLInfo.toV1Request())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend inline fun <T> makeCall(block: (String?, String, ServerVersion) -> T): T {
|
|
||||||
val authHeader = authRepo.getAuthHeader()
|
|
||||||
val url = serverInfoRepo.requireUrl()
|
|
||||||
val version = serverInfoRepo.getVersion()
|
|
||||||
return runCatchingExceptCancel { block(authHeader, url, version) }.getOrElse {
|
|
||||||
if (it is NetworkError.Unauthorized) {
|
|
||||||
logger.e { "Unauthorized, trying to invalidate token" }
|
|
||||||
authRepo.invalidateAuthHeader()
|
|
||||||
// Trying again with new authentication header
|
|
||||||
val newHeader = authRepo.getAuthHeader()
|
|
||||||
logger.e { "New token ${if (newHeader == authHeader) "matches" else "doesn't match"} old token" }
|
|
||||||
if (newHeader == authHeader) throw it else block(newHeader, url, version)
|
|
||||||
} else {
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,8 @@ interface PreferencesStorage {
|
|||||||
|
|
||||||
val isDisclaimerAcceptedKey: Preferences.Key<Boolean>
|
val isDisclaimerAcceptedKey: Preferences.Key<Boolean>
|
||||||
|
|
||||||
|
val lastExecutedMigrationVersionKey: Preferences.Key<Int>
|
||||||
|
|
||||||
suspend fun <T> getValue(key: Preferences.Key<T>): T?
|
suspend fun <T> getValue(key: Preferences.Key<T>): T?
|
||||||
|
|
||||||
suspend fun <T> requireValue(key: Preferences.Key<T>): T
|
suspend fun <T> requireValue(key: Preferences.Key<T>): T
|
||||||
|
|||||||
@@ -4,9 +4,15 @@ import androidx.datastore.core.DataStore
|
|||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -22,6 +28,9 @@ class PreferencesStorageImpl @Inject constructor(
|
|||||||
|
|
||||||
override val isDisclaimerAcceptedKey = booleanPreferencesKey("isDisclaimedAccepted")
|
override val isDisclaimerAcceptedKey = booleanPreferencesKey("isDisclaimedAccepted")
|
||||||
|
|
||||||
|
override val lastExecutedMigrationVersionKey: Preferences.Key<Int> =
|
||||||
|
intPreferencesKey("lastExecutedMigrationVersion")
|
||||||
|
|
||||||
override suspend fun <T> getValue(key: Preferences.Key<T>): T? {
|
override suspend fun <T> getValue(key: Preferences.Key<T>): T? {
|
||||||
val value = dataStore.data.first()[key]
|
val value = dataStore.data.first()[key]
|
||||||
logger.v { "getValue() returned: $value for $key" }
|
logger.v { "getValue() returned: $value for $key" }
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package gq.kirmanak.mealient.architecture
|
package gq.kirmanak.mealient.di
|
||||||
|
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import gq.kirmanak.mealient.architecture.configuration.BuildConfiguration
|
import gq.kirmanak.mealient.architecture.configuration.BuildConfiguration
|
||||||
import gq.kirmanak.mealient.architecture.configuration.BuildConfigurationImpl
|
import gq.kirmanak.mealient.data.configuration.BuildConfigurationImpl
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@@ -14,6 +14,7 @@ import gq.kirmanak.mealient.data.auth.AuthStorage
|
|||||||
import gq.kirmanak.mealient.data.auth.impl.AuthDataSourceImpl
|
import gq.kirmanak.mealient.data.auth.impl.AuthDataSourceImpl
|
||||||
import gq.kirmanak.mealient.data.auth.impl.AuthRepoImpl
|
import gq.kirmanak.mealient.data.auth.impl.AuthRepoImpl
|
||||||
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl
|
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl
|
||||||
|
import gq.kirmanak.mealient.datasource.AuthenticationProvider
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@@ -37,6 +38,10 @@ interface AuthModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
fun bindAuthRepo(authRepo: AuthRepoImpl): AuthRepo
|
fun bindAuthRepo(authRepo: AuthRepoImpl): AuthRepo
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
fun bindAuthProvider(authRepo: AuthRepoImpl): AuthenticationProvider
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
fun bindAuthStorage(authStorageImpl: AuthStorageImpl): AuthStorage
|
fun bindAuthStorage(authStorageImpl: AuthStorageImpl): AuthStorage
|
||||||
|
|||||||
26
app/src/main/java/gq/kirmanak/mealient/di/MigrationModule.kt
Normal file
26
app/src/main/java/gq/kirmanak/mealient/di/MigrationModule.kt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package gq.kirmanak.mealient.di
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import dagger.multibindings.IntoSet
|
||||||
|
import gq.kirmanak.mealient.data.migration.From24AuthMigrationExecutor
|
||||||
|
import gq.kirmanak.mealient.data.migration.MigrationDetector
|
||||||
|
import gq.kirmanak.mealient.data.migration.MigrationDetectorImpl
|
||||||
|
import gq.kirmanak.mealient.data.migration.MigrationExecutor
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface MigrationModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
@IntoSet
|
||||||
|
fun bindFrom24AuthMigrationExecutor(from24AuthMigrationExecutor: From24AuthMigrationExecutor): MigrationExecutor
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
fun bindMigrationDetector(migrationDetectorImpl: MigrationDetectorImpl): MigrationDetector
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
package gq.kirmanak.mealient.ui.recipes.images
|
package gq.kirmanak.mealient.ui.recipes.images
|
||||||
|
|
||||||
import com.bumptech.glide.load.Options
|
import com.bumptech.glide.load.Options
|
||||||
import com.bumptech.glide.load.model.*
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
|
import com.bumptech.glide.load.model.ModelCache
|
||||||
|
import com.bumptech.glide.load.model.ModelLoader
|
||||||
import com.bumptech.glide.load.model.stream.BaseGlideUrlLoader
|
import com.bumptech.glide.load.model.stream.BaseGlideUrlLoader
|
||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
|
||||||
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider
|
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
import gq.kirmanak.mealient.datasource.DataSourceModule.Companion.AUTHORIZATION_HEADER_NAME
|
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -16,7 +16,6 @@ import javax.inject.Singleton
|
|||||||
class RecipeModelLoader private constructor(
|
class RecipeModelLoader private constructor(
|
||||||
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
private val authRepo: AuthRepo,
|
|
||||||
concreteLoader: ModelLoader<GlideUrl, InputStream>,
|
concreteLoader: ModelLoader<GlideUrl, InputStream>,
|
||||||
cache: ModelCache<RecipeSummaryEntity, GlideUrl>,
|
cache: ModelCache<RecipeSummaryEntity, GlideUrl>,
|
||||||
) : BaseGlideUrlLoader<RecipeSummaryEntity>(concreteLoader, cache) {
|
) : BaseGlideUrlLoader<RecipeSummaryEntity>(concreteLoader, cache) {
|
||||||
@@ -25,13 +24,12 @@ class RecipeModelLoader private constructor(
|
|||||||
class Factory @Inject constructor(
|
class Factory @Inject constructor(
|
||||||
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
private val authRepo: AuthRepo,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun build(
|
fun build(
|
||||||
concreteLoader: ModelLoader<GlideUrl, InputStream>,
|
concreteLoader: ModelLoader<GlideUrl, InputStream>,
|
||||||
cache: ModelCache<RecipeSummaryEntity, GlideUrl>,
|
cache: ModelCache<RecipeSummaryEntity, GlideUrl>,
|
||||||
) = RecipeModelLoader(recipeImageUrlProvider, logger, authRepo, concreteLoader, cache)
|
) = RecipeModelLoader(recipeImageUrlProvider, logger, concreteLoader, cache)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,20 +44,4 @@ class RecipeModelLoader private constructor(
|
|||||||
logger.v { "getUrl() called with: model = $model, width = $width, height = $height, options = $options" }
|
logger.v { "getUrl() called with: model = $model, width = $width, height = $height, options = $options" }
|
||||||
return runBlocking { recipeImageUrlProvider.generateImageUrl(model?.imageId) }
|
return runBlocking { recipeImageUrlProvider.generateImageUrl(model?.imageId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getHeaders(
|
|
||||||
model: RecipeSummaryEntity?,
|
|
||||||
width: Int,
|
|
||||||
height: Int,
|
|
||||||
options: Options?
|
|
||||||
): Headers? {
|
|
||||||
val authorization = runBlocking { authRepo.getAuthHeader() }
|
|
||||||
return if (authorization.isNullOrBlank()) {
|
|
||||||
super.getHeaders(model, width, height, options)
|
|
||||||
} else {
|
|
||||||
LazyHeaders.Builder()
|
|
||||||
.setHeader(AUTHORIZATION_HEADER_NAME, authorization)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,8 @@ import gq.kirmanak.mealient.data.auth.AuthRepo
|
|||||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
||||||
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
||||||
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_API_AUTH_HEADER
|
||||||
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_API_TOKEN
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
|
||||||
@@ -51,13 +53,15 @@ class AuthRepoImplTest : BaseUnitTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `when authenticate successfully then saves to storage`() = runTest {
|
fun `when authenticate successfully then saves to storage`() = runTest {
|
||||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
||||||
coEvery { dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) } returns TEST_TOKEN
|
coEvery { dataSource.authenticate(any(), any()) } returns TEST_TOKEN
|
||||||
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
||||||
|
coEvery { dataSource.createApiToken(any()) } returns TEST_API_TOKEN
|
||||||
subject.authenticate(TEST_USERNAME, TEST_PASSWORD)
|
subject.authenticate(TEST_USERNAME, TEST_PASSWORD)
|
||||||
coVerifyAll {
|
coVerify {
|
||||||
|
dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD))
|
||||||
storage.setAuthHeader(TEST_AUTH_HEADER)
|
storage.setAuthHeader(TEST_AUTH_HEADER)
|
||||||
storage.setEmail(TEST_USERNAME)
|
dataSource.createApiToken(eq("Mealient"))
|
||||||
storage.setPassword(TEST_PASSWORD)
|
storage.setAuthHeader(TEST_API_AUTH_HEADER)
|
||||||
}
|
}
|
||||||
confirmVerified(storage)
|
confirmVerified(storage)
|
||||||
}
|
}
|
||||||
@@ -71,50 +75,9 @@ class AuthRepoImplTest : BaseUnitTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when logout then removes email, password and header`() = runTest {
|
fun `when logout expect header removal`() = runTest {
|
||||||
subject.logout()
|
subject.logout()
|
||||||
coVerifyAll {
|
coVerify { storage.setAuthHeader(null) }
|
||||||
storage.setEmail(null)
|
|
||||||
storage.setPassword(null)
|
|
||||||
storage.setAuthHeader(null)
|
|
||||||
}
|
|
||||||
confirmVerified(storage)
|
confirmVerified(storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when invalidate then does not authenticate without email`() = runTest {
|
|
||||||
coEvery { storage.getEmail() } returns null
|
|
||||||
coEvery { storage.getPassword() } returns TEST_PASSWORD
|
|
||||||
subject.invalidateAuthHeader()
|
|
||||||
confirmVerified(dataSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when invalidate then does not authenticate without password`() = runTest {
|
|
||||||
coEvery { storage.getEmail() } returns TEST_USERNAME
|
|
||||||
coEvery { storage.getPassword() } returns null
|
|
||||||
subject.invalidateAuthHeader()
|
|
||||||
confirmVerified(dataSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when invalidate with credentials then calls authenticate`() = runTest {
|
|
||||||
coEvery { storage.getEmail() } returns TEST_USERNAME
|
|
||||||
coEvery { storage.getPassword() } returns TEST_PASSWORD
|
|
||||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
|
||||||
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
|
||||||
coEvery { dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) } returns TEST_TOKEN
|
|
||||||
subject.invalidateAuthHeader()
|
|
||||||
coVerifyAll { dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when invalidate with credentials and auth fails then clears email`() = runTest {
|
|
||||||
coEvery { storage.getEmail() } returns "invalid"
|
|
||||||
coEvery { storage.getPassword() } returns ""
|
|
||||||
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
|
||||||
coEvery { dataSource.authenticate(any(), any()) } throws RuntimeException()
|
|
||||||
subject.invalidateAuthHeader()
|
|
||||||
coVerify { storage.setEmail(null) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -8,13 +8,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||||
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.AUTH_HEADER_KEY
|
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.AUTH_HEADER_KEY
|
||||||
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.EMAIL_KEY
|
|
||||||
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.PASSWORD_KEY
|
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
|
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME
|
|
||||||
import gq.kirmanak.mealient.test.FakeLogger
|
|
||||||
import gq.kirmanak.mealient.test.HiltRobolectricTest
|
import gq.kirmanak.mealient.test.HiltRobolectricTest
|
||||||
import io.mockk.MockKAnnotations
|
import io.mockk.MockKAnnotations
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -32,8 +26,6 @@ class AuthStorageImplTest : HiltRobolectricTest() {
|
|||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
lateinit var context: Context
|
lateinit var context: Context
|
||||||
|
|
||||||
private val logger: Logger = FakeLogger()
|
|
||||||
|
|
||||||
lateinit var subject: AuthStorage
|
lateinit var subject: AuthStorage
|
||||||
|
|
||||||
lateinit var sharedPreferences: SharedPreferences
|
lateinit var sharedPreferences: SharedPreferences
|
||||||
@@ -55,16 +47,4 @@ class AuthStorageImplTest : HiltRobolectricTest() {
|
|||||||
fun `when authHeader is observed then sends null if nothing saved`() = runTest {
|
fun `when authHeader is observed then sends null if nothing saved`() = runTest {
|
||||||
assertThat(subject.authHeaderFlow.first()).isEqualTo(null)
|
assertThat(subject.authHeaderFlow.first()).isEqualTo(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when setEmail then edits shared preferences`() = runTest {
|
|
||||||
subject.setEmail(TEST_USERNAME)
|
|
||||||
assertThat(sharedPreferences.getString(EMAIL_KEY, null)).isEqualTo(TEST_USERNAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when getPassword then reads shared preferences`() = runTest {
|
|
||||||
sharedPreferences.edit(commit = true) { putString(PASSWORD_KEY, TEST_PASSWORD) }
|
|
||||||
assertThat(subject.getPassword()).isEqualTo(TEST_PASSWORD)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ import javax.inject.Inject
|
|||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class DisclaimerStorageImplTest : HiltRobolectricTest() {
|
class DisclaimerStorageImplTest : HiltRobolectricTest() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var subject: DisclaimerStorageImpl
|
lateinit var subject: DisclaimerStorageImpl
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package gq.kirmanak.mealient.data.migration
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
|
import gq.kirmanak.mealient.test.HiltRobolectricTest
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.IOException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@HiltAndroidTest
|
||||||
|
class From24AuthMigrationExecutorTest : HiltRobolectricTest() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ApplicationContext
|
||||||
|
lateinit var context: Context
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var authRepo: AuthRepo
|
||||||
|
|
||||||
|
private lateinit var subject: MigrationExecutor
|
||||||
|
private lateinit var sharedPreferences: SharedPreferences
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockKAnnotations.init(this)
|
||||||
|
sharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE)
|
||||||
|
subject = From24AuthMigrationExecutor(sharedPreferences, authRepo, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when there were email and password expect authentication`() = runTest {
|
||||||
|
sharedPreferences.edit(commit = true) {
|
||||||
|
putString("email", "email_value")
|
||||||
|
putString("password", "pass_value")
|
||||||
|
}
|
||||||
|
subject.executeMigration()
|
||||||
|
coVerify { authRepo.authenticate(eq("email_value"), eq("pass_value")) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when there were email and password expect them gone`() = runTest {
|
||||||
|
sharedPreferences.edit(commit = true) {
|
||||||
|
putString("email", "email_value")
|
||||||
|
putString("password", "pass_value")
|
||||||
|
}
|
||||||
|
subject.executeMigration()
|
||||||
|
assertThat(sharedPreferences.getString("email", null)).isNull()
|
||||||
|
assertThat(sharedPreferences.getString("password", null)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when there is email and password but authenticate fails expect values gone`() = runTest {
|
||||||
|
sharedPreferences.edit(commit = true) {
|
||||||
|
putString("email", "email_value")
|
||||||
|
putString("password", "pass_value")
|
||||||
|
}
|
||||||
|
coEvery { authRepo.authenticate(any(), any()) } throws IOException()
|
||||||
|
subject.executeMigration()
|
||||||
|
assertThat(sharedPreferences.getString("email", null)).isNull()
|
||||||
|
assertThat(sharedPreferences.getString("password", null)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when there was no email and password expect no authentication`() = runTest {
|
||||||
|
subject.executeMigration()
|
||||||
|
coVerify(inverse = true) { authRepo.authenticate(any(), any()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package gq.kirmanak.mealient.data.migration
|
||||||
|
|
||||||
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
|
import gq.kirmanak.mealient.architecture.configuration.BuildConfiguration
|
||||||
|
import gq.kirmanak.mealient.data.storage.PreferencesStorage
|
||||||
|
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class MigrationDetectorImplTest : BaseUnitTest() {
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var buildConfiguration: BuildConfiguration
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var preferencesStorage: PreferencesStorage
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when last version matches current expect no executors to be called`() = runTest {
|
||||||
|
val executors = setOf<MigrationExecutor>(mockk(), mockk())
|
||||||
|
val key = intPreferencesKey("key")
|
||||||
|
every { preferencesStorage.lastExecutedMigrationVersionKey } returns intPreferencesKey("key")
|
||||||
|
coEvery { preferencesStorage.getValue(key) } returns 25
|
||||||
|
coEvery { buildConfiguration.versionCode() } returns 25
|
||||||
|
buildSubject(executors).executeMigrations()
|
||||||
|
executors.forEach {
|
||||||
|
coVerify(inverse = true) { it.migratingFrom }
|
||||||
|
coVerify(inverse = true) { it.executeMigration() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when last version is 24 and current is 25 expect all executors to be checked`() = runTest {
|
||||||
|
val firstExecutor = mockk<MigrationExecutor>()
|
||||||
|
every { firstExecutor.migratingFrom } returns 3
|
||||||
|
|
||||||
|
val secondExecutor = mockk<MigrationExecutor>()
|
||||||
|
every { secondExecutor.migratingFrom } returns 5
|
||||||
|
|
||||||
|
val executors = setOf(firstExecutor, secondExecutor)
|
||||||
|
|
||||||
|
val key = intPreferencesKey("key")
|
||||||
|
every { preferencesStorage.lastExecutedMigrationVersionKey } returns intPreferencesKey("key")
|
||||||
|
|
||||||
|
coEvery { preferencesStorage.getValue(key) } returns 24
|
||||||
|
coEvery { buildConfiguration.versionCode() } returns 25
|
||||||
|
|
||||||
|
buildSubject(executors).executeMigrations()
|
||||||
|
executors.forEach {
|
||||||
|
coVerify { it.migratingFrom }
|
||||||
|
coVerify(inverse = true) { it.executeMigration() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when last version is 24 and current is 25 expect the executor to be called`() = runTest {
|
||||||
|
val firstExecutor = mockk<MigrationExecutor>(relaxUnitFun = true)
|
||||||
|
every { firstExecutor.migratingFrom } returns 24
|
||||||
|
|
||||||
|
val executors = setOf(firstExecutor)
|
||||||
|
|
||||||
|
val key = intPreferencesKey("key")
|
||||||
|
every { preferencesStorage.lastExecutedMigrationVersionKey } returns intPreferencesKey("key")
|
||||||
|
|
||||||
|
coEvery { preferencesStorage.getValue(key) } returns 24
|
||||||
|
coEvery { buildConfiguration.versionCode() } returns 25
|
||||||
|
|
||||||
|
buildSubject(executors).executeMigrations()
|
||||||
|
executors.forEach {
|
||||||
|
coVerify { it.migratingFrom }
|
||||||
|
coVerify { it.executeMigration() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSubject(executors: Set<MigrationExecutor>) = MigrationDetectorImpl(
|
||||||
|
preferencesStorage = preferencesStorage,
|
||||||
|
migrationExecutors = executors,
|
||||||
|
buildConfiguration = buildConfiguration,
|
||||||
|
logger = logger,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package gq.kirmanak.mealient.data.network
|
|||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
||||||
import gq.kirmanak.mealient.datasource.NetworkError
|
|
||||||
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
|
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
|
||||||
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
|
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
|
||||||
@@ -15,7 +14,6 @@ import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_INFO
|
|||||||
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_REQUEST_V0
|
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_REQUEST_V0
|
||||||
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_CREATE_RECIPE_REQUEST_V1
|
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_CREATE_RECIPE_REQUEST_V1
|
||||||
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_FULL_RECIPE_INFO
|
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_FULL_RECIPE_INFO
|
||||||
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_RESPONSE_V0
|
|
||||||
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_RESPONSE_V1
|
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_RESPONSE_V1
|
||||||
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0
|
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0
|
||||||
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1
|
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1
|
||||||
@@ -50,36 +48,14 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
|
|||||||
@Before
|
@Before
|
||||||
override fun setUp() {
|
override fun setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
subject = MealieDataSourceWrapper(serverInfoRepo, authRepo, v0Source, v1Source, logger)
|
subject = MealieDataSourceWrapper(serverInfoRepo, v0Source, v1Source)
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when makeCall fails with Unauthorized expect it to invalidate token`() = runTest {
|
|
||||||
val slug = "porridge"
|
|
||||||
coEvery {
|
|
||||||
v0Source.requestRecipeInfo(any(), isNull(), any())
|
|
||||||
} throws NetworkError.Unauthorized(IOException())
|
|
||||||
coEvery {
|
|
||||||
v0Source.requestRecipeInfo(any(), eq(TEST_AUTH_HEADER), any())
|
|
||||||
} returns PORRIDGE_RECIPE_RESPONSE_V0
|
|
||||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
|
||||||
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
|
||||||
coEvery { authRepo.getAuthHeader() } returns null andThen TEST_AUTH_HEADER
|
|
||||||
|
|
||||||
subject.requestRecipeInfo(slug)
|
|
||||||
|
|
||||||
coVerifySequence {
|
|
||||||
authRepo.getAuthHeader()
|
|
||||||
authRepo.invalidateAuthHeader()
|
|
||||||
authRepo.getAuthHeader()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when server version v1 expect requestRecipeInfo to call v1`() = runTest {
|
fun `when server version v1 expect requestRecipeInfo to call v1`() = runTest {
|
||||||
val slug = "porridge"
|
val slug = "porridge"
|
||||||
coEvery {
|
coEvery {
|
||||||
v1Source.requestRecipeInfo(eq(TEST_BASE_URL), eq(TEST_AUTH_HEADER), eq(slug))
|
v1Source.requestRecipeInfo(eq(TEST_BASE_URL), eq(slug))
|
||||||
} returns PORRIDGE_RECIPE_RESPONSE_V1
|
} returns PORRIDGE_RECIPE_RESPONSE_V1
|
||||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
|
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
|
||||||
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
||||||
@@ -87,7 +63,7 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
|
|||||||
|
|
||||||
val actual = subject.requestRecipeInfo(slug)
|
val actual = subject.requestRecipeInfo(slug)
|
||||||
|
|
||||||
coVerify { v1Source.requestRecipeInfo(eq(TEST_BASE_URL), eq(TEST_AUTH_HEADER), eq(slug)) }
|
coVerify { v1Source.requestRecipeInfo(eq(TEST_BASE_URL), eq(slug)) }
|
||||||
|
|
||||||
assertThat(actual).isEqualTo(PORRIDGE_FULL_RECIPE_INFO)
|
assertThat(actual).isEqualTo(PORRIDGE_FULL_RECIPE_INFO)
|
||||||
}
|
}
|
||||||
@@ -95,7 +71,7 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `when server version v1 expect requestRecipes to call v1`() = runTest {
|
fun `when server version v1 expect requestRecipes to call v1`() = runTest {
|
||||||
coEvery {
|
coEvery {
|
||||||
v1Source.requestRecipes(any(), any(), any(), any())
|
v1Source.requestRecipes(any(), any(), any())
|
||||||
} returns listOf(PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1)
|
} returns listOf(PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1)
|
||||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
|
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
|
||||||
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
||||||
@@ -106,7 +82,7 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
|
|||||||
val page = 5 // 0-9 (1), 10-19 (2), 20-29 (3), 30-39 (4), 40-49 (5)
|
val page = 5 // 0-9 (1), 10-19 (2), 20-29 (3), 30-39 (4), 40-49 (5)
|
||||||
val perPage = 10
|
val perPage = 10
|
||||||
coVerify {
|
coVerify {
|
||||||
v1Source.requestRecipes(eq(TEST_BASE_URL), eq(TEST_AUTH_HEADER), eq(page), eq(perPage))
|
v1Source.requestRecipes(eq(TEST_BASE_URL), eq(page), eq(perPage))
|
||||||
}
|
}
|
||||||
|
|
||||||
assertThat(actual).isEqualTo(listOf(RECIPE_SUMMARY_PORRIDGE_V1))
|
assertThat(actual).isEqualTo(listOf(RECIPE_SUMMARY_PORRIDGE_V1))
|
||||||
@@ -115,7 +91,7 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `when server version v0 expect requestRecipes to call v0`() = runTest {
|
fun `when server version v0 expect requestRecipes to call v0`() = runTest {
|
||||||
coEvery {
|
coEvery {
|
||||||
v0Source.requestRecipes(any(), any(), any(), any())
|
v0Source.requestRecipes(any(), any(), any())
|
||||||
} returns listOf(PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0)
|
} returns listOf(PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0)
|
||||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
||||||
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
||||||
@@ -126,7 +102,7 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
|
|||||||
val actual = subject.requestRecipes(start, limit)
|
val actual = subject.requestRecipes(start, limit)
|
||||||
|
|
||||||
coVerify {
|
coVerify {
|
||||||
v0Source.requestRecipes(eq(TEST_BASE_URL), eq(TEST_AUTH_HEADER), eq(start), eq(limit))
|
v0Source.requestRecipes(eq(TEST_BASE_URL), eq(start), eq(limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
assertThat(actual).isEqualTo(listOf(RECIPE_SUMMARY_PORRIDGE_V0))
|
assertThat(actual).isEqualTo(listOf(RECIPE_SUMMARY_PORRIDGE_V0))
|
||||||
@@ -134,7 +110,7 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
|
|||||||
|
|
||||||
@Test(expected = IOException::class)
|
@Test(expected = IOException::class)
|
||||||
fun `when request fails expect addRecipe to rethrow`() = runTest {
|
fun `when request fails expect addRecipe to rethrow`() = runTest {
|
||||||
coEvery { v0Source.addRecipe(any(), any(), any()) } throws IOException()
|
coEvery { v0Source.addRecipe(any(), any()) } throws IOException()
|
||||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
||||||
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
||||||
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
|
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
|
||||||
@@ -145,7 +121,7 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
|
|||||||
fun `when server version v0 expect addRecipe to call v0`() = runTest {
|
fun `when server version v0 expect addRecipe to call v0`() = runTest {
|
||||||
val slug = "porridge"
|
val slug = "porridge"
|
||||||
|
|
||||||
coEvery { v0Source.addRecipe(any(), any(), any()) } returns slug
|
coEvery { v0Source.addRecipe(any(), any()) } returns slug
|
||||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
||||||
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
||||||
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
|
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
|
||||||
@@ -155,7 +131,6 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
|
|||||||
coVerify {
|
coVerify {
|
||||||
v0Source.addRecipe(
|
v0Source.addRecipe(
|
||||||
eq(TEST_BASE_URL),
|
eq(TEST_BASE_URL),
|
||||||
eq(TEST_AUTH_HEADER),
|
|
||||||
eq(PORRIDGE_ADD_RECIPE_REQUEST_V0),
|
eq(PORRIDGE_ADD_RECIPE_REQUEST_V0),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -167,9 +142,9 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
|
|||||||
fun `when server version v1 expect addRecipe to call v1`() = runTest {
|
fun `when server version v1 expect addRecipe to call v1`() = runTest {
|
||||||
val slug = "porridge"
|
val slug = "porridge"
|
||||||
|
|
||||||
coEvery { v1Source.createRecipe(any(), any(), any()) } returns slug
|
coEvery { v1Source.createRecipe(any(), any()) } returns slug
|
||||||
coEvery {
|
coEvery {
|
||||||
v1Source.updateRecipe(any(), any(), any(), any())
|
v1Source.updateRecipe(any(), any(), any())
|
||||||
} returns PORRIDGE_RECIPE_RESPONSE_V1
|
} returns PORRIDGE_RECIPE_RESPONSE_V1
|
||||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
|
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
|
||||||
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
|
||||||
@@ -180,13 +155,11 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
|
|||||||
coVerifySequence {
|
coVerifySequence {
|
||||||
v1Source.createRecipe(
|
v1Source.createRecipe(
|
||||||
eq(TEST_BASE_URL),
|
eq(TEST_BASE_URL),
|
||||||
eq(TEST_AUTH_HEADER),
|
|
||||||
eq(PORRIDGE_CREATE_RECIPE_REQUEST_V1),
|
eq(PORRIDGE_CREATE_RECIPE_REQUEST_V1),
|
||||||
)
|
)
|
||||||
|
|
||||||
v1Source.updateRecipe(
|
v1Source.updateRecipe(
|
||||||
eq(TEST_BASE_URL),
|
eq(TEST_BASE_URL),
|
||||||
eq(TEST_AUTH_HEADER),
|
|
||||||
eq(slug),
|
eq(slug),
|
||||||
eq(PORRIDGE_UPDATE_RECIPE_REQUEST_V1),
|
eq(PORRIDGE_UPDATE_RECIPE_REQUEST_V1),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ object AuthImplTestData {
|
|||||||
const val TEST_BASE_URL = "https://example.com/"
|
const val TEST_BASE_URL = "https://example.com/"
|
||||||
const val TEST_TOKEN = "TEST_TOKEN"
|
const val TEST_TOKEN = "TEST_TOKEN"
|
||||||
const val TEST_AUTH_HEADER = "Bearer TEST_TOKEN"
|
const val TEST_AUTH_HEADER = "Bearer TEST_TOKEN"
|
||||||
|
const val TEST_API_TOKEN = "TEST_API_TOKEN"
|
||||||
|
const val TEST_API_AUTH_HEADER = "Bearer TEST_API_TOKEN"
|
||||||
const val TEST_VERSION = "v0.5.6"
|
const val TEST_VERSION = "v0.5.6"
|
||||||
val TEST_SERVER_VERSION_V0 = ServerVersion.V0
|
val TEST_SERVER_VERSION_V0 = ServerVersion.V0
|
||||||
val TEST_SERVER_VERSION_V1 = ServerVersion.V1
|
val TEST_SERVER_VERSION_V1 = ServerVersion.V1
|
||||||
|
|||||||
@@ -3,4 +3,6 @@ package gq.kirmanak.mealient.architecture.configuration
|
|||||||
interface BuildConfiguration {
|
interface BuildConfiguration {
|
||||||
|
|
||||||
fun isDebug(): Boolean
|
fun isDebug(): Boolean
|
||||||
|
|
||||||
|
fun versionCode(): Int
|
||||||
}
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package gq.kirmanak.mealient.architecture.configuration
|
|
||||||
|
|
||||||
import gq.kirmanak.mealient.architecture.BuildConfig
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class BuildConfigurationImpl @Inject constructor() : BuildConfiguration {
|
|
||||||
|
|
||||||
@get:JvmName("_isDebug")
|
|
||||||
private val isDebug by lazy { BuildConfig.DEBUG }
|
|
||||||
|
|
||||||
override fun isDebug(): Boolean = isDebug
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource
|
||||||
|
|
||||||
|
interface AuthenticationProvider {
|
||||||
|
|
||||||
|
suspend fun getAuthHeader(): String?
|
||||||
|
|
||||||
|
suspend fun logout()
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,6 +6,12 @@ import dagger.Module
|
|||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import dagger.multibindings.IntoSet
|
||||||
|
import gq.kirmanak.mealient.datasource.impl.AuthInterceptor
|
||||||
|
import gq.kirmanak.mealient.datasource.impl.CacheBuilderImpl
|
||||||
|
import gq.kirmanak.mealient.datasource.impl.NetworkRequestWrapperImpl
|
||||||
|
import gq.kirmanak.mealient.datasource.impl.OkHttpBuilderImpl
|
||||||
|
import gq.kirmanak.mealient.datasource.impl.RetrofitBuilder
|
||||||
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
|
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
|
||||||
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0Impl
|
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0Impl
|
||||||
import gq.kirmanak.mealient.datasource.v0.MealieServiceV0
|
import gq.kirmanak.mealient.datasource.v0.MealieServiceV0
|
||||||
@@ -14,6 +20,7 @@ import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1Impl
|
|||||||
import gq.kirmanak.mealient.datasource.v1.MealieServiceV1
|
import gq.kirmanak.mealient.datasource.v1.MealieServiceV1
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Interceptor
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Converter
|
import retrofit2.Converter
|
||||||
@@ -27,8 +34,6 @@ interface DataSourceModule {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val AUTHORIZATION_HEADER_NAME = "Authorization"
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideJson(): Json = Json {
|
fun provideJson(): Json = Json {
|
||||||
@@ -83,4 +88,9 @@ interface DataSourceModule {
|
|||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
fun bindNetworkRequestWrapper(networkRequestWrapperImpl: NetworkRequestWrapperImpl): NetworkRequestWrapper
|
fun bindNetworkRequestWrapper(networkRequestWrapperImpl: NetworkRequestWrapperImpl): NetworkRequestWrapper
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
@IntoSet
|
||||||
|
fun bindAuthInterceptor(authInterceptor: AuthInterceptor): Interceptor
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource.impl
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import gq.kirmanak.mealient.datasource.AuthenticationProvider
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Provider
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class AuthInterceptor @Inject constructor(
|
||||||
|
private val logger: Logger,
|
||||||
|
private val authenticationProviderProvider: Provider<AuthenticationProvider>,
|
||||||
|
) : Interceptor {
|
||||||
|
|
||||||
|
private val authenticationProvider: AuthenticationProvider
|
||||||
|
get() = authenticationProviderProvider.get()
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
logger.v { "intercept() was called" }
|
||||||
|
val header = getAuthHeader()
|
||||||
|
val request = chain.request().let {
|
||||||
|
if (header == null) it else it.newBuilder().header(HEADER_NAME, header).build()
|
||||||
|
}
|
||||||
|
logger.d { "Sending header $HEADER_NAME=${request.header(HEADER_NAME)}" }
|
||||||
|
return chain.proceed(request).also {
|
||||||
|
logger.v { "Response code is ${it.code}" }
|
||||||
|
if (it.code == 401 && header != null) logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAuthHeader() = runBlocking { authenticationProvider.getAuthHeader() }
|
||||||
|
|
||||||
|
private fun logout() = runBlocking { authenticationProvider.logout() }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@VisibleForTesting
|
||||||
|
const val HEADER_NAME = "Authorization"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package gq.kirmanak.mealient.datasource
|
package gq.kirmanak.mealient.datasource.impl
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.StatFs
|
import android.os.StatFs
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import gq.kirmanak.mealient.datasource.CacheBuilder
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
package gq.kirmanak.mealient.datasource
|
package gq.kirmanak.mealient.datasource.impl
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.datasource.NetworkError
|
||||||
|
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
|
||||||
|
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package gq.kirmanak.mealient.datasource
|
package gq.kirmanak.mealient.datasource.impl
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.datasource.CacheBuilder
|
||||||
|
import gq.kirmanak.mealient.datasource.OkHttpBuilder
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package gq.kirmanak.mealient.datasource
|
package gq.kirmanak.mealient.datasource.impl
|
||||||
|
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package gq.kirmanak.mealient.datasource.v0
|
package gq.kirmanak.mealient.datasource.v0
|
||||||
|
|
||||||
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0
|
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0
|
||||||
|
import gq.kirmanak.mealient.datasource.v0.models.CreateApiTokenRequestV0
|
||||||
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0
|
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0
|
||||||
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0
|
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0
|
||||||
import gq.kirmanak.mealient.datasource.v0.models.ParseRecipeURLRequestV0
|
import gq.kirmanak.mealient.datasource.v0.models.ParseRecipeURLRequestV0
|
||||||
@@ -10,7 +11,6 @@ interface MealieDataSourceV0 {
|
|||||||
|
|
||||||
suspend fun addRecipe(
|
suspend fun addRecipe(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
recipe: AddRecipeRequestV0,
|
recipe: AddRecipeRequestV0,
|
||||||
): String
|
): String
|
||||||
|
|
||||||
@@ -29,20 +29,22 @@ interface MealieDataSourceV0 {
|
|||||||
|
|
||||||
suspend fun requestRecipes(
|
suspend fun requestRecipes(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
start: Int,
|
start: Int,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
): List<GetRecipeSummaryResponseV0>
|
): List<GetRecipeSummaryResponseV0>
|
||||||
|
|
||||||
suspend fun requestRecipeInfo(
|
suspend fun requestRecipeInfo(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
slug: String,
|
slug: String,
|
||||||
): GetRecipeResponseV0
|
): GetRecipeResponseV0
|
||||||
|
|
||||||
suspend fun parseRecipeFromURL(
|
suspend fun parseRecipeFromURL(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
request: ParseRecipeURLRequestV0,
|
request: ParseRecipeURLRequestV0,
|
||||||
): String
|
): String
|
||||||
|
|
||||||
|
suspend fun createApiToken(
|
||||||
|
baseUrl: String,
|
||||||
|
request: CreateApiTokenRequestV0,
|
||||||
|
): String
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import gq.kirmanak.mealient.datasource.NetworkError
|
|||||||
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
|
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
|
||||||
import gq.kirmanak.mealient.datasource.decode
|
import gq.kirmanak.mealient.datasource.decode
|
||||||
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0
|
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0
|
||||||
|
import gq.kirmanak.mealient.datasource.v0.models.CreateApiTokenRequestV0
|
||||||
import gq.kirmanak.mealient.datasource.v0.models.ErrorDetailV0
|
import gq.kirmanak.mealient.datasource.v0.models.ErrorDetailV0
|
||||||
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0
|
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0
|
||||||
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0
|
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0
|
||||||
@@ -26,12 +27,11 @@ class MealieDataSourceV0Impl @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun addRecipe(
|
override suspend fun addRecipe(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
recipe: AddRecipeRequestV0,
|
recipe: AddRecipeRequestV0,
|
||||||
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.addRecipe("$baseUrl/api/recipes/create", token, recipe) },
|
block = { service.addRecipe("$baseUrl/api/recipes/create", recipe) },
|
||||||
logMethod = { "addRecipe" },
|
logMethod = { "addRecipe" },
|
||||||
logParameters = { "baseUrl = $baseUrl, token = $token, recipe = $recipe" }
|
logParameters = { "baseUrl = $baseUrl, recipe = $recipe" }
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun authenticate(
|
override suspend fun authenticate(
|
||||||
@@ -64,33 +64,38 @@ class MealieDataSourceV0Impl @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun requestRecipes(
|
override suspend fun requestRecipes(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
start: Int,
|
start: Int,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
): List<GetRecipeSummaryResponseV0> = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): List<GetRecipeSummaryResponseV0> = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.getRecipeSummary("$baseUrl/api/recipes/summary", token, start, limit) },
|
block = { service.getRecipeSummary("$baseUrl/api/recipes/summary", start, limit) },
|
||||||
logMethod = { "requestRecipes" },
|
logMethod = { "requestRecipes" },
|
||||||
logParameters = { "baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" }
|
logParameters = { "baseUrl = $baseUrl, start = $start, limit = $limit" }
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun requestRecipeInfo(
|
override suspend fun requestRecipeInfo(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
slug: String,
|
slug: String,
|
||||||
): GetRecipeResponseV0 = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): GetRecipeResponseV0 = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.getRecipe("$baseUrl/api/recipes/$slug", token) },
|
block = { service.getRecipe("$baseUrl/api/recipes/$slug") },
|
||||||
logMethod = { "requestRecipeInfo" },
|
logMethod = { "requestRecipeInfo" },
|
||||||
logParameters = { "baseUrl = $baseUrl, token = $token, slug = $slug" }
|
logParameters = { "baseUrl = $baseUrl, slug = $slug" }
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun parseRecipeFromURL(
|
override suspend fun parseRecipeFromURL(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
request: ParseRecipeURLRequestV0
|
request: ParseRecipeURLRequestV0
|
||||||
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", token, request) },
|
block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", request) },
|
||||||
logMethod = { "parseRecipeFromURL" },
|
logMethod = { "parseRecipeFromURL" },
|
||||||
logParameters = { "baseUrl = $baseUrl, token = $token, request = $request" }
|
logParameters = { "baseUrl = $baseUrl, request = $request" },
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun createApiToken(
|
||||||
|
baseUrl: String,
|
||||||
|
request: CreateApiTokenRequestV0,
|
||||||
|
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
|
block = { service.createApiToken("$baseUrl/api/users/api-tokens", request) },
|
||||||
|
logMethod = { "createApiToken" },
|
||||||
|
logParameters = { "baseUrl = $baseUrl, request = $request" }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package gq.kirmanak.mealient.datasource.v0
|
package gq.kirmanak.mealient.datasource.v0
|
||||||
|
|
||||||
import gq.kirmanak.mealient.datasource.DataSourceModule.Companion.AUTHORIZATION_HEADER_NAME
|
|
||||||
import gq.kirmanak.mealient.datasource.v0.models.*
|
import gq.kirmanak.mealient.datasource.v0.models.*
|
||||||
import retrofit2.http.*
|
import retrofit2.http.*
|
||||||
|
|
||||||
@@ -17,7 +16,6 @@ interface MealieServiceV0 {
|
|||||||
@POST
|
@POST
|
||||||
suspend fun addRecipe(
|
suspend fun addRecipe(
|
||||||
@Url url: String,
|
@Url url: String,
|
||||||
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
|
|
||||||
@Body addRecipeRequestV0: AddRecipeRequestV0,
|
@Body addRecipeRequestV0: AddRecipeRequestV0,
|
||||||
): String
|
): String
|
||||||
|
|
||||||
@@ -29,7 +27,6 @@ interface MealieServiceV0 {
|
|||||||
@GET
|
@GET
|
||||||
suspend fun getRecipeSummary(
|
suspend fun getRecipeSummary(
|
||||||
@Url url: String,
|
@Url url: String,
|
||||||
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
|
|
||||||
@Query("start") start: Int,
|
@Query("start") start: Int,
|
||||||
@Query("limit") limit: Int,
|
@Query("limit") limit: Int,
|
||||||
): List<GetRecipeSummaryResponseV0>
|
): List<GetRecipeSummaryResponseV0>
|
||||||
@@ -37,13 +34,17 @@ interface MealieServiceV0 {
|
|||||||
@GET
|
@GET
|
||||||
suspend fun getRecipe(
|
suspend fun getRecipe(
|
||||||
@Url url: String,
|
@Url url: String,
|
||||||
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
|
|
||||||
): GetRecipeResponseV0
|
): GetRecipeResponseV0
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
suspend fun createRecipeFromURL(
|
suspend fun createRecipeFromURL(
|
||||||
@Url url: String,
|
@Url url: String,
|
||||||
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
|
|
||||||
@Body request: ParseRecipeURLRequestV0,
|
@Body request: ParseRecipeURLRequestV0,
|
||||||
): String
|
): String
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun createApiToken(
|
||||||
|
@Url url: String,
|
||||||
|
@Body request: CreateApiTokenRequestV0,
|
||||||
|
): String
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource.v0.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreateApiTokenRequestV0(
|
||||||
|
@SerialName("name") val name: String,
|
||||||
|
)
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package gq.kirmanak.mealient.datasource.v1
|
package gq.kirmanak.mealient.datasource.v1
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
|
||||||
@@ -11,13 +13,11 @@ interface MealieDataSourceV1 {
|
|||||||
|
|
||||||
suspend fun createRecipe(
|
suspend fun createRecipe(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
recipe: CreateRecipeRequestV1,
|
recipe: CreateRecipeRequestV1,
|
||||||
): String
|
): String
|
||||||
|
|
||||||
suspend fun updateRecipe(
|
suspend fun updateRecipe(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
slug: String,
|
slug: String,
|
||||||
recipe: UpdateRecipeRequestV1,
|
recipe: UpdateRecipeRequestV1,
|
||||||
): GetRecipeResponseV1
|
): GetRecipeResponseV1
|
||||||
@@ -37,20 +37,22 @@ interface MealieDataSourceV1 {
|
|||||||
|
|
||||||
suspend fun requestRecipes(
|
suspend fun requestRecipes(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
page: Int,
|
page: Int,
|
||||||
perPage: Int,
|
perPage: Int,
|
||||||
): List<GetRecipeSummaryResponseV1>
|
): List<GetRecipeSummaryResponseV1>
|
||||||
|
|
||||||
suspend fun requestRecipeInfo(
|
suspend fun requestRecipeInfo(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
slug: String,
|
slug: String,
|
||||||
): GetRecipeResponseV1
|
): GetRecipeResponseV1
|
||||||
|
|
||||||
suspend fun parseRecipeFromURL(
|
suspend fun parseRecipeFromURL(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
request: ParseRecipeURLRequestV1,
|
request: ParseRecipeURLRequestV1,
|
||||||
): String
|
): String
|
||||||
|
|
||||||
|
suspend fun createApiToken(
|
||||||
|
baseUrl: String,
|
||||||
|
request: CreateApiTokenRequestV1,
|
||||||
|
): CreateApiTokenResponseV1
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,8 @@ package gq.kirmanak.mealient.datasource.v1
|
|||||||
import gq.kirmanak.mealient.datasource.NetworkError
|
import gq.kirmanak.mealient.datasource.NetworkError
|
||||||
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
|
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
|
||||||
import gq.kirmanak.mealient.datasource.decode
|
import gq.kirmanak.mealient.datasource.decode
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.ErrorDetailV1
|
import gq.kirmanak.mealient.datasource.v1.models.ErrorDetailV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
|
||||||
@@ -27,23 +29,21 @@ class MealieDataSourceV1Impl @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun createRecipe(
|
override suspend fun createRecipe(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
recipe: CreateRecipeRequestV1
|
recipe: CreateRecipeRequestV1
|
||||||
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.createRecipe("$baseUrl/api/recipes", token, recipe) },
|
block = { service.createRecipe("$baseUrl/api/recipes", recipe) },
|
||||||
logMethod = { "createRecipe" },
|
logMethod = { "createRecipe" },
|
||||||
logParameters = { "baseUrl = $baseUrl, token = $token, recipe = $recipe" }
|
logParameters = { "baseUrl = $baseUrl, recipe = $recipe" }
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun updateRecipe(
|
override suspend fun updateRecipe(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
slug: String,
|
slug: String,
|
||||||
recipe: UpdateRecipeRequestV1
|
recipe: UpdateRecipeRequestV1
|
||||||
): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.updateRecipe("$baseUrl/api/recipes/$slug", token, recipe) },
|
block = { service.updateRecipe("$baseUrl/api/recipes/$slug", recipe) },
|
||||||
logMethod = { "updateRecipe" },
|
logMethod = { "updateRecipe" },
|
||||||
logParameters = { "baseUrl = $baseUrl, token = $token, slug = $slug, recipe = $recipe" }
|
logParameters = { "baseUrl = $baseUrl, slug = $slug, recipe = $recipe" }
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun authenticate(
|
override suspend fun authenticate(
|
||||||
@@ -76,35 +76,39 @@ class MealieDataSourceV1Impl @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun requestRecipes(
|
override suspend fun requestRecipes(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
page: Int,
|
page: Int,
|
||||||
perPage: Int
|
perPage: Int
|
||||||
): List<GetRecipeSummaryResponseV1> = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): List<GetRecipeSummaryResponseV1> = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.getRecipeSummary("$baseUrl/api/recipes", token, page, perPage) },
|
block = { service.getRecipeSummary("$baseUrl/api/recipes", page, perPage) },
|
||||||
logMethod = { "requestRecipes" },
|
logMethod = { "requestRecipes" },
|
||||||
logParameters = { "baseUrl = $baseUrl, token = $token, page = $page, perPage = $perPage" }
|
logParameters = { "baseUrl = $baseUrl, page = $page, perPage = $perPage" }
|
||||||
).items
|
).items
|
||||||
|
|
||||||
override suspend fun requestRecipeInfo(
|
override suspend fun requestRecipeInfo(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
slug: String
|
slug: String
|
||||||
): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.getRecipe("$baseUrl/api/recipes/$slug", token) },
|
block = { service.getRecipe("$baseUrl/api/recipes/$slug") },
|
||||||
logMethod = { "requestRecipeInfo" },
|
logMethod = { "requestRecipeInfo" },
|
||||||
logParameters = { "baseUrl = $baseUrl, token = $token, slug = $slug" }
|
logParameters = { "baseUrl = $baseUrl, slug = $slug" }
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun parseRecipeFromURL(
|
override suspend fun parseRecipeFromURL(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String?,
|
|
||||||
request: ParseRecipeURLRequestV1
|
request: ParseRecipeURLRequestV1
|
||||||
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", token, request) },
|
block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", request) },
|
||||||
logMethod = { "parseRecipeFromURL" },
|
logMethod = { "parseRecipeFromURL" },
|
||||||
logParameters = { "baseUrl = $baseUrl, token = $token, request = $request" }
|
logParameters = { "baseUrl = $baseUrl, request = $request" }
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override suspend fun createApiToken(
|
||||||
|
baseUrl: String,
|
||||||
|
request: CreateApiTokenRequestV1
|
||||||
|
): CreateApiTokenResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
|
block = { service.createApiToken("$baseUrl/api/users/api-tokens", request) },
|
||||||
|
logMethod = { "createApiToken" },
|
||||||
|
logParameters = { "baseUrl = $baseUrl, request = $request" }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package gq.kirmanak.mealient.datasource.v1
|
package gq.kirmanak.mealient.datasource.v1
|
||||||
|
|
||||||
import gq.kirmanak.mealient.datasource.DataSourceModule.Companion.AUTHORIZATION_HEADER_NAME
|
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.*
|
import gq.kirmanak.mealient.datasource.v1.models.*
|
||||||
import retrofit2.http.*
|
import retrofit2.http.*
|
||||||
|
|
||||||
@@ -17,14 +16,12 @@ interface MealieServiceV1 {
|
|||||||
@POST
|
@POST
|
||||||
suspend fun createRecipe(
|
suspend fun createRecipe(
|
||||||
@Url url: String,
|
@Url url: String,
|
||||||
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
|
|
||||||
@Body addRecipeRequest: CreateRecipeRequestV1,
|
@Body addRecipeRequest: CreateRecipeRequestV1,
|
||||||
): String
|
): String
|
||||||
|
|
||||||
@PATCH
|
@PATCH
|
||||||
suspend fun updateRecipe(
|
suspend fun updateRecipe(
|
||||||
@Url url: String,
|
@Url url: String,
|
||||||
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
|
|
||||||
@Body addRecipeRequest: UpdateRecipeRequestV1,
|
@Body addRecipeRequest: UpdateRecipeRequestV1,
|
||||||
): GetRecipeResponseV1
|
): GetRecipeResponseV1
|
||||||
|
|
||||||
@@ -36,7 +33,6 @@ interface MealieServiceV1 {
|
|||||||
@GET
|
@GET
|
||||||
suspend fun getRecipeSummary(
|
suspend fun getRecipeSummary(
|
||||||
@Url url: String,
|
@Url url: String,
|
||||||
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
|
|
||||||
@Query("page") page: Int,
|
@Query("page") page: Int,
|
||||||
@Query("perPage") perPage: Int,
|
@Query("perPage") perPage: Int,
|
||||||
): GetRecipesResponseV1
|
): GetRecipesResponseV1
|
||||||
@@ -44,13 +40,17 @@ interface MealieServiceV1 {
|
|||||||
@GET
|
@GET
|
||||||
suspend fun getRecipe(
|
suspend fun getRecipe(
|
||||||
@Url url: String,
|
@Url url: String,
|
||||||
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
|
|
||||||
): GetRecipeResponseV1
|
): GetRecipeResponseV1
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
suspend fun createRecipeFromURL(
|
suspend fun createRecipeFromURL(
|
||||||
@Url url: String,
|
@Url url: String,
|
||||||
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
|
|
||||||
@Body request: ParseRecipeURLRequestV1,
|
@Body request: ParseRecipeURLRequestV1,
|
||||||
): String
|
): String
|
||||||
|
|
||||||
|
@POST
|
||||||
|
suspend fun createApiToken(
|
||||||
|
@Url url: String,
|
||||||
|
@Body request: CreateApiTokenRequestV1,
|
||||||
|
): CreateApiTokenResponseV1
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource.v1.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreateApiTokenRequestV1(
|
||||||
|
@SerialName("name") val name: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource.v1.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreateApiTokenResponseV1(
|
||||||
|
@SerialName("token") val token: String,
|
||||||
|
)
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package gq.kirmanak.mealient
|
|
||||||
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
object ReleaseModule {
|
|
||||||
|
|
||||||
// Release version of the application doesn't have any interceptors but this Set
|
|
||||||
// is required by Dagger, so an empty Set is provided here
|
|
||||||
// Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382)
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideInterceptors(): Set<@JvmSuppressWildcards Interceptor> = emptySet()
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package gq.kirmanak.mealient.datasource
|
package gq.kirmanak.mealient.datasource
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import gq.kirmanak.mealient.datasource.impl.NetworkRequestWrapperImpl
|
||||||
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0Impl
|
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0Impl
|
||||||
import gq.kirmanak.mealient.datasource.v0.MealieServiceV0
|
import gq.kirmanak.mealient.datasource.v0.MealieServiceV0
|
||||||
import gq.kirmanak.mealient.datasource.v0.models.GetTokenResponseV0
|
import gq.kirmanak.mealient.datasource.v0.models.GetTokenResponseV0
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource.impl
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import gq.kirmanak.mealient.datasource.AuthenticationProvider
|
||||||
|
import gq.kirmanak.mealient.datasource.impl.AuthInterceptor.Companion.HEADER_NAME
|
||||||
|
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import io.mockk.slot
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Protocol
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AuthInterceptorTest : BaseUnitTest() {
|
||||||
|
|
||||||
|
private lateinit var subject: Interceptor
|
||||||
|
|
||||||
|
@MockK(relaxed = true)
|
||||||
|
lateinit var authenticationProvider: AuthenticationProvider
|
||||||
|
|
||||||
|
@MockK(relaxed = true)
|
||||||
|
lateinit var chain: Interceptor.Chain
|
||||||
|
|
||||||
|
@Before
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
subject = AuthInterceptor(logger) { authenticationProvider }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when intercept is called expect header to be retrieved`() {
|
||||||
|
subject.intercept(chain)
|
||||||
|
coVerify { authenticationProvider.getAuthHeader() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when intercept is called and no header expect no header`() {
|
||||||
|
coEvery { authenticationProvider.getAuthHeader() } returns null
|
||||||
|
coEvery { chain.request() } returns buildRequest()
|
||||||
|
val requestSlot = slot<Request>()
|
||||||
|
coEvery { chain.proceed(capture(requestSlot)) } returns buildResponse()
|
||||||
|
subject.intercept(chain)
|
||||||
|
assertThat(requestSlot.captured.header(HEADER_NAME)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when intercept is called and no header expect no logout`() {
|
||||||
|
coEvery { authenticationProvider.getAuthHeader() } returns null
|
||||||
|
coEvery { chain.request() } returns buildRequest()
|
||||||
|
coEvery { chain.proceed(any()) } returns buildResponse(code = 200)
|
||||||
|
subject.intercept(chain)
|
||||||
|
coVerify(inverse = true) { authenticationProvider.logout() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when intercept is called with no header and auth fails expect no logout`() {
|
||||||
|
coEvery { authenticationProvider.getAuthHeader() } returns null
|
||||||
|
coEvery { chain.request() } returns buildRequest()
|
||||||
|
coEvery { chain.proceed(any()) } returns buildResponse(code = 401)
|
||||||
|
subject.intercept(chain)
|
||||||
|
coVerify(inverse = true) { authenticationProvider.logout() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when intercept is called and there is a header expect a header`() {
|
||||||
|
coEvery { authenticationProvider.getAuthHeader() } returns "header"
|
||||||
|
coEvery { chain.request() } returns buildRequest()
|
||||||
|
val requestSlot = slot<Request>()
|
||||||
|
coEvery { chain.proceed(capture(requestSlot)) } returns buildResponse()
|
||||||
|
subject.intercept(chain)
|
||||||
|
assertThat(requestSlot.captured.header(HEADER_NAME)).isEqualTo("header")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when intercept is called and there is a header that authenticates expect no logout`() {
|
||||||
|
coEvery { authenticationProvider.getAuthHeader() } returns "header"
|
||||||
|
coEvery { chain.request() } returns buildRequest()
|
||||||
|
coEvery { chain.proceed(any()) } returns buildResponse(code = 200)
|
||||||
|
subject.intercept(chain)
|
||||||
|
coVerify(inverse = true) { authenticationProvider.logout() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when intercept is called and there was a header but still 401 expect logout`() {
|
||||||
|
coEvery { authenticationProvider.getAuthHeader() } returns "header"
|
||||||
|
coEvery { chain.request() } returns buildRequest()
|
||||||
|
coEvery { chain.proceed(any()) } returns buildResponse(code = 401)
|
||||||
|
subject.intercept(chain)
|
||||||
|
coVerify { authenticationProvider.logout() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildResponse(
|
||||||
|
url: String = "http://localhost",
|
||||||
|
code: Int = 200,
|
||||||
|
message: String = if (code == 200) "OK" else "Unauthorized",
|
||||||
|
protocol: Protocol = Protocol.HTTP_2,
|
||||||
|
) = Response.Builder().apply {
|
||||||
|
request(buildRequest(url))
|
||||||
|
code(code)
|
||||||
|
message(message)
|
||||||
|
protocol(protocol)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
private fun buildRequest(
|
||||||
|
url: String = "http://localhost",
|
||||||
|
) = Request.Builder().url(url).build()
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package gq.kirmanak.mealient.test
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import dagger.hilt.android.testing.HiltAndroidRule
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
import dagger.hilt.android.testing.HiltTestApplication
|
import dagger.hilt.android.testing.HiltTestApplication
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@@ -15,6 +16,8 @@ abstract class HiltRobolectricTest {
|
|||||||
@get:Rule
|
@get:Rule
|
||||||
var hiltRule = HiltAndroidRule(this)
|
var hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
|
protected val logger: Logger = FakeLogger()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun inject() {
|
fun inject() {
|
||||||
hiltRule.inject()
|
hiltRule.inject()
|
||||||
|
|||||||
Reference in New Issue
Block a user