Set base url through Interceptor

This commit is contained in:
Kirill Kamakin
2022-12-11 20:22:36 +01:00
parent 85b863227d
commit f6c0e862fc
27 changed files with 265 additions and 234 deletions

View File

@@ -19,18 +19,16 @@ class AuthDataSourceImpl @Inject constructor(
private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion() private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion()
private suspend fun getUrl(): String = serverInfoRepo.requireUrl()
override suspend fun authenticate( override suspend fun authenticate(
username: String, username: String,
password: String, password: String,
): String = when (getVersion()) { ): String = when (getVersion()) {
ServerVersion.V0 -> v0Source.authenticate(getUrl(), username, password) ServerVersion.V0 -> v0Source.authenticate(username, password)
ServerVersion.V1 -> v1Source.authenticate(getUrl(), username, password) ServerVersion.V1 -> v1Source.authenticate(username, password)
} }
override suspend fun createApiToken(name: String): String = when (getVersion()) { override suspend fun createApiToken(name: String): String = when (getVersion()) {
ServerVersion.V0 -> v0Source.createApiToken(getUrl(), CreateApiTokenRequestV0(name)) ServerVersion.V0 -> v0Source.createApiToken(CreateApiTokenRequestV0(name))
ServerVersion.V1 -> v1Source.createApiToken(getUrl(), CreateApiTokenRequestV1(name)).token ServerVersion.V1 -> v1Source.createApiToken(CreateApiTokenRequestV1(name)).token
} }
} }

View File

@@ -4,10 +4,9 @@ interface ServerInfoRepo {
suspend fun getUrl(): String? suspend fun getUrl(): String?
suspend fun requireUrl(): String
suspend fun getVersion(): ServerVersion suspend fun getVersion(): ServerVersion
suspend fun storeBaseURL(baseURL: String, version: String) suspend fun tryBaseURL(baseURL: String): Result<Unit>
} }

View File

