Extract server info repo

This commit is contained in:
Kirill Kamakin
2022-10-29 19:15:57 +02:00
parent 9e9d07db7d
commit cda22215ec
21 changed files with 205 additions and 122 deletions

View File

@@ -1,8 +1,15 @@
package gq.kirmanak.mealient.data.auth package gq.kirmanak.mealient.data.auth
import gq.kirmanak.mealient.data.baseurl.ServerVersion
interface AuthDataSource { 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, baseUrl: String): String suspend fun authenticate(
username: String,
password: String,
baseUrl: String,
serverVersion: ServerVersion,
): String
} }

View File

@@ -1,15 +1,25 @@
package gq.kirmanak.mealient.data.auth.impl 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.baseurl.ServerVersion
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0 import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class AuthDataSourceImpl @Inject constructor( class AuthDataSourceImpl @Inject constructor(
private val v0Source: MealieDataSourceV0, private val v0Source: MealieDataSourceV0,
private val v1Source: MealieDataSourceV1,
) : AuthDataSource { ) : AuthDataSource {
override suspend fun authenticate(username: String, password: String, baseUrl: String): String = override suspend fun authenticate(
v0Source.authenticate(baseUrl, username, password) username: String,
password: String,
baseUrl: String,
serverVersion: ServerVersion,
): String = when (serverVersion) {
ServerVersion.V0 -> v0Source.authenticate(baseUrl, username, password)
ServerVersion.V1 -> v1Source.authenticate(baseUrl, username, password)
}
} }

View File

