From f6c0e862fc32b8d8a9c7c90d1d80afba8b355844 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 11 Dec 2022 20:22:36 +0100 Subject: [PATCH 1/2] Set base url through Interceptor --- .../data/auth/impl/AuthDataSourceImpl.kt | 10 ++-- .../mealient/data/baseurl/ServerInfoRepo.kt | 5 +- .../data/baseurl/ServerInfoRepoImpl.kt | 26 ++++++---- .../data/baseurl/ServerInfoStorage.kt | 5 +- .../data/baseurl/VersionDataSource.kt | 2 +- .../data/baseurl/VersionDataSourceImpl.kt | 6 +-- .../baseurl/impl/ServerInfoStorageImpl.kt | 28 +++++++++-- .../data/network/MealieDataSourceWrapper.kt | 20 ++++---- .../gq/kirmanak/mealient/di/BaseURLModule.kt | 5 ++ .../mealient/ui/baseurl/BaseURLViewModel.kt | 9 +--- .../data/auth/impl/AuthRepoImplTest.kt | 3 -- .../data/baseurl/ServerInfoRepoTest.kt | 50 +++++++++++-------- .../network/MealieDataSourceWrapperTest.kt | 30 ++++------- .../ui/baseurl/BaseURLViewModelTest.kt | 25 +++------- .../mealient/datasource/DataSourceModule.kt | 16 ++++-- .../mealient/datasource/LocalInterceptor.kt | 12 +++++ .../mealient/datasource/ServerUrlProvider.kt | 6 +++ .../datasource/impl/AuthInterceptor.kt | 5 +- .../datasource/impl/BaseUrlInterceptor.kt | 41 +++++++++++++++ .../datasource/impl/OkHttpBuilderImpl.kt | 16 ++++-- .../datasource/v0/MealieDataSourceV0.kt | 7 --- .../datasource/v0/MealieDataSourceV0Impl.kt | 38 ++++++-------- .../mealient/datasource/v0/MealieServiceV0.kt | 25 ++++------ .../datasource/v1/MealieDataSourceV1.kt | 8 --- .../datasource/v1/MealieDataSourceV1Impl.kt | 43 +++++++--------- .../mealient/datasource/v1/MealieServiceV1.kt | 29 ++++------- .../datasource/MealieDataSourceV0ImplTest.kt | 29 ++++++----- 27 files changed, 265 insertions(+), 234 deletions(-) create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/LocalInterceptor.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/ServerUrlProvider.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/BaseUrlInterceptor.kt diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt index 8834372..6ccf14e 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt @@ -19,18 +19,16 @@ class AuthDataSourceImpl @Inject constructor( private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion() - private suspend fun getUrl(): String = serverInfoRepo.requireUrl() - override suspend fun authenticate( username: String, password: String, ): String = when (getVersion()) { - ServerVersion.V0 -> v0Source.authenticate(getUrl(), username, password) - ServerVersion.V1 -> v1Source.authenticate(getUrl(), username, password) + ServerVersion.V0 -> v0Source.authenticate(username, password) + ServerVersion.V1 -> v1Source.authenticate(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 + ServerVersion.V0 -> v0Source.createApiToken(CreateApiTokenRequestV0(name)) + ServerVersion.V1 -> v1Source.createApiToken(CreateApiTokenRequestV1(name)).token } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepo.kt index d4d7f42..153b483 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepo.kt @@ -4,10 +4,9 @@ interface ServerInfoRepo { suspend fun getUrl(): String? - suspend fun requireUrl(): String - suspend fun getVersion(): ServerVersion - suspend fun storeBaseURL(baseURL: String, version: String) + suspend fun tryBaseURL(baseURL: String): Result + } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoImpl.kt index f46c19a..fdd547b 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoImpl.kt @@ -1,6 +1,8 @@ package gq.kirmanak.mealient.data.baseurl import gq.kirmanak.mealient.datasource.NetworkError +import gq.kirmanak.mealient.datasource.ServerUrlProvider +import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject import javax.inject.Singleton @@ -10,7 +12,7 @@ class ServerInfoRepoImpl @Inject constructor( private val serverInfoStorage: ServerInfoStorage, private val versionDataSource: VersionDataSource, private val logger: Logger, -) : ServerInfoRepo { +) : ServerInfoRepo, ServerUrlProvider { override suspend fun getUrl(): String? { val result = serverInfoStorage.getBaseURL() @@ -18,17 +20,11 @@ class ServerInfoRepoImpl @Inject constructor( 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 + version = versionDataSource.getVersionInfo().version val result = determineServerVersion(version) serverInfoStorage.storeServerVersion(version) result @@ -45,8 +41,16 @@ class ServerInfoRepoImpl @Inject constructor( else -> throw NetworkError.NotMealie(IllegalStateException("Server version is unknown: $version")) } - override suspend fun storeBaseURL(baseURL: String, version: String) { - logger.v { "storeBaseURL() called with: baseURL = $baseURL, version = $version" } - serverInfoStorage.storeBaseURL(baseURL, version) + override suspend fun tryBaseURL(baseURL: String): Result { + val oldVersion = serverInfoStorage.getServerVersion() + val oldBaseUrl = serverInfoStorage.getBaseURL() + + return runCatchingExceptCancel { + serverInfoStorage.storeBaseURL(baseURL) + val version = versionDataSource.getVersionInfo().version + serverInfoStorage.storeServerVersion(version) + }.onFailure { + serverInfoStorage.storeBaseURL(oldBaseUrl, oldVersion) + } } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoStorage.kt index 5863b40..09b7f1a 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoStorage.kt @@ -4,9 +4,12 @@ interface ServerInfoStorage { suspend fun getBaseURL(): String? - suspend fun storeBaseURL(baseURL: String, version: String) + suspend fun storeBaseURL(baseURL: String) + + suspend fun storeBaseURL(baseURL: String?, version: String?) suspend fun storeServerVersion(version: String) suspend fun getServerVersion(): String? + } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt index 8af39a7..03909d9 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt @@ -2,5 +2,5 @@ package gq.kirmanak.mealient.data.baseurl interface VersionDataSource { - suspend fun getVersionInfo(baseUrl: String): VersionInfo + suspend fun getVersionInfo(): VersionInfo } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt index 7fd6be0..76df788 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt @@ -16,13 +16,13 @@ class VersionDataSourceImpl @Inject constructor( private val v1Source: MealieDataSourceV1, ) : VersionDataSource { - override suspend fun getVersionInfo(baseUrl: String): VersionInfo { + override suspend fun getVersionInfo(): VersionInfo { val responses = coroutineScope { val v0Deferred = async { - runCatchingExceptCancel { v0Source.getVersionInfo(baseUrl).toVersionInfo() } + runCatchingExceptCancel { v0Source.getVersionInfo().toVersionInfo() } } val v1Deferred = async { - runCatchingExceptCancel { v1Source.getVersionInfo(baseUrl).toVersionInfo() } + runCatchingExceptCancel { v1Source.getVersionInfo().toVersionInfo() } } listOf(v0Deferred, v1Deferred).awaitAll() } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/ServerInfoStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/ServerInfoStorageImpl.kt index 0f850f4..70e1f27 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/ServerInfoStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/ServerInfoStorageImpl.kt @@ -19,11 +19,28 @@ class ServerInfoStorageImpl @Inject constructor( override suspend fun getBaseURL(): String? = getValue(baseUrlKey) - override suspend fun storeBaseURL(baseURL: String, version: String) { - preferencesStorage.storeValues( - Pair(baseUrlKey, baseURL), - Pair(serverVersionKey, version), - ) + override suspend fun storeBaseURL(baseURL: String) { + preferencesStorage.storeValues(Pair(baseUrlKey, baseURL)) + preferencesStorage.removeValues(serverVersionKey) + } + + override suspend fun storeBaseURL(baseURL: String?, version: String?) { + when { + baseURL == null -> { + preferencesStorage.removeValues(baseUrlKey, serverVersionKey) + } + + version != null -> { + preferencesStorage.storeValues( + Pair(baseUrlKey, baseURL), Pair(serverVersionKey, version) + ) + } + + else -> { + preferencesStorage.removeValues(serverVersionKey) + preferencesStorage.storeValues(Pair(baseUrlKey, baseURL)) + } + } } override suspend fun getServerVersion(): String? = getValue(serverVersionKey) @@ -33,4 +50,5 @@ class ServerInfoStorageImpl @Inject constructor( } private suspend fun getValue(key: Preferences.Key): T? = preferencesStorage.getValue(key) + } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt index 3f48597..3731ca9 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt @@ -29,13 +29,11 @@ class MealieDataSourceWrapper @Inject constructor( private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion() - private suspend fun getUrl(): String = serverInfoRepo.requireUrl() - override suspend fun addRecipe(recipe: AddRecipeInfo): String = when (getVersion()) { - ServerVersion.V0 -> v0Source.addRecipe(getUrl(), recipe.toV0Request()) + ServerVersion.V0 -> v0Source.addRecipe(recipe.toV0Request()) ServerVersion.V1 -> { - val slug = v1Source.createRecipe(getUrl(), recipe.toV1CreateRequest()) - v1Source.updateRecipe(getUrl(), slug, recipe.toV1UpdateRequest()) + val slug = v1Source.createRecipe(recipe.toV1CreateRequest()) + v1Source.updateRecipe(slug, recipe.toV1UpdateRequest()) slug } } @@ -45,25 +43,25 @@ class MealieDataSourceWrapper @Inject constructor( limit: Int, ): List = when (getVersion()) { ServerVersion.V0 -> { - v0Source.requestRecipes(getUrl(), start, limit).map { it.toRecipeSummaryInfo() } + v0Source.requestRecipes(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(getUrl(), page, limit).map { it.toRecipeSummaryInfo() } + v1Source.requestRecipes(page, limit).map { it.toRecipeSummaryInfo() } } } override suspend fun requestRecipeInfo(slug: String): FullRecipeInfo = when (getVersion()) { - ServerVersion.V0 -> v0Source.requestRecipeInfo(getUrl(), slug).toFullRecipeInfo() - ServerVersion.V1 -> v1Source.requestRecipeInfo(getUrl(), slug).toFullRecipeInfo() + ServerVersion.V0 -> v0Source.requestRecipeInfo(slug).toFullRecipeInfo() + ServerVersion.V1 -> v1Source.requestRecipeInfo(slug).toFullRecipeInfo() } override suspend fun parseRecipeFromURL( parseRecipeURLInfo: ParseRecipeURLInfo, ): String = when (getVersion()) { - ServerVersion.V0 -> v0Source.parseRecipeFromURL(getUrl(), parseRecipeURLInfo.toV0Request()) - ServerVersion.V1 -> v1Source.parseRecipeFromURL(getUrl(), parseRecipeURLInfo.toV1Request()) + ServerVersion.V0 -> v0Source.parseRecipeFromURL(parseRecipeURLInfo.toV0Request()) + ServerVersion.V1 -> v1Source.parseRecipeFromURL(parseRecipeURLInfo.toV1Request()) } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt index 2e6f1a6..0f208d0 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt @@ -6,6 +6,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import gq.kirmanak.mealient.data.baseurl.* import gq.kirmanak.mealient.data.baseurl.impl.ServerInfoStorageImpl +import gq.kirmanak.mealient.datasource.ServerUrlProvider import javax.inject.Singleton @Module @@ -23,4 +24,8 @@ interface BaseURLModule { @Binds @Singleton fun bindServerInfoRepo(serverInfoRepoImpl: ServerInfoRepoImpl): ServerInfoRepo + + @Binds + @Singleton + fun bindServerUrlProvider(serverInfoRepoImpl: ServerInfoRepoImpl): ServerUrlProvider } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt index f532cc7..6330c7a 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt @@ -7,9 +7,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo -import gq.kirmanak.mealient.data.baseurl.VersionDataSource import gq.kirmanak.mealient.data.recipes.RecipeRepo -import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.OperationUiState import kotlinx.coroutines.launch @@ -20,7 +18,6 @@ class BaseURLViewModel @Inject constructor( private val serverInfoRepo: ServerInfoRepo, private val authRepo: AuthRepo, private val recipeRepo: RecipeRepo, - private val versionDataSource: VersionDataSource, private val logger: Logger, ) : ViewModel() { @@ -42,10 +39,8 @@ class BaseURLViewModel @Inject constructor( _uiState.value = OperationUiState.fromResult(Result.success(Unit)) return } - val result = runCatchingExceptCancel { - // If it returns proper version info then it must be a Mealie - val version = versionDataSource.getVersionInfo(baseURL).version - serverInfoRepo.storeBaseURL(baseURL, version) + val result = serverInfoRepo.tryBaseURL(baseURL) + if (result.isSuccess) { authRepo.logout() recipeRepo.clearLocalData() } diff --git a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt index fa38ed2..3bcefb2 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt @@ -9,7 +9,6 @@ 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_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD import gq.kirmanak.mealient.test.AuthImplTestData.TEST_SERVER_VERSION_V0 import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN @@ -54,7 +53,6 @@ class AuthRepoImplTest : BaseUnitTest() { fun `when authenticate successfully then saves to storage`() = runTest { coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0 coEvery { dataSource.authenticate(any(), any()) } returns TEST_TOKEN - coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL coEvery { dataSource.createApiToken(any()) } returns TEST_API_TOKEN subject.authenticate(TEST_USERNAME, TEST_PASSWORD) coVerify { @@ -69,7 +67,6 @@ class AuthRepoImplTest : BaseUnitTest() { @Test fun `when authenticate fails then does not change storage`() = runTest { coEvery { dataSource.authenticate(any(), any()) } throws RuntimeException() - coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL runCatchingExceptCancel { subject.authenticate("invalid", "") } confirmVerified(storage) } diff --git a/app/src/test/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoTest.kt index 1ce1936..17b2b65 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoTest.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test +import java.io.IOException @OptIn(ExperimentalCoroutinesApi::class) class ServerInfoRepoTest : BaseUnitTest() { @@ -44,12 +45,6 @@ class ServerInfoRepoTest : BaseUnitTest() { assertThat(subject.getUrl()).isEqualTo(expected) } - @Test(expected = IllegalStateException::class) - fun `when storage returns null url expect requireUrl to throw`() = runTest { - coEvery { storage.getBaseURL() } returns null - subject.requireUrl() - } - @Test fun `when getUrl expect storage is accessed`() = runTest { coEvery { storage.getBaseURL() } returns null @@ -58,32 +53,45 @@ class ServerInfoRepoTest : BaseUnitTest() { } @Test - fun `when requireUrl expect storage is accessed`() = runTest { - coEvery { storage.getBaseURL() } returns TEST_BASE_URL - subject.requireUrl() - coVerify { storage.getBaseURL() } + fun `when tryBaseURL succeeds expect call to storage`() = runTest { + coEvery { storage.getServerVersion() } returns null + coEvery { storage.getBaseURL() } returns null + coEvery { dataSource.getVersionInfo() } returns VersionInfo(TEST_VERSION) + subject.tryBaseURL(TEST_BASE_URL) + coVerify { + storage.storeBaseURL(eq(TEST_BASE_URL)) + dataSource.getVersionInfo() + storage.storeServerVersion(TEST_VERSION) + } } @Test - fun `when storeBaseUrl expect call to storage`() = runTest { - subject.storeBaseURL(TEST_BASE_URL, TEST_VERSION) - coVerify { storage.storeBaseURL(TEST_BASE_URL, TEST_VERSION) } + fun `when tryBaseURL fails expect call to storage`() = runTest { + coEvery { storage.getServerVersion() } returns "serverVersion" + coEvery { storage.getBaseURL() } returns "baseUrl" + coEvery { dataSource.getVersionInfo() } throws IOException() + subject.tryBaseURL(TEST_BASE_URL) + coVerify { + storage.storeBaseURL(eq(TEST_BASE_URL)) + dataSource.getVersionInfo() + storage.storeBaseURL(eq("baseUrl"), eq("serverVersion")) + } } @Test fun `when storage is empty expect getVersion to call data source`() = runTest { coEvery { storage.getServerVersion() } returns null coEvery { storage.getBaseURL() } returns TEST_BASE_URL - coEvery { dataSource.getVersionInfo(eq(TEST_BASE_URL)) } returns VERSION_INFO_V0 + coEvery { dataSource.getVersionInfo() } returns VERSION_INFO_V0 subject.getVersion() - coVerify { dataSource.getVersionInfo(eq(TEST_BASE_URL)) } + coVerify { dataSource.getVersionInfo() } } @Test fun `when storage is empty and data source has value expect getVersion to save it`() = runTest { coEvery { storage.getServerVersion() } returns null coEvery { storage.getBaseURL() } returns TEST_BASE_URL - coEvery { dataSource.getVersionInfo(eq(TEST_BASE_URL)) } returns VersionInfo(TEST_VERSION) + coEvery { dataSource.getVersionInfo() } returns VersionInfo(TEST_VERSION) subject.getVersion() coVerify { storage.storeServerVersion(TEST_VERSION) } } @@ -92,7 +100,7 @@ class ServerInfoRepoTest : BaseUnitTest() { fun `when data source has invalid value expect getVersion to throw`() = runTest { coEvery { storage.getServerVersion() } returns null coEvery { storage.getBaseURL() } returns TEST_BASE_URL - coEvery { dataSource.getVersionInfo(eq(TEST_BASE_URL)) } returns VersionInfo("v2.0.0") + coEvery { dataSource.getVersionInfo() } returns VersionInfo("v2.0.0") subject.getVersion() } @@ -100,7 +108,7 @@ class ServerInfoRepoTest : BaseUnitTest() { fun `when data source has invalid value expect getVersion not to save`() = runTest { coEvery { storage.getServerVersion() } returns null coEvery { storage.getBaseURL() } returns TEST_BASE_URL - coEvery { dataSource.getVersionInfo(eq(TEST_BASE_URL)) } returns VersionInfo("v2.0.0") + coEvery { dataSource.getVersionInfo() } returns VersionInfo("v2.0.0") subject.runCatching { getVersion() } coVerify(inverse = true) { storage.storeServerVersion(any()) } } @@ -116,7 +124,7 @@ class ServerInfoRepoTest : BaseUnitTest() { fun `when storage has value expect getVersion to not call data source`() = runTest { coEvery { storage.getServerVersion() } returns TEST_VERSION subject.getVersion() - coVerify(inverse = true) { dataSource.getVersionInfo(any()) } + coVerify(inverse = true) { dataSource.getVersionInfo() } } @Test @@ -135,7 +143,7 @@ class ServerInfoRepoTest : BaseUnitTest() { fun `when data source has valid v0 value expect getVersion to return it`() = runTest { coEvery { storage.getServerVersion() } returns null coEvery { storage.getBaseURL() } returns TEST_BASE_URL - coEvery { dataSource.getVersionInfo(eq(TEST_BASE_URL)) } returns VersionInfo("v0.5.6") + coEvery { dataSource.getVersionInfo() } returns VersionInfo("v0.5.6") assertThat(subject.getVersion()).isEqualTo(ServerVersion.V0) } @@ -143,7 +151,7 @@ class ServerInfoRepoTest : BaseUnitTest() { fun `when data source has valid v1 value expect getVersion to return it`() = runTest { coEvery { storage.getServerVersion() } returns null coEvery { storage.getBaseURL() } returns TEST_BASE_URL - coEvery { dataSource.getVersionInfo(eq(TEST_BASE_URL)) } returns VersionInfo("v1.0.0-beta05") + coEvery { dataSource.getVersionInfo() } returns VersionInfo("v1.0.0-beta05") assertThat(subject.getVersion()).isEqualTo(ServerVersion.V1) } } \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapperTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapperTest.kt index 464792c..418933f 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapperTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapperTest.kt @@ -6,7 +6,6 @@ import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0 import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 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_SERVER_VERSION_V0 import gq.kirmanak.mealient.test.AuthImplTestData.TEST_SERVER_VERSION_V1 import gq.kirmanak.mealient.test.BaseUnitTest @@ -55,15 +54,14 @@ class MealieDataSourceWrapperTest : BaseUnitTest() { fun `when server version v1 expect requestRecipeInfo to call v1`() = runTest { val slug = "porridge" coEvery { - v1Source.requestRecipeInfo(eq(TEST_BASE_URL), eq(slug)) + v1Source.requestRecipeInfo(eq(slug)) } returns PORRIDGE_RECIPE_RESPONSE_V1 coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1 - coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER val actual = subject.requestRecipeInfo(slug) - coVerify { v1Source.requestRecipeInfo(eq(TEST_BASE_URL), eq(slug)) } + coVerify { v1Source.requestRecipeInfo(eq(slug)) } assertThat(actual).isEqualTo(PORRIDGE_FULL_RECIPE_INFO) } @@ -71,10 +69,9 @@ class MealieDataSourceWrapperTest : BaseUnitTest() { @Test fun `when server version v1 expect requestRecipes to call v1`() = runTest { coEvery { - v1Source.requestRecipes(any(), any(), any()) + v1Source.requestRecipes(any(), any()) } returns listOf(PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1) coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1 - coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER val actual = subject.requestRecipes(40, 10) @@ -82,7 +79,7 @@ class MealieDataSourceWrapperTest : BaseUnitTest() { val page = 5 // 0-9 (1), 10-19 (2), 20-29 (3), 30-39 (4), 40-49 (5) val perPage = 10 coVerify { - v1Source.requestRecipes(eq(TEST_BASE_URL), eq(page), eq(perPage)) + v1Source.requestRecipes(eq(page), eq(perPage)) } assertThat(actual).isEqualTo(listOf(RECIPE_SUMMARY_PORRIDGE_V1)) @@ -91,10 +88,9 @@ class MealieDataSourceWrapperTest : BaseUnitTest() { @Test fun `when server version v0 expect requestRecipes to call v0`() = runTest { coEvery { - v0Source.requestRecipes(any(), any(), any()) + v0Source.requestRecipes(any(), any()) } returns listOf(PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0) coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0 - coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER val start = 40 @@ -102,7 +98,7 @@ class MealieDataSourceWrapperTest : BaseUnitTest() { val actual = subject.requestRecipes(start, limit) coVerify { - v0Source.requestRecipes(eq(TEST_BASE_URL), eq(start), eq(limit)) + v0Source.requestRecipes(eq(start), eq(limit)) } assertThat(actual).isEqualTo(listOf(RECIPE_SUMMARY_PORRIDGE_V0)) @@ -110,9 +106,8 @@ class MealieDataSourceWrapperTest : BaseUnitTest() { @Test(expected = IOException::class) fun `when request fails expect addRecipe to rethrow`() = runTest { - coEvery { v0Source.addRecipe(any(), any()) } throws IOException() + coEvery { v0Source.addRecipe(any()) } throws IOException() coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0 - coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER subject.addRecipe(PORRIDGE_ADD_RECIPE_INFO) } @@ -121,16 +116,14 @@ class MealieDataSourceWrapperTest : BaseUnitTest() { fun `when server version v0 expect addRecipe to call v0`() = runTest { val slug = "porridge" - coEvery { v0Source.addRecipe(any(), any()) } returns slug + coEvery { v0Source.addRecipe(any()) } returns slug coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0 - coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER val actual = subject.addRecipe(PORRIDGE_ADD_RECIPE_INFO) coVerify { v0Source.addRecipe( - eq(TEST_BASE_URL), eq(PORRIDGE_ADD_RECIPE_REQUEST_V0), ) } @@ -142,24 +135,21 @@ class MealieDataSourceWrapperTest : BaseUnitTest() { fun `when server version v1 expect addRecipe to call v1`() = runTest { val slug = "porridge" - coEvery { v1Source.createRecipe(any(), any()) } returns slug + coEvery { v1Source.createRecipe(any()) } returns slug coEvery { - v1Source.updateRecipe(any(), any(), any()) + v1Source.updateRecipe(any(), any()) } returns PORRIDGE_RECIPE_RESPONSE_V1 coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1 - coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER val actual = subject.addRecipe(PORRIDGE_ADD_RECIPE_INFO) coVerifySequence { v1Source.createRecipe( - eq(TEST_BASE_URL), eq(PORRIDGE_CREATE_RECIPE_REQUEST_V1), ) v1Source.updateRecipe( - eq(TEST_BASE_URL), eq(slug), eq(PORRIDGE_UPDATE_RECIPE_REQUEST_V1), ) diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt index 5d7bab7..c0fd1d2 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt @@ -3,11 +3,8 @@ package gq.kirmanak.mealient.ui.baseurl import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo -import gq.kirmanak.mealient.data.baseurl.VersionDataSource -import gq.kirmanak.mealient.data.baseurl.VersionInfo import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL -import gq.kirmanak.mealient.test.AuthImplTestData.TEST_VERSION import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.ui.OperationUiState import io.mockk.coEvery @@ -31,9 +28,6 @@ class BaseURLViewModelTest : BaseUnitTest() { @MockK(relaxUnitFun = true) lateinit var recipeRepo: RecipeRepo - @MockK - lateinit var versionDataSource: VersionDataSource - lateinit var subject: BaseURLViewModel @Before @@ -43,7 +37,6 @@ class BaseURLViewModelTest : BaseUnitTest() { serverInfoRepo = serverInfoRepo, authRepo = authRepo, recipeRepo = recipeRepo, - versionDataSource = versionDataSource, logger = logger, ) } @@ -51,13 +44,13 @@ class BaseURLViewModelTest : BaseUnitTest() { @Test fun `when saveBaseURL expect no version checks given that current URL matches new`() = runTest { setupSaveBaseUrlWithOldUrl() - coVerify(inverse = true) { versionDataSource.getVersionInfo(any()) } + coVerify(inverse = true) { serverInfoRepo.tryBaseURL(any()) } } @Test fun `when saveBaseURL expect URL isn't saved given that current URL matches new`() = runTest { setupSaveBaseUrlWithOldUrl() - coVerify(inverse = true) { serverInfoRepo.storeBaseURL(any(), any()) } + coVerify(inverse = true) { serverInfoRepo.tryBaseURL(any()) } } @Test @@ -74,7 +67,7 @@ class BaseURLViewModelTest : BaseUnitTest() { private fun TestScope.setupSaveBaseUrlWithOldUrl() { coEvery { serverInfoRepo.getUrl() } returns TEST_BASE_URL - versionDataSourceReturnsSuccess() + coEvery { serverInfoRepo.tryBaseURL(any()) } returns Result.success(Unit) subject.saveBaseUrl(TEST_BASE_URL) advanceUntilIdle() } @@ -82,7 +75,7 @@ class BaseURLViewModelTest : BaseUnitTest() { @Test fun `when saveBaseUrl expect URL is saved given that new URL doesn't match old`() = runTest { setupSaveBaseUrlWithNewUrl() - coVerify { serverInfoRepo.storeBaseURL(eq(TEST_BASE_URL), eq(TEST_VERSION)) } + coVerify { serverInfoRepo.tryBaseURL(eq(TEST_BASE_URL)) } } @Test @@ -99,21 +92,15 @@ class BaseURLViewModelTest : BaseUnitTest() { private fun TestScope.setupSaveBaseUrlWithNewUrl() { coEvery { serverInfoRepo.getUrl() } returns null - versionDataSourceReturnsSuccess() + coEvery { serverInfoRepo.tryBaseURL(any()) } returns Result.success(Unit) subject.saveBaseUrl(TEST_BASE_URL) advanceUntilIdle() } - private fun versionDataSourceReturnsSuccess() { - coEvery { - versionDataSource.getVersionInfo(eq(TEST_BASE_URL)) - } returns VersionInfo(TEST_VERSION) - } - @Test fun `when saveBaseURL expect error given that version can't be fetched`() = runTest { coEvery { serverInfoRepo.getUrl() } returns null - coEvery { versionDataSource.getVersionInfo(eq(TEST_BASE_URL)) } throws IOException() + coEvery { serverInfoRepo.tryBaseURL(any()) } returns Result.failure(IOException()) subject.saveBaseUrl(TEST_BASE_URL) advanceUntilIdle() assertThat(subject.uiState.value).isInstanceOf(OperationUiState.Failure::class.java) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt index 73ed120..9d09761 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt @@ -8,6 +8,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet import gq.kirmanak.mealient.datasource.impl.AuthInterceptor +import gq.kirmanak.mealient.datasource.impl.BaseUrlInterceptor import gq.kirmanak.mealient.datasource.impl.CacheBuilderImpl import gq.kirmanak.mealient.datasource.impl.NetworkRequestWrapperImpl import gq.kirmanak.mealient.datasource.impl.OkHttpBuilderImpl @@ -20,7 +21,6 @@ import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1Impl import gq.kirmanak.mealient.datasource.v1.MealieServiceV1 import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json -import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import retrofit2.Converter @@ -55,8 +55,11 @@ interface DataSourceModule { @Provides @Singleton - fun provideRetrofit(retrofitBuilder: RetrofitBuilder): Retrofit = - retrofitBuilder.buildRetrofit("https://beta.mealie.io/") + fun provideRetrofit(retrofitBuilder: RetrofitBuilder): Retrofit { + // Fake base URL which will be replaced later by BaseUrlInterceptor + // Solution was suggested here https://github.com/square/retrofit/issues/2161#issuecomment-274204152 + return retrofitBuilder.buildRetrofit("http://localhost/") + } @Provides @Singleton @@ -92,5 +95,10 @@ interface DataSourceModule { @Binds @Singleton @IntoSet - fun bindAuthInterceptor(authInterceptor: AuthInterceptor): Interceptor + fun bindAuthInterceptor(authInterceptor: AuthInterceptor): LocalInterceptor + + @Binds + @Singleton + @IntoSet + fun bindBaseUrlInterceptor(baseUrlInterceptor: BaseUrlInterceptor): LocalInterceptor } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/LocalInterceptor.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/LocalInterceptor.kt new file mode 100644 index 0000000..351a48a --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/LocalInterceptor.kt @@ -0,0 +1,12 @@ +package gq.kirmanak.mealient.datasource + +import okhttp3.Interceptor +import okhttp3.OkHttpClient + +/** + * Marker interface which is different from [Interceptor] only in how it is handled. + * [Interceptor]s are added as network interceptors to OkHttpClient whereas [LocalInterceptor]s + * are added via [OkHttpClient.Builder.addInterceptor] function. They will observe the + * full call lifecycle, whereas network interceptors will see only the network part. + */ +interface LocalInterceptor : Interceptor \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/ServerUrlProvider.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/ServerUrlProvider.kt new file mode 100644 index 0000000..bf18129 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/ServerUrlProvider.kt @@ -0,0 +1,6 @@ +package gq.kirmanak.mealient.datasource + +interface ServerUrlProvider { + + suspend fun getUrl(): String? +} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/AuthInterceptor.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/AuthInterceptor.kt index 89b2acf..b4fe9fc 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/AuthInterceptor.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/AuthInterceptor.kt @@ -2,6 +2,7 @@ package gq.kirmanak.mealient.datasource.impl import androidx.annotation.VisibleForTesting import gq.kirmanak.mealient.datasource.AuthenticationProvider +import gq.kirmanak.mealient.datasource.LocalInterceptor import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.runBlocking import okhttp3.Interceptor @@ -14,13 +15,13 @@ import javax.inject.Singleton class AuthInterceptor @Inject constructor( private val logger: Logger, private val authenticationProviderProvider: Provider, -) : Interceptor { +) : LocalInterceptor { private val authenticationProvider: AuthenticationProvider get() = authenticationProviderProvider.get() override fun intercept(chain: Interceptor.Chain): Response { - logger.v { "intercept() was called" } + logger.v { "intercept() was called with: request = ${chain.request()}" } val header = getAuthHeader() val request = chain.request().let { if (header == null) it else it.newBuilder().header(HEADER_NAME, header).build() diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/BaseUrlInterceptor.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/BaseUrlInterceptor.kt new file mode 100644 index 0000000..933e5d5 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/BaseUrlInterceptor.kt @@ -0,0 +1,41 @@ +package gq.kirmanak.mealient.datasource.impl + +import gq.kirmanak.mealient.datasource.LocalInterceptor +import gq.kirmanak.mealient.datasource.ServerUrlProvider +import gq.kirmanak.mealient.logging.Logger +import kotlinx.coroutines.runBlocking +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.Interceptor +import okhttp3.Response +import java.io.IOException +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton + +@Singleton +class BaseUrlInterceptor @Inject constructor( + private val serverUrlProviderProvider: Provider, + private val logger: Logger, +) : LocalInterceptor { + + private val serverUrlProvider: ServerUrlProvider + get() = serverUrlProviderProvider.get() + + override fun intercept(chain: Interceptor.Chain): Response { + logger.v { "intercept() was called with: request = ${chain.request()}" } + val oldRequest = chain.request() + val baseUrl = getBaseUrl() + val correctUrl = oldRequest.url + .newBuilder() + .host(baseUrl.host) + .scheme(baseUrl.scheme) + .build() + val newRequest = oldRequest.newBuilder().url(correctUrl).build() + logger.d { "Replaced ${oldRequest.url} with ${newRequest.url}" } + return chain.proceed(newRequest) + } + + private fun getBaseUrl() = runBlocking { + serverUrlProvider.getUrl()?.toHttpUrlOrNull() ?: throw IOException("Base URL is unknown") + } +} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/OkHttpBuilderImpl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/OkHttpBuilderImpl.kt index 21ca1d1..13b532f 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/OkHttpBuilderImpl.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/OkHttpBuilderImpl.kt @@ -1,7 +1,9 @@ package gq.kirmanak.mealient.datasource.impl import gq.kirmanak.mealient.datasource.CacheBuilder +import gq.kirmanak.mealient.datasource.LocalInterceptor import gq.kirmanak.mealient.datasource.OkHttpBuilder +import gq.kirmanak.mealient.logging.Logger import okhttp3.Interceptor import okhttp3.OkHttpClient import javax.inject.Inject @@ -12,10 +14,16 @@ class OkHttpBuilderImpl @Inject constructor( private val cacheBuilder: CacheBuilder, // Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382) private val interceptors: Set<@JvmSuppressWildcards Interceptor>, + private val localInterceptors: Set<@JvmSuppressWildcards LocalInterceptor>, + private val logger: Logger, ) : OkHttpBuilder { - override fun buildOkHttp(): OkHttpClient = OkHttpClient.Builder() - .apply { interceptors.forEach(::addNetworkInterceptor) } - .cache(cacheBuilder.buildCache()) - .build() + override fun buildOkHttp(): OkHttpClient { + logger.v { "buildOkHttp() was called with cacheBuilder = $cacheBuilder, interceptors = $interceptors, localInterceptors = $localInterceptors" } + return OkHttpClient.Builder().apply { + localInterceptors.forEach(::addInterceptor) + interceptors.forEach(::addNetworkInterceptor) + cache(cacheBuilder.buildCache()) + }.build() + } } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0.kt index 92b70f6..01aeac1 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0.kt @@ -10,7 +10,6 @@ import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0 interface MealieDataSourceV0 { suspend fun addRecipe( - baseUrl: String, recipe: AddRecipeRequestV0, ): String @@ -18,33 +17,27 @@ interface MealieDataSourceV0 { * Tries to acquire authentication token using the provided credentials */ suspend fun authenticate( - baseUrl: String, username: String, password: String, ): String suspend fun getVersionInfo( - baseUrl: String, ): VersionResponseV0 suspend fun requestRecipes( - baseUrl: String, start: Int, limit: Int, ): List suspend fun requestRecipeInfo( - baseUrl: String, slug: String, ): GetRecipeResponseV0 suspend fun parseRecipeFromURL( - baseUrl: String, request: ParseRecipeURLRequestV0, ): String suspend fun createApiToken( - baseUrl: String, request: CreateApiTokenRequestV0, ): String } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt index 473a3f4..7d145be 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt @@ -26,34 +26,30 @@ class MealieDataSourceV0Impl @Inject constructor( ) : MealieDataSourceV0 { override suspend fun addRecipe( - baseUrl: String, recipe: AddRecipeRequestV0, ): String = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.addRecipe("$baseUrl/api/recipes/create", recipe) }, + block = { service.addRecipe(recipe) }, logMethod = { "addRecipe" }, - logParameters = { "baseUrl = $baseUrl, recipe = $recipe" } + logParameters = { "recipe = $recipe" } ) override suspend fun authenticate( - baseUrl: String, username: String, password: String, ): String = networkRequestWrapper.makeCall( - block = { service.getToken("$baseUrl/api/auth/token", username, password) }, + block = { service.getToken(username, password) }, logMethod = { "authenticate" }, - logParameters = { "baseUrl = $baseUrl, username = $username, password = $password" } + logParameters = { "username = $username, password = $password" } ).map { it.accessToken }.getOrElse { val errorBody = (it as? HttpException)?.response()?.errorBody() ?: throw it val errorDetailV0 = errorBody.decode(json) throw if (errorDetailV0.detail == "Unauthorized") NetworkError.Unauthorized(it) else it } - override suspend fun getVersionInfo( - baseUrl: String - ): VersionResponseV0 = networkRequestWrapper.makeCall( - block = { service.getVersion("$baseUrl/api/debug/version") }, + override suspend fun getVersionInfo(): VersionResponseV0 = networkRequestWrapper.makeCall( + block = { service.getVersion() }, logMethod = { "getVersionInfo" }, - logParameters = { "baseUrl = $baseUrl" }, + logParameters = { "" }, ).getOrElse { throw when (it) { is HttpException, is SerializationException -> NetworkError.NotMealie(it) @@ -63,39 +59,35 @@ class MealieDataSourceV0Impl @Inject constructor( } override suspend fun requestRecipes( - baseUrl: String, start: Int, limit: Int, ): List = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.getRecipeSummary("$baseUrl/api/recipes/summary", start, limit) }, + block = { service.getRecipeSummary(start, limit) }, logMethod = { "requestRecipes" }, - logParameters = { "baseUrl = $baseUrl, start = $start, limit = $limit" } + logParameters = { "start = $start, limit = $limit" } ) override suspend fun requestRecipeInfo( - baseUrl: String, slug: String, ): GetRecipeResponseV0 = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.getRecipe("$baseUrl/api/recipes/$slug") }, + block = { service.getRecipe(slug) }, logMethod = { "requestRecipeInfo" }, - logParameters = { "baseUrl = $baseUrl, slug = $slug" } + logParameters = { "slug = $slug" } ) override suspend fun parseRecipeFromURL( - baseUrl: String, request: ParseRecipeURLRequestV0 ): String = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", request) }, + block = { service.createRecipeFromURL(request) }, logMethod = { "parseRecipeFromURL" }, - logParameters = { "baseUrl = $baseUrl, request = $request" }, + logParameters = { "request = $request" }, ) override suspend fun createApiToken( - baseUrl: String, request: CreateApiTokenRequestV0, ): String = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.createApiToken("$baseUrl/api/users/api-tokens", request) }, + block = { service.createApiToken(request) }, logMethod = { "createApiToken" }, - logParameters = { "baseUrl = $baseUrl, request = $request" } + logParameters = { "request = $request" } ) } diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt index 79b8ef7..a5626f0 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt @@ -6,45 +6,38 @@ import retrofit2.http.* interface MealieServiceV0 { @FormUrlEncoded - @POST + @POST("/api/auth/token") suspend fun getToken( - @Url url: String, @Field("username") username: String, @Field("password") password: String, ): GetTokenResponseV0 - @POST + @POST("/api/recipes/create") suspend fun addRecipe( - @Url url: String, @Body addRecipeRequestV0: AddRecipeRequestV0, ): String - @GET - suspend fun getVersion( - @Url url: String, - ): VersionResponseV0 + @GET("/api/debug/version") + suspend fun getVersion(): VersionResponseV0 - @GET + @GET("/api/recipes/summary") suspend fun getRecipeSummary( - @Url url: String, @Query("start") start: Int, @Query("limit") limit: Int, ): List - @GET + @GET("/api/recipes/{slug}") suspend fun getRecipe( - @Url url: String, + @Path("slug") slug: String, ): GetRecipeResponseV0 - @POST + @POST("/api/recipes/create-url") suspend fun createRecipeFromURL( - @Url url: String, @Body request: ParseRecipeURLRequestV0, ): String - @POST + @POST("/api/users/api-tokens") suspend fun createApiToken( - @Url url: String, @Body request: CreateApiTokenRequestV0, ): String } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt index 5bc2b02..332000e 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt @@ -12,12 +12,10 @@ import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1 interface MealieDataSourceV1 { suspend fun createRecipe( - baseUrl: String, recipe: CreateRecipeRequestV1, ): String suspend fun updateRecipe( - baseUrl: String, slug: String, recipe: UpdateRecipeRequestV1, ): GetRecipeResponseV1 @@ -26,33 +24,27 @@ interface MealieDataSourceV1 { * Tries to acquire authentication token using the provided credentials */ suspend fun authenticate( - baseUrl: String, username: String, password: String, ): String suspend fun getVersionInfo( - baseUrl: String, ): VersionResponseV1 suspend fun requestRecipes( - baseUrl: String, page: Int, perPage: Int, ): List suspend fun requestRecipeInfo( - baseUrl: String, slug: String, ): GetRecipeResponseV1 suspend fun parseRecipeFromURL( - baseUrl: String, request: ParseRecipeURLRequestV1, ): String suspend fun createApiToken( - baseUrl: String, request: CreateApiTokenRequestV1, ): CreateApiTokenResponseV1 } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt index aa009d8..18105f0 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt @@ -28,44 +28,39 @@ class MealieDataSourceV1Impl @Inject constructor( ) : MealieDataSourceV1 { override suspend fun createRecipe( - baseUrl: String, recipe: CreateRecipeRequestV1 ): String = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.createRecipe("$baseUrl/api/recipes", recipe) }, + block = { service.createRecipe(recipe) }, logMethod = { "createRecipe" }, - logParameters = { "baseUrl = $baseUrl, recipe = $recipe" } + logParameters = { "recipe = $recipe" } ) override suspend fun updateRecipe( - baseUrl: String, slug: String, recipe: UpdateRecipeRequestV1 ): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.updateRecipe("$baseUrl/api/recipes/$slug", recipe) }, + block = { service.updateRecipe(recipe, slug) }, logMethod = { "updateRecipe" }, - logParameters = { "baseUrl = $baseUrl, slug = $slug, recipe = $recipe" } + logParameters = { "slug = $slug, recipe = $recipe" } ) override suspend fun authenticate( - baseUrl: String, username: String, password: String, ): String = networkRequestWrapper.makeCall( - block = { service.getToken("$baseUrl/api/auth/token", username, password) }, + block = { service.getToken(username, password) }, logMethod = { "authenticate" }, - logParameters = { "baseUrl = $baseUrl, username = $username, password = $password" } + logParameters = { "username = $username, password = $password" } ).map { it.accessToken }.getOrElse { val errorBody = (it as? HttpException)?.response()?.errorBody() ?: throw it val errorDetailV0 = errorBody.decode(json) throw if (errorDetailV0.detail == "Unauthorized") NetworkError.Unauthorized(it) else it } - override suspend fun getVersionInfo( - baseUrl: String, - ): VersionResponseV1 = networkRequestWrapper.makeCall( - block = { service.getVersion("$baseUrl/api/app/about") }, + override suspend fun getVersionInfo(): VersionResponseV1 = networkRequestWrapper.makeCall( + block = { service.getVersion() }, logMethod = { "getVersionInfo" }, - logParameters = { "baseUrl = $baseUrl" }, + logParameters = { "" }, ).getOrElse { throw when (it) { is HttpException, is SerializationException -> NetworkError.NotMealie(it) @@ -75,40 +70,36 @@ class MealieDataSourceV1Impl @Inject constructor( } override suspend fun requestRecipes( - baseUrl: String, page: Int, perPage: Int ): List = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.getRecipeSummary("$baseUrl/api/recipes", page, perPage) }, + block = { service.getRecipeSummary(page, perPage) }, logMethod = { "requestRecipes" }, - logParameters = { "baseUrl = $baseUrl, page = $page, perPage = $perPage" } + logParameters = { "page = $page, perPage = $perPage" } ).items override suspend fun requestRecipeInfo( - baseUrl: String, slug: String ): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.getRecipe("$baseUrl/api/recipes/$slug") }, + block = { service.getRecipe(slug) }, logMethod = { "requestRecipeInfo" }, - logParameters = { "baseUrl = $baseUrl, slug = $slug" } + logParameters = { "slug = $slug" } ) override suspend fun parseRecipeFromURL( - baseUrl: String, request: ParseRecipeURLRequestV1 ): String = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", request) }, + block = { service.createRecipeFromURL(request) }, logMethod = { "parseRecipeFromURL" }, - logParameters = { "baseUrl = $baseUrl, request = $request" } + logParameters = { "request = $request" } ) override suspend fun createApiToken( - baseUrl: String, request: CreateApiTokenRequestV1 ): CreateApiTokenResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.createApiToken("$baseUrl/api/users/api-tokens", request) }, + block = { service.createApiToken(request) }, logMethod = { "createApiToken" }, - logParameters = { "baseUrl = $baseUrl, request = $request" } + logParameters = { "request = $request" } ) } diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt index 85271cf..a91ef39 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt @@ -6,51 +6,44 @@ import retrofit2.http.* interface MealieServiceV1 { @FormUrlEncoded - @POST + @POST("/api/auth/token") suspend fun getToken( - @Url url: String, @Field("username") username: String, @Field("password") password: String, ): GetTokenResponseV1 - @POST + @POST("/api/recipes") suspend fun createRecipe( - @Url url: String, @Body addRecipeRequest: CreateRecipeRequestV1, ): String - @PATCH + @PATCH("/api/recipes/{slug}") suspend fun updateRecipe( - @Url url: String, @Body addRecipeRequest: UpdateRecipeRequestV1, + @Path("slug") slug: String, ): GetRecipeResponseV1 - @GET - suspend fun getVersion( - @Url url: String, - ): VersionResponseV1 + @GET("/api/app/about") + suspend fun getVersion(): VersionResponseV1 - @GET + @GET("/api/recipes") suspend fun getRecipeSummary( - @Url url: String, @Query("page") page: Int, @Query("perPage") perPage: Int, ): GetRecipesResponseV1 - @GET + @GET("/api/recipes/{slug}") suspend fun getRecipe( - @Url url: String, + @Path("slug") slug: String, ): GetRecipeResponseV1 - @POST + @POST("/api/recipes/create-url") suspend fun createRecipeFromURL( - @Url url: String, @Body request: ParseRecipeURLRequestV1, ): String - @POST + @POST("/api/users/api-tokens") suspend fun createApiToken( - @Url url: String, @Body request: CreateApiTokenRequestV1, ): CreateApiTokenResponseV1 } \ No newline at end of file diff --git a/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceV0ImplTest.kt b/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceV0ImplTest.kt index 43a3527..0f2528a 100644 --- a/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceV0ImplTest.kt +++ b/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceV0ImplTest.kt @@ -38,34 +38,34 @@ class MealieDataSourceV0ImplTest : BaseUnitTest() { @Test(expected = NetworkError.NotMealie::class) fun `when getVersionInfo and getVersion throws HttpException then NotMealie`() = runTest { val error = HttpException(Response.error(404, "".toJsonResponseBody())) - coEvery { service.getVersion(any()) } throws error - subject.getVersionInfo(TEST_BASE_URL) + coEvery { service.getVersion() } throws error + subject.getVersionInfo() } @Test(expected = NetworkError.NotMealie::class) fun `when getVersionInfo and getVersion throws SerializationException then NotMealie`() = runTest { - coEvery { service.getVersion(any()) } throws SerializationException() - subject.getVersionInfo(TEST_BASE_URL) + coEvery { service.getVersion() } throws SerializationException() + subject.getVersionInfo() } @Test(expected = NetworkError.NoServerConnection::class) fun `when getVersionInfo and getVersion throws IOException then NoServerConnection`() = runTest { - coEvery { service.getVersion(any()) } throws ConnectException() - subject.getVersionInfo(TEST_BASE_URL) + coEvery { service.getVersion() } throws ConnectException() + subject.getVersionInfo() } @Test fun `when getVersionInfo and getVersion returns result then result`() = runTest { val versionResponse = VersionResponseV0("v0.5.6") - coEvery { service.getVersion(any()) } returns versionResponse - assertThat(subject.getVersionInfo(TEST_BASE_URL)).isSameInstanceAs(versionResponse) + coEvery { service.getVersion() } returns versionResponse + assertThat(subject.getVersionInfo()).isSameInstanceAs(versionResponse) } @Test fun `when authentication is successful then token is correct`() = runTest { - coEvery { service.getToken(any(), any(), any()) } returns GetTokenResponseV0(TEST_TOKEN) + coEvery { service.getToken(any(), any()) } returns GetTokenResponseV0(TEST_TOKEN) assertThat(callAuthenticate()).isEqualTo(TEST_TOKEN) } @@ -73,7 +73,7 @@ class MealieDataSourceV0ImplTest : BaseUnitTest() { fun `when authenticate receives 401 and Unauthorized then throws Unauthorized`() = runTest { val body = "{\"detail\":\"Unauthorized\"}".toJsonResponseBody() coEvery { - service.getToken(any(), any(), any()) + service.getToken(any(), any()) } throws HttpException(Response.error(401, body)) callAuthenticate() } @@ -82,7 +82,7 @@ class MealieDataSourceV0ImplTest : BaseUnitTest() { fun `when authenticate receives 401 but not Unauthorized then throws NotMealie`() = runTest { val body = "{\"detail\":\"Something\"}".toJsonResponseBody() coEvery { - service.getToken(any(), any(), any()) + service.getToken(any(), any()) } throws HttpException(Response.error(401, body)) callAuthenticate() } @@ -91,22 +91,21 @@ class MealieDataSourceV0ImplTest : BaseUnitTest() { fun `when authenticate receives 404 and empty body then throws NotMealie`() = runTest { val body = "".toJsonResponseBody() coEvery { - service.getToken(any(), any(), any()) + service.getToken(any(), any()) } throws HttpException(Response.error(401, body)) callAuthenticate() } @Test(expected = IOException::class) fun `when authenticate and getToken throws then throws NoServerConnection`() = runTest { - coEvery { service.getToken(any(), any(), any()) } throws IOException("Server not found") + coEvery { service.getToken(any(), any()) } throws IOException("Server not found") callAuthenticate() } private suspend fun callAuthenticate(): String = - subject.authenticate(TEST_USERNAME, TEST_PASSWORD, TEST_BASE_URL) + subject.authenticate(TEST_PASSWORD, TEST_BASE_URL) companion object { - const val TEST_USERNAME = "TEST_USERNAME" const val TEST_PASSWORD = "TEST_PASSWORD" const val TEST_BASE_URL = "https://example.com/" const val TEST_TOKEN = "TEST_TOKEN" From f1ee255c9f44a1903864e2db4a36d6a3d2a8672f Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 11 Dec 2022 20:32:02 +0100 Subject: [PATCH 2/2] Revert "Fix binding set of interceptors twice in release" This reverts commit 31089eb499df6fdaa5637d355b5085d1f417aeb7. --- .../gq/kirmanak/mealient/ReleaseModule.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 datasource/src/release/java/gq/kirmanak/mealient/ReleaseModule.kt diff --git a/datasource/src/release/java/gq/kirmanak/mealient/ReleaseModule.kt b/datasource/src/release/java/gq/kirmanak/mealient/ReleaseModule.kt new file mode 100644 index 0000000..1be726e --- /dev/null +++ b/datasource/src/release/java/gq/kirmanak/mealient/ReleaseModule.kt @@ -0,0 +1,20 @@ +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() +}