@@ -1,6 +1,8 @@
package gq.kirmanak.mealient.data.baseurl package gq.kirmanak.mealient.data.baseurl
import gq.kirmanak.mealient.datasource.NetworkError 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 gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -10,7 +12,7 @@ class ServerInfoRepoImpl @Inject constructor(
private val serverInfoStorage: ServerInfoStorage, private val serverInfoStorage: ServerInfoStorage,
private val versionDataSource: VersionDataSource, private val versionDataSource: VersionDataSource,
private val logger: Logger, private val logger: Logger,
) : ServerInfoRepo { ) : ServerInfoRepo, ServerUrlProvider {
override suspend fun getUrl(): String? { override suspend fun getUrl(): String? {
val result = serverInfoStorage.getBaseURL() val result = serverInfoStorage.getBaseURL()
@@ -18,17 +20,11 @@ class ServerInfoRepoImpl @Inject constructor(
return 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 { override suspend fun getVersion(): ServerVersion {
var version = serverInfoStorage.getServerVersion() var version = serverInfoStorage.getServerVersion()
val serverVersion = if (version == null) { val serverVersion = if (version == null) {
logger.d { "getVersion: version is null, requesting" } logger.d { "getVersion: version is null, requesting" }
version = versionDataSource.getVersionInfo(requireUrl()).version version = versionDataSource.getVersionInfo().version
val result = determineServerVersion(version) val result = determineServerVersion(version)
serverInfoStorage.storeServerVersion(version) serverInfoStorage.storeServerVersion(version)
result result
@@ -45,8 +41,16 @@ class ServerInfoRepoImpl @Inject constructor(
else -> throw NetworkError.NotMealie(IllegalStateException("Server version is unknown: $version")) else -> throw NetworkError.NotMealie(IllegalStateException("Server version is unknown: $version"))
} }
override suspend fun storeBaseURL(baseURL: String, version: String) { override suspend fun tryBaseURL(baseURL: String): Result<Unit> {
logger.v { "storeBaseURL() called with: baseURL = $baseURL, version = $version" } val oldVersion = serverInfoStorage.getServerVersion()
serverInfoStorage.storeBaseURL(baseURL, version) val oldBaseUrl = serverInfoStorage.getBaseURL()
return runCatchingExceptCancel {
serverInfoStorage.storeBaseURL(baseURL)
val version = versionDataSource.getVersionInfo().version
serverInfoStorage.storeServerVersion(version)
}.onFailure {
serverInfoStorage.storeBaseURL(oldBaseUrl, oldVersion)
}
} }
} }

View File

@@ -4,9 +4,12 @@ interface ServerInfoStorage {
suspend fun getBaseURL(): String? 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 storeServerVersion(version: String)
suspend fun getServerVersion(): String? suspend fun getServerVersion(): String?
} }

View File

@@ -2,5 +2,5 @@ package gq.kirmanak.mealient.data.baseurl
interface VersionDataSource { interface VersionDataSource {
suspend fun getVersionInfo(baseUrl: String): VersionInfo suspend fun getVersionInfo(): VersionInfo
} }

View File

@@ -16,13 +16,13 @@ class VersionDataSourceImpl @Inject constructor(
private val v1Source: MealieDataSourceV1, private val v1Source: MealieDataSourceV1,
) : VersionDataSource { ) : VersionDataSource {
override suspend fun getVersionInfo(baseUrl: String): VersionInfo { override suspend fun getVersionInfo(): VersionInfo {
val responses = coroutineScope { val responses = coroutineScope {
val v0Deferred = async { val v0Deferred = async {
runCatchingExceptCancel { v0Source.getVersionInfo(baseUrl).toVersionInfo() } runCatchingExceptCancel { v0Source.getVersionInfo().toVersionInfo() }
} }
val v1Deferred = async { val v1Deferred = async {
runCatchingExceptCancel { v1Source.getVersionInfo(baseUrl).toVersionInfo() } runCatchingExceptCancel { v1Source.getVersionInfo().toVersionInfo() }
} }
listOf(v0Deferred, v1Deferred).awaitAll() listOf(v0Deferred, v1Deferred).awaitAll()
} }

View File

@@ -19,11 +19,28 @@ class ServerInfoStorageImpl @Inject constructor(
override suspend fun getBaseURL(): String? = getValue(baseUrlKey) override suspend fun getBaseURL(): String? = getValue(baseUrlKey)
override suspend fun storeBaseURL(baseURL: String, version: String) { override suspend fun storeBaseURL(baseURL: String) {
preferencesStorage.storeValues( preferencesStorage.storeValues(Pair(baseUrlKey, baseURL))
Pair(baseUrlKey, baseURL), preferencesStorage.removeValues(serverVersionKey)
Pair(serverVersionKey, version), }
)
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) override suspend fun getServerVersion(): String? = getValue(serverVersionKey)
@@ -33,4 +50,5 @@ class ServerInfoStorageImpl @Inject constructor(
} }
private suspend fun <T> getValue(key: Preferences.Key<T>): T? = preferencesStorage.getValue(key) private suspend fun <T> getValue(key: Preferences.Key<T>): T? = preferencesStorage.getValue(key)
} }

View File

@@ -29,13 +29,11 @@ class MealieDataSourceWrapper @Inject constructor(
private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion() private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion()
private suspend fun getUrl(): String = serverInfoRepo.requireUrl()
override suspend fun addRecipe(recipe: AddRecipeInfo): String = when (getVersion()) { override suspend fun addRecipe(recipe: AddRecipeInfo): String = when (getVersion()) {
ServerVersion.V0 -> v0Source.addRecipe(getUrl(), recipe.toV0Request()) ServerVersion.V0 -> v0Source.addRecipe(recipe.toV0Request())
ServerVersion.V1 -> { ServerVersion.V1 -> {
val slug = v1Source.createRecipe(getUrl(), recipe.toV1CreateRequest()) val slug = v1Source.createRecipe(recipe.toV1CreateRequest())
v1Source.updateRecipe(getUrl(), slug, recipe.toV1UpdateRequest()) v1Source.updateRecipe(slug, recipe.toV1UpdateRequest())
slug slug
} }
} }
@@ -45,25 +43,25 @@ class MealieDataSourceWrapper @Inject constructor(
limit: Int, limit: Int,
): List<RecipeSummaryInfo> = when (getVersion()) { ): List<RecipeSummaryInfo> = when (getVersion()) {
ServerVersion.V0 -> { ServerVersion.V0 -> {
v0Source.requestRecipes(getUrl(), start, limit).map { it.toRecipeSummaryInfo() } v0Source.requestRecipes(start, limit).map { it.toRecipeSummaryInfo() }
} }
ServerVersion.V1 -> { ServerVersion.V1 -> {
// Imagine start is 30 and limit is 15. It means that we already have page 1 and 2, now we need page 3 // Imagine start is 30 and limit is 15. It means that we already have page 1 and 2, now we need page 3
val page = start / limit + 1 val page = start / limit + 1
v1Source.requestRecipes(getUrl(), page, limit).map { it.toRecipeSummaryInfo() } v1Source.requestRecipes(page, limit).map { it.toRecipeSummaryInfo() }
} }
} }
override suspend fun requestRecipeInfo(slug: String): FullRecipeInfo = when (getVersion()) { override suspend fun requestRecipeInfo(slug: String): FullRecipeInfo = when (getVersion()) {
ServerVersion.V0 -> v0Source.requestRecipeInfo(getUrl(), slug).toFullRecipeInfo() ServerVersion.V0 -> v0Source.requestRecipeInfo(slug).toFullRecipeInfo()
ServerVersion.V1 -> v1Source.requestRecipeInfo(getUrl(), slug).toFullRecipeInfo() ServerVersion.V1 -> v1Source.requestRecipeInfo(slug).toFullRecipeInfo()
} }
override suspend fun parseRecipeFromURL( override suspend fun parseRecipeFromURL(
parseRecipeURLInfo: ParseRecipeURLInfo, parseRecipeURLInfo: ParseRecipeURLInfo,
): String = when (getVersion()) { ): String = when (getVersion()) {
ServerVersion.V0 -> v0Source.parseRecipeFromURL(getUrl(), parseRecipeURLInfo.toV0Request()) ServerVersion.V0 -> v0Source.parseRecipeFromURL(parseRecipeURLInfo.toV0Request())
ServerVersion.V1 -> v1Source.parseRecipeFromURL(getUrl(), parseRecipeURLInfo.toV1Request()) ServerVersion.V1 -> v1Source.parseRecipeFromURL(parseRecipeURLInfo.toV1Request())
} }
} }

View File

@@ -6,6 +6,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import gq.kirmanak.mealient.data.baseurl.* import gq.kirmanak.mealient.data.baseurl.*
import gq.kirmanak.mealient.data.baseurl.impl.ServerInfoStorageImpl import gq.kirmanak.mealient.data.baseurl.impl.ServerInfoStorageImpl
import gq.kirmanak.mealient.datasource.ServerUrlProvider
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@@ -23,4 +24,8 @@ interface BaseURLModule {
@Binds @Binds
@Singleton @Singleton
fun bindServerInfoRepo(serverInfoRepoImpl: ServerInfoRepoImpl): ServerInfoRepo fun bindServerInfoRepo(serverInfoRepoImpl: ServerInfoRepoImpl): ServerInfoRepo
@Binds
@Singleton
fun bindServerUrlProvider(serverInfoRepoImpl: ServerInfoRepoImpl): ServerUrlProvider
} }

View File

@@ -7,9 +7,7 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.data.baseurl.VersionDataSource
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -20,7 +18,6 @@ class BaseURLViewModel @Inject constructor(
private val serverInfoRepo: ServerInfoRepo, private val serverInfoRepo: ServerInfoRepo,
private val authRepo: AuthRepo, private val authRepo: AuthRepo,
private val recipeRepo: RecipeRepo, private val recipeRepo: RecipeRepo,
private val versionDataSource: VersionDataSource,
private val logger: Logger, private val logger: Logger,
) : ViewModel() { ) : ViewModel() {
@@ -42,10 +39,8 @@ class BaseURLViewModel @Inject constructor(
_uiState.value = OperationUiState.fromResult(Result.success(Unit)) _uiState.value = OperationUiState.fromResult(Result.success(Unit))
return return
} }
val result = runCatchingExceptCancel { val result = serverInfoRepo.tryBaseURL(baseURL)
// If it returns proper version info then it must be a Mealie if (result.isSuccess) {
val version = versionDataSource.getVersionInfo(baseURL).version
serverInfoRepo.storeBaseURL(baseURL, version)
authRepo.logout() authRepo.logout()
recipeRepo.clearLocalData() recipeRepo.clearLocalData()
} }

View File

@@ -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_AUTH_HEADER
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_API_TOKEN import gq.kirmanak.mealient.test.AuthImplTestData.TEST_API_TOKEN
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD 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_SERVER_VERSION_V0
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN
@@ -54,7 +53,6 @@ class AuthRepoImplTest : BaseUnitTest() {
fun `when authenticate successfully then saves to storage`() = runTest { fun `when authenticate successfully then saves to storage`() = runTest {
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0 coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
coEvery { dataSource.authenticate(any(), any()) } returns TEST_TOKEN coEvery { dataSource.authenticate(any(), any()) } returns TEST_TOKEN
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
coEvery { dataSource.createApiToken(any()) } returns TEST_API_TOKEN coEvery { dataSource.createApiToken(any()) } returns TEST_API_TOKEN
subject.authenticate(TEST_USERNAME, TEST_PASSWORD) subject.authenticate(TEST_USERNAME, TEST_PASSWORD)
coVerify { coVerify {
@@ -69,7 +67,6 @@ class AuthRepoImplTest : BaseUnitTest() {
@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()) } throws RuntimeException() coEvery { dataSource.authenticate(any(), any()) } throws RuntimeException()
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
runCatchingExceptCancel { subject.authenticate("invalid", "") } runCatchingExceptCancel { subject.authenticate("invalid", "") }
confirmVerified(storage) confirmVerified(storage)
} }

View File

@@ -13,6 +13,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.io.IOException
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class ServerInfoRepoTest : BaseUnitTest() { class ServerInfoRepoTest : BaseUnitTest() {
@@ -44,12 +45,6 @@ class ServerInfoRepoTest : BaseUnitTest() {
assertThat(subject.getUrl()).isEqualTo(expected) 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 @Test
fun `when getUrl expect storage is accessed`() = runTest { fun `when getUrl expect storage is accessed`() = runTest {
coEvery { storage.getBaseURL() } returns null coEvery { storage.getBaseURL() } returns null
@@ -58,32 +53,45 @@ class ServerInfoRepoTest : BaseUnitTest() {
} }
@Test @Test
fun `when requireUrl expect storage is accessed`() = runTest { fun `when tryBaseURL succeeds expect call to storage`() = runTest {
coEvery { storage.getBaseURL() } returns TEST_BASE_URL coEvery { storage.getServerVersion() } returns null
subject.requireUrl() coEvery { storage.getBaseURL() } returns null
coVerify { storage.getBaseURL() } 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 @Test
fun `when storeBaseUrl expect call to storage`() = runTest { fun `when tryBaseURL fails expect call to storage`() = runTest {
subject.storeBaseURL(TEST_BASE_URL, TEST_VERSION) coEvery { storage.getServerVersion() } returns "serverVersion"
coVerify { storage.storeBaseURL(TEST_BASE_URL, TEST_VERSION) } 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 @Test
fun `when storage is empty expect getVersion to call data source`() = runTest { fun `when storage is empty expect getVersion to call data source`() = runTest {
coEvery { storage.getServerVersion() } returns null coEvery { storage.getServerVersion() } returns null
coEvery { storage.getBaseURL() } returns TEST_BASE_URL 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() subject.getVersion()
coVerify { dataSource.getVersionInfo(eq(TEST_BASE_URL)) } coVerify { dataSource.getVersionInfo() }
} }
@Test @Test
fun `when storage is empty and data source has value expect getVersion to save it`() = runTest { fun `when storage is empty and data source has value expect getVersion to save it`() = runTest {
coEvery { storage.getServerVersion() } returns null coEvery { storage.getServerVersion() } returns null
coEvery { storage.getBaseURL() } returns TEST_BASE_URL 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() subject.getVersion()
coVerify { storage.storeServerVersion(TEST_VERSION) } coVerify { storage.storeServerVersion(TEST_VERSION) }
} }
@@ -92,7 +100,7 @@ class ServerInfoRepoTest : BaseUnitTest() {
fun `when data source has invalid value expect getVersion to throw`() = runTest { fun `when data source has invalid value expect getVersion to throw`() = runTest {
coEvery { storage.getServerVersion() } returns null coEvery { storage.getServerVersion() } returns null
coEvery { storage.getBaseURL() } returns TEST_BASE_URL 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() subject.getVersion()
} }
@@ -100,7 +108,7 @@ class ServerInfoRepoTest : BaseUnitTest() {
fun `when data source has invalid value expect getVersion not to save`() = runTest { fun `when data source has invalid value expect getVersion not to save`() = runTest {
coEvery { storage.getServerVersion() } returns null coEvery { storage.getServerVersion() } returns null
coEvery { storage.getBaseURL() } returns TEST_BASE_URL 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() } subject.runCatching { getVersion() }
coVerify(inverse = true) { storage.storeServerVersion(any()) } 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 { fun `when storage has value expect getVersion to not call data source`() = runTest {
coEvery { storage.getServerVersion() } returns TEST_VERSION coEvery { storage.getServerVersion() } returns TEST_VERSION
subject.getVersion() subject.getVersion()
coVerify(inverse = true) { dataSource.getVersionInfo(any()) } coVerify(inverse = true) { dataSource.getVersionInfo() }
} }
@Test @Test
@@ -135,7 +143,7 @@ class ServerInfoRepoTest : BaseUnitTest() {
fun `when data source has valid v0 value expect getVersion to return it`() = runTest { fun `when data source has valid v0 value expect getVersion to return it`() = runTest {
coEvery { storage.getServerVersion() } returns null coEvery { storage.getServerVersion() } returns null
coEvery { storage.getBaseURL() } returns TEST_BASE_URL 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) 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 { fun `when data source has valid v1 value expect getVersion to return it`() = runTest {
coEvery { storage.getServerVersion() } returns null coEvery { storage.getServerVersion() } returns null
coEvery { storage.getBaseURL() } returns TEST_BASE_URL 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) assertThat(subject.getVersion()).isEqualTo(ServerVersion.V1)
} }
} }

View File

@@ -6,7 +6,6 @@ import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0 import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
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_V0
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_SERVER_VERSION_V1 import gq.kirmanak.mealient.test.AuthImplTestData.TEST_SERVER_VERSION_V1
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
@@ -55,15 +54,14 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
fun `when server version v1 expect requestRecipeInfo to call v1`() = runTest { fun `when server version v1 expect requestRecipeInfo to call v1`() = runTest {
val slug = "porridge" val slug = "porridge"
coEvery { coEvery {
v1Source.requestRecipeInfo(eq(TEST_BASE_URL), eq(slug)) v1Source.requestRecipeInfo(eq(slug))
} returns PORRIDGE_RECIPE_RESPONSE_V1 } returns PORRIDGE_RECIPE_RESPONSE_V1
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1 coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
val actual = subject.requestRecipeInfo(slug) 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) assertThat(actual).isEqualTo(PORRIDGE_FULL_RECIPE_INFO)
} }
@@ -71,10 +69,9 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
@Test @Test
fun `when server version v1 expect requestRecipes to call v1`() = runTest { fun `when server version v1 expect requestRecipes to call v1`() = runTest {
coEvery { coEvery {
v1Source.requestRecipes(any(), any(), any()) v1Source.requestRecipes(any(), any())
} returns listOf(PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1) } returns listOf(PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1)
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1 coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
val actual = subject.requestRecipes(40, 10) 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 page = 5 // 0-9 (1), 10-19 (2), 20-29 (3), 30-39 (4), 40-49 (5)
val perPage = 10 val perPage = 10
coVerify { coVerify {
v1Source.requestRecipes(eq(TEST_BASE_URL), eq(page), eq(perPage)) v1Source.requestRecipes(eq(page), eq(perPage))
} }
assertThat(actual).isEqualTo(listOf(RECIPE_SUMMARY_PORRIDGE_V1)) assertThat(actual).isEqualTo(listOf(RECIPE_SUMMARY_PORRIDGE_V1))
@@ -91,10 +88,9 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
@Test @Test
fun `when server version v0 expect requestRecipes to call v0`() = runTest { fun `when server version v0 expect requestRecipes to call v0`() = runTest {
coEvery { coEvery {
v0Source.requestRecipes(any(), any(), any()) v0Source.requestRecipes(any(), any())
} returns listOf(PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0) } returns listOf(PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0)
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0 coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
val start = 40 val start = 40
@@ -102,7 +98,7 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
val actual = subject.requestRecipes(start, limit) val actual = subject.requestRecipes(start, limit)
coVerify { coVerify {
v0Source.requestRecipes(eq(TEST_BASE_URL), eq(start), eq(limit)) v0Source.requestRecipes(eq(start), eq(limit))
} }
assertThat(actual).isEqualTo(listOf(RECIPE_SUMMARY_PORRIDGE_V0)) assertThat(actual).isEqualTo(listOf(RECIPE_SUMMARY_PORRIDGE_V0))
@@ -110,9 +106,8 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
@Test(expected = IOException::class) @Test(expected = IOException::class)
fun `when request fails expect addRecipe to rethrow`() = runTest { fun `when request fails expect addRecipe to rethrow`() = runTest {
coEvery { v0Source.addRecipe(any(), any()) } throws IOException() coEvery { v0Source.addRecipe(any()) } throws IOException()
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0 coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
subject.addRecipe(PORRIDGE_ADD_RECIPE_INFO) subject.addRecipe(PORRIDGE_ADD_RECIPE_INFO)
} }
@@ -121,16 +116,14 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
fun `when server version v0 expect addRecipe to call v0`() = runTest { fun `when server version v0 expect addRecipe to call v0`() = runTest {
val slug = "porridge" val slug = "porridge"
coEvery { v0Source.addRecipe(any(), any()) } returns slug coEvery { v0Source.addRecipe(any()) } returns slug
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0 coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
val actual = subject.addRecipe(PORRIDGE_ADD_RECIPE_INFO) val actual = subject.addRecipe(PORRIDGE_ADD_RECIPE_INFO)
coVerify { coVerify {
v0Source.addRecipe( v0Source.addRecipe(
eq(TEST_BASE_URL),
eq(PORRIDGE_ADD_RECIPE_REQUEST_V0), eq(PORRIDGE_ADD_RECIPE_REQUEST_V0),
) )
} }
@@ -142,24 +135,21 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
fun `when server version v1 expect addRecipe to call v1`() = runTest { fun `when server version v1 expect addRecipe to call v1`() = runTest {
val slug = "porridge" val slug = "porridge"
coEvery { v1Source.createRecipe(any(), any()) } returns slug coEvery { v1Source.createRecipe(any()) } returns slug
coEvery { coEvery {
v1Source.updateRecipe(any(), any(), any()) v1Source.updateRecipe(any(), any())
} returns PORRIDGE_RECIPE_RESPONSE_V1 } returns PORRIDGE_RECIPE_RESPONSE_V1
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1 coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
val actual = subject.addRecipe(PORRIDGE_ADD_RECIPE_INFO) val actual = subject.addRecipe(PORRIDGE_ADD_RECIPE_INFO)
coVerifySequence { coVerifySequence {
v1Source.createRecipe( v1Source.createRecipe(
eq(TEST_BASE_URL),
eq(PORRIDGE_CREATE_RECIPE_REQUEST_V1), eq(PORRIDGE_CREATE_RECIPE_REQUEST_V1),
) )
v1Source.updateRecipe( v1Source.updateRecipe(
eq(TEST_BASE_URL),
eq(slug), eq(slug),
eq(PORRIDGE_UPDATE_RECIPE_REQUEST_V1), eq(PORRIDGE_UPDATE_RECIPE_REQUEST_V1),
) )

View File

@@ -3,11 +3,8 @@ package gq.kirmanak.mealient.ui.baseurl
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.data.baseurl.VersionDataSource
import gq.kirmanak.mealient.data.baseurl.VersionInfo
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_VERSION
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import io.mockk.coEvery import io.mockk.coEvery
@@ -31,9 +28,6 @@ class BaseURLViewModelTest : BaseUnitTest() {
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var recipeRepo: RecipeRepo lateinit var recipeRepo: RecipeRepo
@MockK
lateinit var versionDataSource: VersionDataSource
lateinit var subject: BaseURLViewModel lateinit var subject: BaseURLViewModel
@Before @Before
@@ -43,7 +37,6 @@ class BaseURLViewModelTest : BaseUnitTest() {
serverInfoRepo = serverInfoRepo, serverInfoRepo = serverInfoRepo,
authRepo = authRepo, authRepo = authRepo,
recipeRepo = recipeRepo, recipeRepo = recipeRepo,
versionDataSource = versionDataSource,
logger = logger, logger = logger,
) )
} }
@@ -51,13 +44,13 @@ class BaseURLViewModelTest : BaseUnitTest() {
@Test @Test
fun `when saveBaseURL expect no version checks given that current URL matches new`() = runTest { fun `when saveBaseURL expect no version checks given that current URL matches new`() = runTest {
setupSaveBaseUrlWithOldUrl() setupSaveBaseUrlWithOldUrl()
coVerify(inverse = true) { versionDataSource.getVersionInfo(any()) } coVerify(inverse = true) { serverInfoRepo.tryBaseURL(any()) }
} }
@Test @Test
fun `when saveBaseURL expect URL isn't saved given that current URL matches new`() = runTest { fun `when saveBaseURL expect URL isn't saved given that current URL matches new`() = runTest {
setupSaveBaseUrlWithOldUrl() setupSaveBaseUrlWithOldUrl()
coVerify(inverse = true) { serverInfoRepo.storeBaseURL(any(), any()) } coVerify(inverse = true) { serverInfoRepo.tryBaseURL(any()) }
} }
@Test @Test
@@ -74,7 +67,7 @@ class BaseURLViewModelTest : BaseUnitTest() {
private fun TestScope.setupSaveBaseUrlWithOldUrl() { private fun TestScope.setupSaveBaseUrlWithOldUrl() {
coEvery { serverInfoRepo.getUrl() } returns TEST_BASE_URL coEvery { serverInfoRepo.getUrl() } returns TEST_BASE_URL
versionDataSourceReturnsSuccess() coEvery { serverInfoRepo.tryBaseURL(any()) } returns Result.success(Unit)
subject.saveBaseUrl(TEST_BASE_URL) subject.saveBaseUrl(TEST_BASE_URL)
advanceUntilIdle() advanceUntilIdle()
} }
@@ -82,7 +75,7 @@ class BaseURLViewModelTest : BaseUnitTest() {
@Test @Test
fun `when saveBaseUrl expect URL is saved given that new URL doesn't match old`() = runTest { fun `when saveBaseUrl expect URL is saved given that new URL doesn't match old`() = runTest {
setupSaveBaseUrlWithNewUrl() setupSaveBaseUrlWithNewUrl()
coVerify { serverInfoRepo.storeBaseURL(eq(TEST_BASE_URL), eq(TEST_VERSION)) } coVerify { serverInfoRepo.tryBaseURL(eq(TEST_BASE_URL)) }
} }
@Test @Test
@@ -99,21 +92,15 @@ class BaseURLViewModelTest : BaseUnitTest() {
private fun TestScope.setupSaveBaseUrlWithNewUrl() { private fun TestScope.setupSaveBaseUrlWithNewUrl() {
coEvery { serverInfoRepo.getUrl() } returns null coEvery { serverInfoRepo.getUrl() } returns null
versionDataSourceReturnsSuccess() coEvery { serverInfoRepo.tryBaseURL(any()) } returns Result.success(Unit)
subject.saveBaseUrl(TEST_BASE_URL) subject.saveBaseUrl(TEST_BASE_URL)
advanceUntilIdle() advanceUntilIdle()
} }
private fun versionDataSourceReturnsSuccess() {
coEvery {
versionDataSource.getVersionInfo(eq(TEST_BASE_URL))
} returns VersionInfo(TEST_VERSION)
}
@Test @Test
fun `when saveBaseURL expect error given that version can't be fetched`() = runTest { fun `when saveBaseURL expect error given that version can't be fetched`() = runTest {
coEvery { serverInfoRepo.getUrl() } returns null 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) subject.saveBaseUrl(TEST_BASE_URL)
advanceUntilIdle() advanceUntilIdle()
assertThat(subject.uiState.value).isInstanceOf(OperationUiState.Failure::class.java) assertThat(subject.uiState.value).isInstanceOf(OperationUiState.Failure::class.java)

View File

@@ -8,6 +8,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet import dagger.multibindings.IntoSet
import gq.kirmanak.mealient.datasource.impl.AuthInterceptor 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.CacheBuilderImpl
import gq.kirmanak.mealient.datasource.impl.NetworkRequestWrapperImpl import gq.kirmanak.mealient.datasource.impl.NetworkRequestWrapperImpl
import gq.kirmanak.mealient.datasource.impl.OkHttpBuilderImpl 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 gq.kirmanak.mealient.datasource.v1.MealieServiceV1
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Converter import retrofit2.Converter
@@ -55,8 +55,11 @@ interface DataSourceModule {
@Provides @Provides
@Singleton @Singleton
fun provideRetrofit(retrofitBuilder: RetrofitBuilder): Retrofit = fun provideRetrofit(retrofitBuilder: RetrofitBuilder): Retrofit {
retrofitBuilder.buildRetrofit("https://beta.mealie.io/") // 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 @Provides
@Singleton @Singleton
@@ -92,5 +95,10 @@ interface DataSourceModule {
@Binds @Binds
@Singleton @Singleton
@IntoSet @IntoSet
fun bindAuthInterceptor(authInterceptor: AuthInterceptor): Interceptor fun bindAuthInterceptor(authInterceptor: AuthInterceptor): LocalInterceptor
@Binds
@Singleton
@IntoSet
fun bindBaseUrlInterceptor(baseUrlInterceptor: BaseUrlInterceptor): LocalInterceptor
} }

View File

@@ -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

View File

@@ -0,0 +1,6 @@
package gq.kirmanak.mealient.datasource
interface ServerUrlProvider {
suspend fun getUrl(): String?
}

View File

@@ -2,6 +2,7 @@ package gq.kirmanak.mealient.datasource.impl
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import gq.kirmanak.mealient.datasource.AuthenticationProvider import gq.kirmanak.mealient.datasource.AuthenticationProvider
import gq.kirmanak.mealient.datasource.LocalInterceptor
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor import okhttp3.Interceptor
@@ -14,13 +15,13 @@ import javax.inject.Singleton
class AuthInterceptor @Inject constructor( class AuthInterceptor @Inject constructor(
private val logger: Logger, private val logger: Logger,
private val authenticationProviderProvider: Provider<AuthenticationProvider>, private val authenticationProviderProvider: Provider<AuthenticationProvider>,
) : Interceptor { ) : LocalInterceptor {
private val authenticationProvider: AuthenticationProvider private val authenticationProvider: AuthenticationProvider
get() = authenticationProviderProvider.get() get() = authenticationProviderProvider.get()
override fun intercept(chain: Interceptor.Chain): Response { 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 header = getAuthHeader()
val request = chain.request().let { val request = chain.request().let {
if (header == null) it else it.newBuilder().header(HEADER_NAME, header).build() if (header == null) it else it.newBuilder().header(HEADER_NAME, header).build()

View File

@@ -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<ServerUrlProvider>,
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")
}
}

View File

@@ -1,7 +1,9 @@
package gq.kirmanak.mealient.datasource.impl package gq.kirmanak.mealient.datasource.impl
import gq.kirmanak.mealient.datasource.CacheBuilder import gq.kirmanak.mealient.datasource.CacheBuilder
import gq.kirmanak.mealient.datasource.LocalInterceptor
import gq.kirmanak.mealient.datasource.OkHttpBuilder import gq.kirmanak.mealient.datasource.OkHttpBuilder
import gq.kirmanak.mealient.logging.Logger
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import javax.inject.Inject import javax.inject.Inject
@@ -12,10 +14,16 @@ class OkHttpBuilderImpl @Inject constructor(
private val cacheBuilder: CacheBuilder, private val cacheBuilder: CacheBuilder,
// Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382) // Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382)
private val interceptors: Set<@JvmSuppressWildcards Interceptor>, private val interceptors: Set<@JvmSuppressWildcards Interceptor>,
private val localInterceptors: Set<@JvmSuppressWildcards LocalInterceptor>,
private val logger: Logger,
) : OkHttpBuilder { ) : OkHttpBuilder {
override fun buildOkHttp(): OkHttpClient = OkHttpClient.Builder() override fun buildOkHttp(): OkHttpClient {
.apply { interceptors.forEach(::addNetworkInterceptor) } logger.v { "buildOkHttp() was called with cacheBuilder = $cacheBuilder, interceptors = $interceptors, localInterceptors = $localInterceptors" }
.cache(cacheBuilder.buildCache()) return OkHttpClient.Builder().apply {
.build() localInterceptors.forEach(::addInterceptor)
interceptors.forEach(::addNetworkInterceptor)
cache(cacheBuilder.buildCache())
}.build()
}
} }

View File

@@ -10,7 +10,6 @@ import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0
interface MealieDataSourceV0 { interface MealieDataSourceV0 {
suspend fun addRecipe( suspend fun addRecipe(
baseUrl: String,
recipe: AddRecipeRequestV0, recipe: AddRecipeRequestV0,
): String ): String
@@ -18,33 +17,27 @@ interface MealieDataSourceV0 {
* Tries to acquire authentication token using the provided credentials * Tries to acquire authentication token using the provided credentials
*/ */
suspend fun authenticate( suspend fun authenticate(
baseUrl: String,
username: String, username: String,
password: String, password: String,
): String ): String
suspend fun getVersionInfo( suspend fun getVersionInfo(
baseUrl: String,
): VersionResponseV0 ): VersionResponseV0
suspend fun requestRecipes( suspend fun requestRecipes(
baseUrl: String,
start: Int, start: Int,
limit: Int, limit: Int,
): List<GetRecipeSummaryResponseV0> ): List<GetRecipeSummaryResponseV0>
suspend fun requestRecipeInfo( suspend fun requestRecipeInfo(
baseUrl: String,
slug: String, slug: String,
): GetRecipeResponseV0 ): GetRecipeResponseV0
suspend fun parseRecipeFromURL( suspend fun parseRecipeFromURL(
baseUrl: String,
request: ParseRecipeURLRequestV0, request: ParseRecipeURLRequestV0,
): String ): String
suspend fun createApiToken( suspend fun createApiToken(
baseUrl: String,
request: CreateApiTokenRequestV0, request: CreateApiTokenRequestV0,
): String ): String
} }

View File

@@ -26,34 +26,30 @@ class MealieDataSourceV0Impl @Inject constructor(
) : MealieDataSourceV0 { ) : MealieDataSourceV0 {
override suspend fun addRecipe( override suspend fun addRecipe(
baseUrl: String,
recipe: AddRecipeRequestV0, recipe: AddRecipeRequestV0,
): String = networkRequestWrapper.makeCallAndHandleUnauthorized( ): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.addRecipe("$baseUrl/api/recipes/create", recipe) }, block = { service.addRecipe(recipe) },
logMethod = { "addRecipe" }, logMethod = { "addRecipe" },
logParameters = { "baseUrl = $baseUrl, recipe = $recipe" } logParameters = { "recipe = $recipe" }
) )
override suspend fun authenticate( override suspend fun authenticate(
baseUrl: String,
username: String, username: String,
password: String, password: String,
): String = networkRequestWrapper.makeCall( ): String = networkRequestWrapper.makeCall(
block = { service.getToken("$baseUrl/api/auth/token", username, password) }, block = { service.getToken(username, password) },
logMethod = { "authenticate" }, logMethod = { "authenticate" },
logParameters = { "baseUrl = $baseUrl, username = $username, password = $password" } logParameters = { "username = $username, password = $password" }
).map { it.accessToken }.getOrElse { ).map { it.accessToken }.getOrElse {
val errorBody = (it as? HttpException)?.response()?.errorBody() ?: throw it val errorBody = (it as? HttpException)?.response()?.errorBody() ?: throw it
val errorDetailV0 = errorBody.decode<ErrorDetailV0>(json) val errorDetailV0 = errorBody.decode<ErrorDetailV0>(json)
throw if (errorDetailV0.detail == "Unauthorized") NetworkError.Unauthorized(it) else it throw if (errorDetailV0.detail == "Unauthorized") NetworkError.Unauthorized(it) else it
} }
override suspend fun getVersionInfo( override suspend fun getVersionInfo(): VersionResponseV0 = networkRequestWrapper.makeCall(
baseUrl: String block = { service.getVersion() },
): VersionResponseV0 = networkRequestWrapper.makeCall(
block = { service.getVersion("$baseUrl/api/debug/version") },
logMethod = { "getVersionInfo" }, logMethod = { "getVersionInfo" },
logParameters = { "baseUrl = $baseUrl" }, logParameters = { "" },
).getOrElse { ).getOrElse {
throw when (it) { throw when (it) {
is HttpException, is SerializationException -> NetworkError.NotMealie(it) is HttpException, is SerializationException -> NetworkError.NotMealie(it)
@@ -63,39 +59,35 @@ class MealieDataSourceV0Impl @Inject constructor(
} }
override suspend fun requestRecipes( override suspend fun requestRecipes(
baseUrl: String,
start: Int, start: Int,
limit: Int, limit: Int,
): List<GetRecipeSummaryResponseV0> = networkRequestWrapper.makeCallAndHandleUnauthorized( ): List<GetRecipeSummaryResponseV0> = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getRecipeSummary("$baseUrl/api/recipes/summary", start, limit) }, block = { service.getRecipeSummary(start, limit) },
logMethod = { "requestRecipes" }, logMethod = { "requestRecipes" },
logParameters = { "baseUrl = $baseUrl, start = $start, limit = $limit" } logParameters = { "start = $start, limit = $limit" }
) )
override suspend fun requestRecipeInfo( override suspend fun requestRecipeInfo(
baseUrl: String,
slug: String, slug: String,
): GetRecipeResponseV0 = networkRequestWrapper.makeCallAndHandleUnauthorized( ): GetRecipeResponseV0 = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getRecipe("$baseUrl/api/recipes/$slug") }, block = { service.getRecipe(slug) },
logMethod = { "requestRecipeInfo" }, logMethod = { "requestRecipeInfo" },
logParameters = { "baseUrl = $baseUrl, slug = $slug" } logParameters = { "slug = $slug" }
) )
override suspend fun parseRecipeFromURL( override suspend fun parseRecipeFromURL(
baseUrl: String,
request: ParseRecipeURLRequestV0 request: ParseRecipeURLRequestV0
): String = networkRequestWrapper.makeCallAndHandleUnauthorized( ): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", request) }, block = { service.createRecipeFromURL(request) },
logMethod = { "parseRecipeFromURL" }, logMethod = { "parseRecipeFromURL" },
logParameters = { "baseUrl = $baseUrl, request = $request" }, logParameters = { "request = $request" },
) )
override suspend fun createApiToken( override suspend fun createApiToken(
baseUrl: String,
request: CreateApiTokenRequestV0, request: CreateApiTokenRequestV0,
): String = networkRequestWrapper.makeCallAndHandleUnauthorized( ): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createApiToken("$baseUrl/api/users/api-tokens", request) }, block = { service.createApiToken(request) },
logMethod = { "createApiToken" }, logMethod = { "createApiToken" },
logParameters = { "baseUrl = $baseUrl, request = $request" } logParameters = { "request = $request" }
) )
} }

View File

@@ -6,45 +6,38 @@ import retrofit2.http.*
interface MealieServiceV0 { interface MealieServiceV0 {
@FormUrlEncoded @FormUrlEncoded
@POST @POST("/api/auth/token")
suspend fun getToken( suspend fun getToken(
@Url url: String,
@Field("username") username: String, @Field("username") username: String,
@Field("password") password: String, @Field("password") password: String,
): GetTokenResponseV0 ): GetTokenResponseV0
@POST @POST("/api/recipes/create")
suspend fun addRecipe( suspend fun addRecipe(
@Url url: String,
@Body addRecipeRequestV0: AddRecipeRequestV0, @Body addRecipeRequestV0: AddRecipeRequestV0,
): String ): String
@GET @GET("/api/debug/version")
suspend fun getVersion( suspend fun getVersion(): VersionResponseV0
@Url url: String,
): VersionResponseV0
@GET @GET("/api/recipes/summary")
suspend fun getRecipeSummary( suspend fun getRecipeSummary(
@Url url: String,
@Query("start") start: Int, @Query("start") start: Int,
@Query("limit") limit: Int, @Query("limit") limit: Int,
): List<GetRecipeSummaryResponseV0> ): List<GetRecipeSummaryResponseV0>
@GET @GET("/api/recipes/{slug}")
suspend fun getRecipe( suspend fun getRecipe(
@Url url: String, @Path("slug") slug: String,
): GetRecipeResponseV0 ): GetRecipeResponseV0
@POST @POST("/api/recipes/create-url")
suspend fun createRecipeFromURL( suspend fun createRecipeFromURL(
@Url url: String,
@Body request: ParseRecipeURLRequestV0, @Body request: ParseRecipeURLRequestV0,
): String ): String
@POST @POST("/api/users/api-tokens")
suspend fun createApiToken( suspend fun createApiToken(
@Url url: String,
@Body request: CreateApiTokenRequestV0, @Body request: CreateApiTokenRequestV0,
): String ): String
} }

View File

@@ -12,12 +12,10 @@ import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
interface MealieDataSourceV1 { interface MealieDataSourceV1 {
suspend fun createRecipe( suspend fun createRecipe(
baseUrl: String,
recipe: CreateRecipeRequestV1, recipe: CreateRecipeRequestV1,
): String ): String
suspend fun updateRecipe( suspend fun updateRecipe(
baseUrl: String,
slug: String, slug: String,
recipe: UpdateRecipeRequestV1, recipe: UpdateRecipeRequestV1,
): GetRecipeResponseV1 ): GetRecipeResponseV1
@@ -26,33 +24,27 @@ interface MealieDataSourceV1 {
* Tries to acquire authentication token using the provided credentials * Tries to acquire authentication token using the provided credentials
*/ */
suspend fun authenticate( suspend fun authenticate(
baseUrl: String,
username: String, username: String,
password: String, password: String,
): String ): String
suspend fun getVersionInfo( suspend fun getVersionInfo(
baseUrl: String,
): VersionResponseV1 ): VersionResponseV1
suspend fun requestRecipes( suspend fun requestRecipes(
baseUrl: String,
page: Int, page: Int,
perPage: Int, perPage: Int,
): List<GetRecipeSummaryResponseV1> ): List<GetRecipeSummaryResponseV1>
suspend fun requestRecipeInfo( suspend fun requestRecipeInfo(
baseUrl: String,
slug: String, slug: String,
): GetRecipeResponseV1 ): GetRecipeResponseV1
suspend fun parseRecipeFromURL( suspend fun parseRecipeFromURL(
baseUrl: String,
request: ParseRecipeURLRequestV1, request: ParseRecipeURLRequestV1,
): String ): String
suspend fun createApiToken( suspend fun createApiToken(
baseUrl: String,
request: CreateApiTokenRequestV1, request: CreateApiTokenRequestV1,
): CreateApiTokenResponseV1 ): CreateApiTokenResponseV1
} }

View File

@@ -28,44 +28,39 @@ class MealieDataSourceV1Impl @Inject constructor(
) : MealieDataSourceV1 { ) : MealieDataSourceV1 {
override suspend fun createRecipe( override suspend fun createRecipe(
baseUrl: String,
recipe: CreateRecipeRequestV1 recipe: CreateRecipeRequestV1
): String = networkRequestWrapper.makeCallAndHandleUnauthorized( ): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createRecipe("$baseUrl/api/recipes", recipe) }, block = { service.createRecipe(recipe) },
logMethod = { "createRecipe" }, logMethod = { "createRecipe" },
logParameters = { "baseUrl = $baseUrl, recipe = $recipe" } logParameters = { "recipe = $recipe" }
) )
override suspend fun updateRecipe( override suspend fun updateRecipe(
baseUrl: String,
slug: String, slug: String,
recipe: UpdateRecipeRequestV1 recipe: UpdateRecipeRequestV1
): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized( ): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.updateRecipe("$baseUrl/api/recipes/$slug", recipe) }, block = { service.updateRecipe(recipe, slug) },
logMethod = { "updateRecipe" }, logMethod = { "updateRecipe" },
logParameters = { "baseUrl = $baseUrl, slug = $slug, recipe = $recipe" } logParameters = { "slug = $slug, recipe = $recipe" }
) )
override suspend fun authenticate( override suspend fun authenticate(
baseUrl: String,
username: String, username: String,
password: String, password: String,
): String = networkRequestWrapper.makeCall( ): String = networkRequestWrapper.makeCall(
block = { service.getToken("$baseUrl/api/auth/token", username, password) }, block = { service.getToken(username, password) },
logMethod = { "authenticate" }, logMethod = { "authenticate" },
logParameters = { "baseUrl = $baseUrl, username = $username, password = $password" } logParameters = { "username = $username, password = $password" }
).map { it.accessToken }.getOrElse { ).map { it.accessToken }.getOrElse {
val errorBody = (it as? HttpException)?.response()?.errorBody() ?: throw it val errorBody = (it as? HttpException)?.response()?.errorBody() ?: throw it
val errorDetailV0 = errorBody.decode<ErrorDetailV1>(json) val errorDetailV0 = errorBody.decode<ErrorDetailV1>(json)
throw if (errorDetailV0.detail == "Unauthorized") NetworkError.Unauthorized(it) else it throw if (errorDetailV0.detail == "Unauthorized") NetworkError.Unauthorized(it) else it
} }
override suspend fun getVersionInfo( override suspend fun getVersionInfo(): VersionResponseV1 = networkRequestWrapper.makeCall(
baseUrl: String, block = { service.getVersion() },
): VersionResponseV1 = networkRequestWrapper.makeCall(
block = { service.getVersion("$baseUrl/api/app/about") },
logMethod = { "getVersionInfo" }, logMethod = { "getVersionInfo" },
logParameters = { "baseUrl = $baseUrl" }, logParameters = { "" },
).getOrElse { ).getOrElse {
throw when (it) { throw when (it) {
is HttpException, is SerializationException -> NetworkError.NotMealie(it) is HttpException, is SerializationException -> NetworkError.NotMealie(it)
@@ -75,40 +70,36 @@ class MealieDataSourceV1Impl @Inject constructor(
} }
override suspend fun requestRecipes( override suspend fun requestRecipes(
baseUrl: String,
page: Int, page: Int,
perPage: Int perPage: Int
): List<GetRecipeSummaryResponseV1> = networkRequestWrapper.makeCallAndHandleUnauthorized( ): List<GetRecipeSummaryResponseV1> = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getRecipeSummary("$baseUrl/api/recipes", page, perPage) }, block = { service.getRecipeSummary(page, perPage) },
logMethod = { "requestRecipes" }, logMethod = { "requestRecipes" },
logParameters = { "baseUrl = $baseUrl, page = $page, perPage = $perPage" } logParameters = { "page = $page, perPage = $perPage" }
).items ).items
override suspend fun requestRecipeInfo( override suspend fun requestRecipeInfo(
baseUrl: String,
slug: String slug: String
): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized( ): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getRecipe("$baseUrl/api/recipes/$slug") }, block = { service.getRecipe(slug) },
logMethod = { "requestRecipeInfo" }, logMethod = { "requestRecipeInfo" },
logParameters = { "baseUrl = $baseUrl, slug = $slug" } logParameters = { "slug = $slug" }
) )
override suspend fun parseRecipeFromURL( override suspend fun parseRecipeFromURL(
baseUrl: String,
request: ParseRecipeURLRequestV1 request: ParseRecipeURLRequestV1
): String = networkRequestWrapper.makeCallAndHandleUnauthorized( ): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", request) }, block = { service.createRecipeFromURL(request) },
logMethod = { "parseRecipeFromURL" }, logMethod = { "parseRecipeFromURL" },
logParameters = { "baseUrl = $baseUrl, request = $request" } logParameters = { "request = $request" }
) )
override suspend fun createApiToken( override suspend fun createApiToken(
baseUrl: String,
request: CreateApiTokenRequestV1 request: CreateApiTokenRequestV1
): CreateApiTokenResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized( ): CreateApiTokenResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createApiToken("$baseUrl/api/users/api-tokens", request) }, block = { service.createApiToken(request) },
logMethod = { "createApiToken" }, logMethod = { "createApiToken" },
logParameters = { "baseUrl = $baseUrl, request = $request" } logParameters = { "request = $request" }
) )
} }

View File

@@ -6,51 +6,44 @@ import retrofit2.http.*
interface MealieServiceV1 { interface MealieServiceV1 {
@FormUrlEncoded @FormUrlEncoded
@POST @POST("/api/auth/token")
suspend fun getToken( suspend fun getToken(
@Url url: String,
@Field("username") username: String, @Field("username") username: String,
@Field("password") password: String, @Field("password") password: String,
): GetTokenResponseV1 ): GetTokenResponseV1
@POST @POST("/api/recipes")
suspend fun createRecipe( suspend fun createRecipe(
@Url url: String,
@Body addRecipeRequest: CreateRecipeRequestV1, @Body addRecipeRequest: CreateRecipeRequestV1,
): String ): String
@PATCH @PATCH("/api/recipes/{slug}")
suspend fun updateRecipe( suspend fun updateRecipe(
@Url url: String,
@Body addRecipeRequest: UpdateRecipeRequestV1, @Body addRecipeRequest: UpdateRecipeRequestV1,
@Path("slug") slug: String,
): GetRecipeResponseV1 ): GetRecipeResponseV1
@GET @GET("/api/app/about")
suspend fun getVersion( suspend fun getVersion(): VersionResponseV1
@Url url: String,
): VersionResponseV1
@GET @GET("/api/recipes")
suspend fun getRecipeSummary( suspend fun getRecipeSummary(
@Url url: String,
@Query("page") page: Int, @Query("page") page: Int,
@Query("perPage") perPage: Int, @Query("perPage") perPage: Int,
): GetRecipesResponseV1 ): GetRecipesResponseV1
@GET @GET("/api/recipes/{slug}")
suspend fun getRecipe( suspend fun getRecipe(
@Url url: String, @Path("slug") slug: String,
): GetRecipeResponseV1 ): GetRecipeResponseV1
@POST @POST("/api/recipes/create-url")
suspend fun createRecipeFromURL( suspend fun createRecipeFromURL(
@Url url: String,
@Body request: ParseRecipeURLRequestV1, @Body request: ParseRecipeURLRequestV1,
): String ): String
@POST @POST("/api/users/api-tokens")
suspend fun createApiToken( suspend fun createApiToken(
@Url url: String,
@Body request: CreateApiTokenRequestV1, @Body request: CreateApiTokenRequestV1,
): CreateApiTokenResponseV1 ): CreateApiTokenResponseV1
} }

View File

@@ -38,34 +38,34 @@ class MealieDataSourceV0ImplTest : BaseUnitTest() {
@Test(expected = NetworkError.NotMealie::class) @Test(expected = NetworkError.NotMealie::class)
fun `when getVersionInfo and getVersion throws HttpException then NotMealie`() = runTest { fun `when getVersionInfo and getVersion throws HttpException then NotMealie`() = runTest {
val error = HttpException(Response.error<VersionResponseV0>(404, "".toJsonResponseBody())) val error = HttpException(Response.error<VersionResponseV0>(404, "".toJsonResponseBody()))
coEvery { service.getVersion(any()) } throws error coEvery { service.getVersion() } throws error
subject.getVersionInfo(TEST_BASE_URL) subject.getVersionInfo()
} }
@Test(expected = NetworkError.NotMealie::class) @Test(expected = NetworkError.NotMealie::class)
fun `when getVersionInfo and getVersion throws SerializationException then NotMealie`() = fun `when getVersionInfo and getVersion throws SerializationException then NotMealie`() =
runTest { runTest {
coEvery { service.getVersion(any()) } throws SerializationException() coEvery { service.getVersion() } throws SerializationException()
subject.getVersionInfo(TEST_BASE_URL) subject.getVersionInfo()
} }
@Test(expected = NetworkError.NoServerConnection::class) @Test(expected = NetworkError.NoServerConnection::class)
fun `when getVersionInfo and getVersion throws IOException then NoServerConnection`() = fun `when getVersionInfo and getVersion throws IOException then NoServerConnection`() =
runTest { runTest {
coEvery { service.getVersion(any()) } throws ConnectException() coEvery { service.getVersion() } throws ConnectException()
subject.getVersionInfo(TEST_BASE_URL) subject.getVersionInfo()
} }
@Test @Test
fun `when getVersionInfo and getVersion returns result then result`() = runTest { fun `when getVersionInfo and getVersion returns result then result`() = runTest {
val versionResponse = VersionResponseV0("v0.5.6") val versionResponse = VersionResponseV0("v0.5.6")
coEvery { service.getVersion(any()) } returns versionResponse coEvery { service.getVersion() } returns versionResponse
assertThat(subject.getVersionInfo(TEST_BASE_URL)).isSameInstanceAs(versionResponse) assertThat(subject.getVersionInfo()).isSameInstanceAs(versionResponse)
} }
@Test @Test
fun `when authentication is successful then token is correct`() = runTest { 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) assertThat(callAuthenticate()).isEqualTo(TEST_TOKEN)
} }
@@ -73,7 +73,7 @@ class MealieDataSourceV0ImplTest : BaseUnitTest() {
fun `when authenticate receives 401 and Unauthorized then throws Unauthorized`() = runTest { fun `when authenticate receives 401 and Unauthorized then throws Unauthorized`() = runTest {
val body = "{\"detail\":\"Unauthorized\"}".toJsonResponseBody() val body = "{\"detail\":\"Unauthorized\"}".toJsonResponseBody()
coEvery { coEvery {
service.getToken(any(), any(), any()) service.getToken(any(), any())
} throws HttpException(Response.error<GetTokenResponseV0>(401, body)) } throws HttpException(Response.error<GetTokenResponseV0>(401, body))
callAuthenticate() callAuthenticate()
} }
@@ -82,7 +82,7 @@ class MealieDataSourceV0ImplTest : BaseUnitTest() {
fun `when authenticate receives 401 but not Unauthorized then throws NotMealie`() = runTest { fun `when authenticate receives 401 but not Unauthorized then throws NotMealie`() = runTest {
val body = "{\"detail\":\"Something\"}".toJsonResponseBody() val body = "{\"detail\":\"Something\"}".toJsonResponseBody()
coEvery { coEvery {
service.getToken(any(), any(), any()) service.getToken(any(), any())
} throws HttpException(Response.error<GetTokenResponseV0>(401, body)) } throws HttpException(Response.error<GetTokenResponseV0>(401, body))
callAuthenticate() callAuthenticate()
} }
@@ -91,22 +91,21 @@ class MealieDataSourceV0ImplTest : BaseUnitTest() {
fun `when authenticate receives 404 and empty body then throws NotMealie`() = runTest { fun `when authenticate receives 404 and empty body then throws NotMealie`() = runTest {
val body = "".toJsonResponseBody() val body = "".toJsonResponseBody()
coEvery { coEvery {
service.getToken(any(), any(), any()) service.getToken(any(), any())
} throws HttpException(Response.error<GetTokenResponseV0>(401, body)) } throws HttpException(Response.error<GetTokenResponseV0>(401, body))
callAuthenticate() callAuthenticate()
} }
@Test(expected = IOException::class) @Test(expected = IOException::class)
fun `when authenticate and getToken throws then throws NoServerConnection`() = runTest { 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() callAuthenticate()
} }
private suspend fun callAuthenticate(): String = private suspend fun callAuthenticate(): String =
subject.authenticate(TEST_USERNAME, TEST_PASSWORD, TEST_BASE_URL) subject.authenticate(TEST_PASSWORD, TEST_BASE_URL)
companion object { companion object {
const val TEST_USERNAME = "TEST_USERNAME"
const val TEST_PASSWORD = "TEST_PASSWORD" const val TEST_PASSWORD = "TEST_PASSWORD"
const val TEST_BASE_URL = "https://example.com/" const val TEST_BASE_URL = "https://example.com/"
const val TEST_TOKEN = "TEST_TOKEN" const val TEST_TOKEN = "TEST_TOKEN"