@@ -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.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -15,7 +15,7 @@ import javax.inject.Singleton
class AuthRepoImpl @Inject constructor( class AuthRepoImpl @Inject constructor(
private val authStorage: AuthStorage, private val authStorage: AuthStorage,
private val authDataSource: AuthDataSource, private val authDataSource: AuthDataSource,
private val baseURLStorage: BaseURLStorage, private val serverInfoRepo: ServerInfoRepo,
private val logger: Logger, private val logger: Logger,
) : AuthRepo { ) : AuthRepo {
@@ -24,7 +24,9 @@ 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" }
authDataSource.authenticate(email, password, baseURLStorage.requireBaseURL()) val version = serverInfoRepo.getVersion()
val url = serverInfoRepo.requireUrl()
authDataSource.authenticate(email, password, url, version)
.let { AUTH_HEADER_FORMAT.format(it) } .let { AUTH_HEADER_FORMAT.format(it) }
.let { authStorage.setAuthHeader(it) } .let { authStorage.setAuthHeader(it) }
authStorage.setEmail(email) authStorage.setEmail(email)

View File

@@ -0,0 +1,11 @@
package gq.kirmanak.mealient.data.baseurl
interface ServerInfoRepo {
suspend fun getUrl(): String?
suspend fun requireUrl(): String
suspend fun getVersion(): ServerVersion
}

View File

@@ -0,0 +1,47 @@
package gq.kirmanak.mealient.data.baseurl
import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ServerInfoRepoImpl @Inject constructor(
private val serverInfoStorage: ServerInfoStorage,
private val versionDataSource: VersionDataSource,
private val logger: Logger,
) : ServerInfoRepo {
override suspend fun getUrl(): String? {
val result = serverInfoStorage.getBaseURL()
logger.v { "getUrl() returned: $result" }
return result
}
override suspend fun requireUrl(): String {
val result = checkNotNull(getUrl()) { "Server URL was null when it was required" }
logger.v { "requireUrl() returned: $result" }
return result
}
override suspend fun getVersion(): ServerVersion {
var version = serverInfoStorage.getServerVersion()
val serverVersion = if (version == null) {
logger.d { "getVersion: version is null, requesting" }
version = versionDataSource.getVersionInfo(requireUrl()).version
val result = determineServerVersion(version)
serverInfoStorage.storeServerVersion(version)
result
} else {
determineServerVersion(version)
}
logger.v { "getVersion() returned: $serverVersion from $version" }
return serverVersion
}
private fun determineServerVersion(version: String): ServerVersion = when {
version.startsWith("v0") -> ServerVersion.V0
version.startsWith("v1") -> ServerVersion.V1
else -> throw NetworkError.NotMealie(IllegalStateException("Server version is unknown: $version"))
}
}

View File

@@ -1,11 +1,9 @@
package gq.kirmanak.mealient.data.baseurl package gq.kirmanak.mealient.data.baseurl
interface BaseURLStorage { interface ServerInfoStorage {
suspend fun getBaseURL(): String? suspend fun getBaseURL(): String?
suspend fun requireBaseURL(): String
suspend fun storeBaseURL(baseURL: String, version: String) suspend fun storeBaseURL(baseURL: String, version: String)
suspend fun storeServerVersion(version: String) suspend fun storeServerVersion(version: String)

View File

@@ -0,0 +1,3 @@
package gq.kirmanak.mealient.data.baseurl
enum class ServerVersion { V0, V1 }

View File

@@ -0,0 +1,37 @@
package gq.kirmanak.mealient.data.baseurl
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.extensions.toVersionInfo
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class VersionDataSourceImpl @Inject constructor(
private val v0Source: MealieDataSourceV0,
private val v1Source: MealieDataSourceV1,
) : VersionDataSource {
override suspend fun getVersionInfo(baseUrl: String): VersionInfo {
val responses = coroutineScope {
val v0Deferred = async {
runCatchingExceptCancel { v0Source.getVersionInfo(baseUrl).toVersionInfo() }
}
val v1Deferred = async {
runCatchingExceptCancel { v1Source.getVersionInfo(baseUrl).toVersionInfo() }
}
listOf(v0Deferred, v1Deferred).awaitAll()
}
val firstSuccess = responses.firstNotNullOfOrNull { it.getOrNull() }
if (firstSuccess == null) {
throw responses.firstNotNullOf { it.exceptionOrNull() }
} else {
return firstSuccess
}
}
}

View File

@@ -1,15 +1,15 @@
package gq.kirmanak.mealient.data.baseurl.impl package gq.kirmanak.mealient.data.baseurl.impl
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.ServerInfoStorage
import gq.kirmanak.mealient.data.storage.PreferencesStorage import gq.kirmanak.mealient.data.storage.PreferencesStorage
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class BaseURLStorageImpl @Inject constructor( class ServerInfoStorageImpl @Inject constructor(
private val preferencesStorage: PreferencesStorage, private val preferencesStorage: PreferencesStorage,
) : BaseURLStorage { ) : ServerInfoStorage {
private val baseUrlKey: Preferences.Key<String> private val baseUrlKey: Preferences.Key<String>
get() = preferencesStorage.baseUrlKey get() = preferencesStorage.baseUrlKey
@@ -19,10 +19,6 @@ class BaseURLStorageImpl @Inject constructor(
override suspend fun getBaseURL(): String? = getValue(baseUrlKey) override suspend fun getBaseURL(): String? = getValue(baseUrlKey)
override suspend fun requireBaseURL(): String = checkNotNull(getBaseURL()) {
"Base URL was null when it was required"
}
override suspend fun storeBaseURL(baseURL: String, version: String) { override suspend fun storeBaseURL(baseURL: String, version: String) {
preferencesStorage.storeValues( preferencesStorage.storeValues(
Pair(baseUrlKey, baseURL), Pair(baseUrlKey, baseURL),

View File

@@ -2,9 +2,8 @@ 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.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.data.baseurl.VersionDataSource import gq.kirmanak.mealient.data.baseurl.ServerVersion
import gq.kirmanak.mealient.data.baseurl.VersionInfo
import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo
@@ -12,75 +11,51 @@ 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.v0.models.AddRecipeRequestV0 import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.extensions.toFullRecipeInfo import gq.kirmanak.mealient.extensions.toFullRecipeInfo
import gq.kirmanak.mealient.extensions.toRecipeSummaryInfo import gq.kirmanak.mealient.extensions.toRecipeSummaryInfo
import gq.kirmanak.mealient.extensions.toVersionInfo
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
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 baseURLStorage: BaseURLStorage, private val serverInfoRepo: ServerInfoRepo,
private val authRepo: AuthRepo, private val authRepo: AuthRepo,
private val v0source: MealieDataSourceV0, private val v0source: MealieDataSourceV0,
private val v1Source: MealieDataSourceV1, private val v1Source: MealieDataSourceV1,
) : AddRecipeDataSource, RecipeDataSource, VersionDataSource { ) : AddRecipeDataSource, RecipeDataSource {
override suspend fun addRecipe(recipe: AddRecipeRequestV0): String = override suspend fun addRecipe(recipe: AddRecipeRequestV0): String = withAuthHeader { token ->
withAuthHeader { token -> v0source.addRecipe(getUrl(), token, recipe) } v0source.addRecipe(getUrl(), token, recipe)
override suspend fun getVersionInfo(baseUrl: String): VersionInfo {
val responses = coroutineScope {
val v0Deferred = async {
runCatchingExceptCancel { v0source.getVersionInfo(baseUrl).toVersionInfo() }
}
val v1Deferred = async {
runCatchingExceptCancel { v1Source.getVersionInfo(baseUrl).toVersionInfo() }
}
listOf(v0Deferred, v1Deferred).awaitAll()
}
val firstSuccess = responses.firstNotNullOfOrNull { it.getOrNull() }
if (firstSuccess == null) {
throw responses.firstNotNullOf { it.exceptionOrNull() }
} else {
return firstSuccess
}
} }
override suspend fun requestRecipes(start: Int, limit: Int): List<RecipeSummaryInfo> = override suspend fun requestRecipes(
withAuthHeader { token -> start: Int,
limit: Int
): List<RecipeSummaryInfo> = withAuthHeader { token ->
val url = getUrl() val url = getUrl()
if (isV1()) { when (getVersion()) {
v1Source.requestRecipes(url, token, start, limit).map { it.toRecipeSummaryInfo() } ServerVersion.V0 -> {
} else {
v0source.requestRecipes(url, token, start, limit).map { it.toRecipeSummaryInfo() } v0source.requestRecipes(url, token, start, limit).map { it.toRecipeSummaryInfo() }
} }
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
val page = start / limit + 1
v1Source.requestRecipes(url, token, page, limit).map { it.toRecipeSummaryInfo() }
}
}
} }
override suspend fun requestRecipeInfo(slug: String): FullRecipeInfo = override suspend fun requestRecipeInfo(slug: String): FullRecipeInfo = withAuthHeader { token ->
withAuthHeader { token ->
val url = getUrl() val url = getUrl()
if (isV1()) { when (getVersion()) {
v1Source.requestRecipeInfo(url, token, slug).toFullRecipeInfo() ServerVersion.V0 -> v0source.requestRecipeInfo(url, token, slug).toFullRecipeInfo()
} else { ServerVersion.V1 -> v1Source.requestRecipeInfo(url, token, slug).toFullRecipeInfo()
v0source.requestRecipeInfo(url, token, slug).toFullRecipeInfo()
} }
} }
private suspend fun getUrl() = baseURLStorage.requireBaseURL() private suspend fun getUrl() = serverInfoRepo.requireUrl()
private suspend fun isV1(): Boolean { private suspend fun getVersion() = serverInfoRepo.getVersion()
var version = baseURLStorage.getServerVersion()
if (version == null) {
version = getVersionInfo(getUrl()).version
baseURLStorage.storeServerVersion(version)
}
return version.startsWith("v1")
}
private suspend inline fun <T> withAuthHeader(block: (String?) -> T): T = private suspend inline fun <T> withAuthHeader(block: (String?) -> T): T =
runCatching { block(authRepo.getAuthHeader()) }.getOrElse { runCatching { block(authRepo.getAuthHeader()) }.getOrElse {

View File

@@ -1,6 +1,6 @@
package gq.kirmanak.mealient.data.recipes.impl package gq.kirmanak.mealient.data.recipes.impl
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import javax.inject.Inject import javax.inject.Inject
@@ -8,7 +8,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class RecipeImageUrlProviderImpl @Inject constructor( class RecipeImageUrlProviderImpl @Inject constructor(
private val baseURLStorage: BaseURLStorage, private val serverInfoRepo: ServerInfoRepo,
private val logger: Logger, private val logger: Logger,
) : RecipeImageUrlProvider { ) : RecipeImageUrlProvider {
@@ -16,7 +16,7 @@ class RecipeImageUrlProviderImpl @Inject constructor(
logger.v { "generateImageUrl() called with: slug = $slug" } logger.v { "generateImageUrl() called with: slug = $slug" }
slug?.takeUnless { it.isBlank() } ?: return null slug?.takeUnless { it.isBlank() } ?: return null
val imagePath = IMAGE_PATH_FORMAT.format(slug) val imagePath = IMAGE_PATH_FORMAT.format(slug)
val baseUrl = baseURLStorage.getBaseURL()?.takeUnless { it.isEmpty() } val baseUrl = serverInfoRepo.getUrl()?.takeUnless { it.isEmpty() }
val result = baseUrl?.toHttpUrlOrNull() val result = baseUrl?.toHttpUrlOrNull()
?.newBuilder() ?.newBuilder()
?.addPathSegments(imagePath) ?.addPathSegments(imagePath)

View File

@@ -4,10 +4,8 @@ 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.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.*
import gq.kirmanak.mealient.data.baseurl.VersionDataSource import gq.kirmanak.mealient.data.baseurl.impl.ServerInfoStorageImpl
import gq.kirmanak.mealient.data.baseurl.impl.BaseURLStorageImpl
import gq.kirmanak.mealient.data.network.MealieDataSourceWrapper
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@@ -16,9 +14,13 @@ interface BaseURLModule {
@Binds @Binds
@Singleton @Singleton
fun bindVersionDataSource(mealieDataSourceWrapper: MealieDataSourceWrapper): VersionDataSource fun bindVersionDataSource(versionDataSourceImpl: VersionDataSourceImpl): VersionDataSource
@Binds @Binds
@Singleton @Singleton
fun bindBaseUrlStorage(baseURLStorageImpl: BaseURLStorageImpl): BaseURLStorage fun bindBaseUrlStorage(baseURLStorageImpl: ServerInfoStorageImpl): ServerInfoStorage
@Binds
@Singleton
fun bindServerInfoRepo(serverInfoRepoImpl: ServerInfoRepoImpl): ServerInfoRepo
} }

View File

@@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.ServerInfoStorage
import gq.kirmanak.mealient.data.baseurl.VersionDataSource import gq.kirmanak.mealient.data.baseurl.VersionDataSource
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
@@ -15,7 +15,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class BaseURLViewModel @Inject constructor( class BaseURLViewModel @Inject constructor(
private val baseURLStorage: BaseURLStorage, private val serverInfoStorage: ServerInfoStorage,
private val versionDataSource: VersionDataSource, private val versionDataSource: VersionDataSource,
private val logger: Logger, private val logger: Logger,
) : ViewModel() { ) : ViewModel() {
@@ -36,7 +36,7 @@ class BaseURLViewModel @Inject constructor(
val result = runCatchingExceptCancel { val result = runCatchingExceptCancel {
// If it returns proper version info then it must be a Mealie // If it returns proper version info then it must be a Mealie
val version = versionDataSource.getVersionInfo(baseURL).version val version = versionDataSource.getVersionInfo(baseURL).version
baseURLStorage.storeBaseURL(baseURL, version) serverInfoStorage.storeBaseURL(baseURL, version)
} }
logger.i { "checkBaseURL: result is $result" } logger.i { "checkBaseURL: result is $result" }
_uiState.value = OperationUiState.fromResult(result) _uiState.value = OperationUiState.fromResult(result)

View File

@@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -15,7 +15,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SplashViewModel @Inject constructor( class SplashViewModel @Inject constructor(
private val disclaimerStorage: DisclaimerStorage, private val disclaimerStorage: DisclaimerStorage,
private val baseURLStorage: BaseURLStorage, private val serverInfoRepo: ServerInfoRepo,
) : ViewModel() { ) : ViewModel() {
private val _nextDestination = MutableLiveData<NavDirections>() private val _nextDestination = MutableLiveData<NavDirections>()
val nextDestination: LiveData<NavDirections> = _nextDestination val nextDestination: LiveData<NavDirections> = _nextDestination
@@ -25,7 +25,7 @@ class SplashViewModel @Inject constructor(
delay(1000) delay(1000)
_nextDestination.value = when { _nextDestination.value = when {
!disclaimerStorage.isDisclaimerAccepted() -> SplashFragmentDirections.actionSplashFragmentToDisclaimerFragment() !disclaimerStorage.isDisclaimerAccepted() -> SplashFragmentDirections.actionSplashFragmentToDisclaimerFragment()
baseURLStorage.getBaseURL() == null -> SplashFragmentDirections.actionSplashFragmentToBaseURLFragment() serverInfoRepo.getUrl() == null -> SplashFragmentDirections.actionSplashFragmentToBaseURLFragment()
else -> SplashFragmentDirections.actionSplashFragmentToRecipesFragment() else -> SplashFragmentDirections.actionSplashFragmentToRecipesFragment()
} }
} }

View File

@@ -4,7 +4,7 @@ import com.google.common.truth.Truth.assertThat
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.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.ServerInfoStorage
import gq.kirmanak.mealient.logging.Logger 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_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
@@ -27,7 +27,7 @@ class AuthRepoImplTest {
lateinit var dataSource: AuthDataSource lateinit var dataSource: AuthDataSource
@MockK @MockK
lateinit var baseURLStorage: BaseURLStorage lateinit var serverInfoStorage: ServerInfoStorage
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var storage: AuthStorage lateinit var storage: AuthStorage
@@ -40,7 +40,7 @@ class AuthRepoImplTest {
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
subject = AuthRepoImpl(storage, dataSource, baseURLStorage, logger) subject = AuthRepoImpl(storage, dataSource, serverInfoStorage, logger)
} }
@Test @Test
@@ -58,7 +58,7 @@ class AuthRepoImplTest {
eq(TEST_BASE_URL) eq(TEST_BASE_URL)
) )
} returns TEST_TOKEN } returns TEST_TOKEN
coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL coEvery { serverInfoStorage.requireBaseURL() } returns TEST_BASE_URL
subject.authenticate(TEST_USERNAME, TEST_PASSWORD) subject.authenticate(TEST_USERNAME, TEST_PASSWORD)
coVerifyAll { coVerifyAll {
storage.setAuthHeader(TEST_AUTH_HEADER) storage.setAuthHeader(TEST_AUTH_HEADER)
@@ -71,7 +71,7 @@ class AuthRepoImplTest {
@Test @Test
fun `when authenticate fails then does not change storage`() = runTest { fun `when authenticate fails then does not change storage`() = runTest {
coEvery { dataSource.authenticate(any(), any(), any()) } throws RuntimeException() coEvery { dataSource.authenticate(any(), any(), any()) } throws RuntimeException()
coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL coEvery { serverInfoStorage.requireBaseURL() } returns TEST_BASE_URL
runCatching { subject.authenticate("invalid", "") } runCatching { subject.authenticate("invalid", "") }
confirmVerified(storage) confirmVerified(storage)
} }
@@ -107,7 +107,7 @@ class AuthRepoImplTest {
fun `when invalidate with credentials then calls authenticate`() = runTest { fun `when invalidate with credentials then calls authenticate`() = runTest {
coEvery { storage.getEmail() } returns TEST_USERNAME coEvery { storage.getEmail() } returns TEST_USERNAME
coEvery { storage.getPassword() } returns TEST_PASSWORD coEvery { storage.getPassword() } returns TEST_PASSWORD
coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL coEvery { serverInfoStorage.requireBaseURL() } returns TEST_BASE_URL
coEvery { coEvery {
dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD), eq(TEST_BASE_URL)) dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD), eq(TEST_BASE_URL))
} returns TEST_TOKEN } returns TEST_TOKEN
@@ -121,7 +121,7 @@ class AuthRepoImplTest {
fun `when invalidate with credentials and auth fails then clears email`() = runTest { fun `when invalidate with credentials and auth fails then clears email`() = runTest {
coEvery { storage.getEmail() } returns "invalid" coEvery { storage.getEmail() } returns "invalid"
coEvery { storage.getPassword() } returns "" coEvery { storage.getPassword() } returns ""
coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL coEvery { serverInfoStorage.requireBaseURL() } returns TEST_BASE_URL
coEvery { dataSource.authenticate(any(), any(), any()) } throws RuntimeException() coEvery { dataSource.authenticate(any(), any(), any()) } throws RuntimeException()
subject.invalidateAuthHeader() subject.invalidateAuthHeader()
coVerify { storage.setEmail(null) } coVerify { storage.setEmail(null) }

View File

@@ -2,7 +2,7 @@ package gq.kirmanak.mealient.data.baseurl
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.baseurl.impl.BaseURLStorageImpl import gq.kirmanak.mealient.data.baseurl.impl.ServerInfoStorageImpl
import gq.kirmanak.mealient.data.storage.PreferencesStorage import gq.kirmanak.mealient.data.storage.PreferencesStorage
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.coEvery import io.mockk.coEvery
@@ -15,19 +15,19 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class BaseURLStorageImplTest { class ServerInfoStorageImplTest {
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var preferencesStorage: PreferencesStorage lateinit var preferencesStorage: PreferencesStorage
lateinit var subject: BaseURLStorage lateinit var subject: ServerInfoStorage
private val baseUrlKey = stringPreferencesKey("baseUrlKey") private val baseUrlKey = stringPreferencesKey("baseUrlKey")
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
subject = BaseURLStorageImpl(preferencesStorage) subject = ServerInfoStorageImpl(preferencesStorage)
every { preferencesStorage.baseUrlKey } returns baseUrlKey every { preferencesStorage.baseUrlKey } returns baseUrlKey
} }

View File

@@ -1,7 +1,7 @@
package gq.kirmanak.mealient.data.network package gq.kirmanak.mealient.data.network
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.ServerInfoStorage
import gq.kirmanak.mealient.datasource.NetworkError import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0 import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
@@ -21,7 +21,7 @@ import java.io.IOException
class MealieDataSourceV0WrapperTest { class MealieDataSourceV0WrapperTest {
@MockK @MockK
lateinit var baseURLStorage: BaseURLStorage lateinit var serverInfoStorage: ServerInfoStorage
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var authRepo: AuthRepo lateinit var authRepo: AuthRepo
@@ -34,12 +34,12 @@ class MealieDataSourceV0WrapperTest {
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
subject = MealieDataSourceWrapper(baseURLStorage, authRepo, mealieDataSourceV0) subject = MealieDataSourceWrapper(serverInfoStorage, authRepo, mealieDataSourceV0)
} }
@Test @Test
fun `when withAuthHeader fails with Unauthorized then invalidates auth`() = runTest { fun `when withAuthHeader fails with Unauthorized then invalidates auth`() = runTest {
coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL coEvery { serverInfoStorage.requireBaseURL() } returns TEST_BASE_URL
coEvery { authRepo.getAuthHeader() } returns null andThen TEST_AUTH_HEADER coEvery { authRepo.getAuthHeader() } returns null andThen TEST_AUTH_HEADER
coEvery { coEvery {
mealieDataSourceV0.requestRecipeInfo(eq(TEST_BASE_URL), isNull(), eq("cake")) mealieDataSourceV0.requestRecipeInfo(eq(TEST_BASE_URL), isNull(), eq("cake"))

View File

@@ -1,7 +1,7 @@
package gq.kirmanak.mealient.data.recipes.impl package gq.kirmanak.mealient.data.recipes.impl
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.ServerInfoStorage
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.coEvery import io.mockk.coEvery
@@ -17,7 +17,7 @@ class RecipeImageUrlProviderImplTest {
lateinit var subject: RecipeImageUrlProvider lateinit var subject: RecipeImageUrlProvider
@MockK @MockK
lateinit var baseURLStorage: BaseURLStorage lateinit var serverInfoStorage: ServerInfoStorage
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var logger: Logger lateinit var logger: Logger
@@ -25,7 +25,7 @@ class RecipeImageUrlProviderImplTest {
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
subject = RecipeImageUrlProviderImpl(baseURLStorage, logger) subject = RecipeImageUrlProviderImpl(serverInfoStorage, logger)
prepareBaseURL("https://google.com/") prepareBaseURL("https://google.com/")
} }
@@ -81,6 +81,6 @@ class RecipeImageUrlProviderImplTest {
} }
private fun prepareBaseURL(baseURL: String?) { private fun prepareBaseURL(baseURL: String?) {
coEvery { baseURLStorage.getBaseURL() } returns baseURL coEvery { serverInfoStorage.getBaseURL() } returns baseURL
} }
} }

View File

@@ -1,6 +1,6 @@
package gq.kirmanak.mealient.ui.baseurl package gq.kirmanak.mealient.ui.baseurl
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.ServerInfoStorage
import gq.kirmanak.mealient.data.baseurl.VersionDataSource import gq.kirmanak.mealient.data.baseurl.VersionDataSource
import gq.kirmanak.mealient.data.baseurl.VersionInfo import gq.kirmanak.mealient.data.baseurl.VersionInfo
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
@@ -21,7 +21,7 @@ import org.junit.Test
class BaseURLViewModelTest : RobolectricTest() { class BaseURLViewModelTest : RobolectricTest() {
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var baseURLStorage: BaseURLStorage lateinit var serverInfoStorage: ServerInfoStorage
@MockK @MockK
lateinit var versionDataSource: VersionDataSource lateinit var versionDataSource: VersionDataSource
@@ -34,7 +34,7 @@ class BaseURLViewModelTest : RobolectricTest() {
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
subject = BaseURLViewModel(baseURLStorage, versionDataSource, logger) subject = BaseURLViewModel(serverInfoStorage, versionDataSource, logger)
} }
@Test @Test
@@ -44,6 +44,6 @@ class BaseURLViewModelTest : RobolectricTest() {
} returns VersionInfo(TEST_VERSION) } returns VersionInfo(TEST_VERSION)
subject.saveBaseUrl(TEST_BASE_URL) subject.saveBaseUrl(TEST_BASE_URL)
advanceUntilIdle() advanceUntilIdle()
coVerify { baseURLStorage.storeBaseURL(eq(TEST_BASE_URL), eq(TEST_VERSION)) } coVerify { serverInfoStorage.storeBaseURL(eq(TEST_BASE_URL), eq(TEST_VERSION)) }
} }
} }

View File

@@ -29,8 +29,8 @@ interface MealieDataSourceV1 {
suspend fun requestRecipes( suspend fun requestRecipes(
baseUrl: String, baseUrl: String,
token: String?, token: String?,
start: Int, page: Int,
limit: Int, perPage: Int,
): List<GetRecipeSummaryResponseV1> ): List<GetRecipeSummaryResponseV1>
suspend fun requestRecipeInfo( suspend fun requestRecipeInfo(

View File

@@ -48,18 +48,13 @@ class MealieDataSourceV1Impl @Inject constructor(
override suspend fun requestRecipes( override suspend fun requestRecipes(
baseUrl: String, baseUrl: String,
token: String?, token: String?,
start: Int, page: Int,
limit: Int perPage: Int
): List<GetRecipeSummaryResponseV1> { ): List<GetRecipeSummaryResponseV1> = makeCall(
// Imagine start is 30 and limit is 15. It means that we already have page 1 and 2, now we need page 3
val perPage = limit
val page = start / perPage + 1
return makeCall(
block = { getRecipeSummary("$baseUrl/api/recipes", token, page, perPage) }, block = { getRecipeSummary("$baseUrl/api/recipes", token, page, perPage) },
logMethod = { "requestRecipesV1" }, logMethod = { "requestRecipesV1" },
logParameters = { "baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" } logParameters = { "baseUrl = $baseUrl, token = $token, page = $page, perPage = $perPage" }
).map { it.items }.getOrThrowUnauthorized() ).map { it.items }.getOrThrowUnauthorized()
}
override suspend fun requestRecipeInfo( override suspend fun requestRecipeInfo(
baseUrl: String, baseUrl: String,