Simplify network layer (#175)
* Use Ktor for network requests * Remove V0 version * Remove Retrofit dependency * Fix url * Update versions of dependencies * Revert kotlinx-datetime Due to https://github.com/Kotlin/kotlinx-datetime/issues/304 * Rename leftovers * Remove OkHttp * Remove unused manifest * Remove unused Hilt module * Fix building empty image URLs * Use OkHttp as engine for Ktor * Reduce visibility of internal classes * Fix first set up test * Store only auth token, not header * Remove UnitInfo/FoodInfo/VersionInfo/NewShoppingListItemInfo * Remove RecipeSummaryInfo and ShoppingListsInfo * Remove FullShoppingListInfo * Remove ParseRecipeURLInfo * Remove FullRecipeInfo * Sign out if access token does not work * Rename getVersionInfo method * Update version name
This commit is contained in:
@@ -16,8 +16,8 @@ plugins {
|
||||
android {
|
||||
defaultConfig {
|
||||
applicationId = "gq.kirmanak.mealient"
|
||||
versionCode = 30
|
||||
versionName = "0.4.1"
|
||||
versionCode = 31
|
||||
versionName = "0.4.2"
|
||||
testInstrumentationRunner = "gq.kirmanak.mealient.MealientTestRunner"
|
||||
testInstrumentationRunnerArguments += mapOf("clearPackageData" to "true")
|
||||
resourceConfigurations += listOf("en", "es", "ru", "fr", "nl", "pt", "de")
|
||||
@@ -55,7 +55,7 @@ android {
|
||||
|
||||
namespace = "gq.kirmanak.mealient"
|
||||
|
||||
packagingOptions {
|
||||
packaging {
|
||||
resources.excludes += "DebugProbesKt.bin"
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,15 @@ import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
|
||||
val versionV1Response = MockResponse().setResponseCode(200).setBody(
|
||||
"""{"production":true,"version":"v1.0.0beta-5","demoStatus":false,"allowSignup":true}"""
|
||||
)
|
||||
val versionV1Response = MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody("""{"production":true,"version":"v1.0.0beta-5","demoStatus":false,"allowSignup":true}""")
|
||||
|
||||
val notFoundResponse = MockResponse().setResponseCode(404).setBody("""{"detail":"Not found"}"""")
|
||||
val notFoundResponse = MockResponse()
|
||||
.setResponseCode(404)
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody("""{"detail":"Not found"}"""")
|
||||
|
||||
fun MockWebServer.dispatch(block: (String, RecordedRequest) -> MockResponse) {
|
||||
dispatcher = object : Dispatcher() {
|
||||
|
||||
@@ -9,7 +9,7 @@ interface AuthRepo : ShoppingListsAuthRepo {
|
||||
|
||||
suspend fun authenticate(email: String, password: String)
|
||||
|
||||
suspend fun getAuthHeader(): String?
|
||||
suspend fun getAuthToken(): String?
|
||||
|
||||
suspend fun logout()
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AuthStorage {
|
||||
|
||||
val authHeaderFlow: Flow<String?>
|
||||
val authTokenFlow: Flow<String?>
|
||||
|
||||
suspend fun setAuthHeader(authHeader: String?)
|
||||
suspend fun setAuthToken(authToken: String?)
|
||||
|
||||
suspend fun getAuthHeader(): String?
|
||||
suspend fun getAuthToken(): String?
|
||||
}
|
||||
@@ -1,32 +1,19 @@
|
||||
package gq.kirmanak.mealient.data.auth.impl
|
||||
|
||||
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerVersion
|
||||
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
|
||||
import gq.kirmanak.mealient.datasource.v0.models.CreateApiTokenRequestV0
|
||||
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1
|
||||
import gq.kirmanak.mealient.datasource.MealieDataSource
|
||||
import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
|
||||
import javax.inject.Inject
|
||||
|
||||
class AuthDataSourceImpl @Inject constructor(
|
||||
private val serverInfoRepo: ServerInfoRepo,
|
||||
private val v0Source: MealieDataSourceV0,
|
||||
private val v1Source: MealieDataSourceV1,
|
||||
private val dataSource: MealieDataSource,
|
||||
) : AuthDataSource {
|
||||
|
||||
private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion()
|
||||
|
||||
override suspend fun authenticate(
|
||||
username: String,
|
||||
password: String,
|
||||
): String = when (getVersion()) {
|
||||
ServerVersion.V0 -> v0Source.authenticate(username, password)
|
||||
ServerVersion.V1 -> v1Source.authenticate(username, password)
|
||||
override suspend fun authenticate(username: String, password: String): String {
|
||||
return dataSource.authenticate(username, password)
|
||||
}
|
||||
|
||||
override suspend fun createApiToken(name: String): String = when (getVersion()) {
|
||||
ServerVersion.V0 -> v0Source.createApiToken(CreateApiTokenRequestV0(name)).token
|
||||
ServerVersion.V1 -> v1Source.createApiToken(CreateApiTokenRequestV1(name)).token
|
||||
override suspend fun createApiToken(name: String): String {
|
||||
return dataSource.createApiToken(CreateApiTokenRequest(name)).token
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import gq.kirmanak.mealient.data.auth.AuthDataSource
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||
import gq.kirmanak.mealient.datasource.AuthenticationProvider
|
||||
import gq.kirmanak.mealient.datasource.SignOutHandler
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -13,28 +14,29 @@ class AuthRepoImpl @Inject constructor(
|
||||
private val authStorage: AuthStorage,
|
||||
private val authDataSource: AuthDataSource,
|
||||
private val logger: Logger,
|
||||
private val signOutHandler: SignOutHandler,
|
||||
) : AuthRepo, AuthenticationProvider {
|
||||
|
||||
override val isAuthorizedFlow: Flow<Boolean>
|
||||
get() = authStorage.authHeaderFlow.map { it != null }
|
||||
get() = authStorage.authTokenFlow.map { it != null }
|
||||
|
||||
override suspend fun authenticate(email: String, password: String) {
|
||||
logger.v { "authenticate() called with: email = $email, password = $password" }
|
||||
val token = authDataSource.authenticate(email, password)
|
||||
authStorage.setAuthHeader(AUTH_HEADER_FORMAT.format(token))
|
||||
authStorage.setAuthToken(token)
|
||||
val apiToken = authDataSource.createApiToken(API_TOKEN_NAME)
|
||||
authStorage.setAuthHeader(AUTH_HEADER_FORMAT.format(apiToken))
|
||||
authStorage.setAuthToken(apiToken)
|
||||
}
|
||||
|
||||
override suspend fun getAuthHeader(): String? = authStorage.getAuthHeader()
|
||||
override suspend fun getAuthToken(): String? = authStorage.getAuthToken()
|
||||
|
||||
override suspend fun logout() {
|
||||
logger.v { "logout() called" }
|
||||
authStorage.setAuthHeader(null)
|
||||
authStorage.setAuthToken(null)
|
||||
signOutHandler.signOut()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val AUTH_HEADER_FORMAT = "Bearer %s"
|
||||
private const val API_TOKEN_NAME = "Mealient"
|
||||
}
|
||||
}
|
||||
@@ -22,15 +22,15 @@ class AuthStorageImpl @Inject constructor(
|
||||
private val logger: Logger,
|
||||
) : AuthStorage {
|
||||
|
||||
override val authHeaderFlow: Flow<String?>
|
||||
override val authTokenFlow: Flow<String?>
|
||||
get() = sharedPreferences
|
||||
.prefsChangeFlow(logger) { getString(AUTH_HEADER_KEY, null) }
|
||||
.prefsChangeFlow(logger) { getString(AUTH_TOKEN_KEY, null) }
|
||||
.distinctUntilChanged()
|
||||
private val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
|
||||
override suspend fun setAuthHeader(authHeader: String?) = putString(AUTH_HEADER_KEY, authHeader)
|
||||
override suspend fun setAuthToken(authToken: String?) = putString(AUTH_TOKEN_KEY, authToken)
|
||||
|
||||
override suspend fun getAuthHeader(): String? = getString(AUTH_HEADER_KEY)
|
||||
override suspend fun getAuthToken(): String? = getString(AUTH_TOKEN_KEY)
|
||||
|
||||
private suspend fun putString(
|
||||
key: String,
|
||||
@@ -48,6 +48,6 @@ class AuthStorageImpl @Inject constructor(
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
const val AUTH_HEADER_KEY = "authHeader"
|
||||
const val AUTH_TOKEN_KEY = "authToken"
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,10 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ServerInfoRepo {
|
||||
|
||||
suspend fun getUrl(): String?
|
||||
|
||||
suspend fun getVersion(): ServerVersion
|
||||
|
||||
suspend fun tryBaseURL(baseURL: String): Result<Unit>
|
||||
|
||||
fun versionUpdates(): Flow<ServerVersion>
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
import gq.kirmanak.mealient.datasource.ServerUrlProvider
|
||||
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
class ServerInfoRepoImpl @Inject constructor(
|
||||
@@ -20,47 +16,18 @@ class ServerInfoRepoImpl @Inject constructor(
|
||||
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().version
|
||||
val result = determineServerVersion(version)
|
||||
serverInfoStorage.storeServerVersion(version)
|
||||
result
|
||||
} else {
|
||||
determineServerVersion(version)
|
||||
}
|
||||
logger.v { "getVersion() returned: $serverVersion from $version" }
|
||||
return serverVersion
|
||||
}
|
||||
|
||||
private fun determineServerVersion(version: String): ServerVersion = when {
|
||||
version.startsWith("v0") -> ServerVersion.V0
|
||||
version.startsWith("v1") -> ServerVersion.V1
|
||||
else -> {
|
||||
logger.w { "Unknown server version: $version" }
|
||||
ServerVersion.V1
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun tryBaseURL(baseURL: String): Result<Unit> {
|
||||
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)
|
||||
}
|
||||
|
||||
try {
|
||||
versionDataSource.requestVersion()
|
||||
} catch (e: Throwable) {
|
||||
serverInfoStorage.storeBaseURL(oldBaseUrl)
|
||||
return Result.failure(e)
|
||||
}
|
||||
|
||||
override fun versionUpdates(): Flow<ServerVersion> {
|
||||
return serverInfoStorage
|
||||
.serverVersionUpdates()
|
||||
.filterNotNull()
|
||||
.map { determineServerVersion(it) }
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,18 +1,9 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ServerInfoStorage {
|
||||
|
||||
suspend fun getBaseURL(): String?
|
||||
|
||||
suspend fun storeBaseURL(baseURL: String)
|
||||
suspend fun storeBaseURL(baseURL: String?)
|
||||
|
||||
suspend fun storeBaseURL(baseURL: String?, version: String?)
|
||||
|
||||
suspend fun storeServerVersion(version: String)
|
||||
|
||||
suspend fun getServerVersion(): String?
|
||||
|
||||
fun serverVersionUpdates(): Flow<String?>
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
enum class ServerVersion { V0, V1 }
|
||||
@@ -1,8 +1,8 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
import gq.kirmanak.mealient.datasource.models.VersionInfo
|
||||
import gq.kirmanak.mealient.datasource.models.VersionResponse
|
||||
|
||||
interface VersionDataSource {
|
||||
|
||||
suspend fun getVersionInfo(): VersionInfo
|
||||
suspend fun requestVersion(): VersionResponse
|
||||
}
|
||||
@@ -1,37 +1,14 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
import gq.kirmanak.mealient.datasource.models.VersionInfo
|
||||
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
||||
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
|
||||
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
|
||||
import gq.kirmanak.mealient.model_mapper.ModelMapper
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import gq.kirmanak.mealient.datasource.MealieDataSource
|
||||
import gq.kirmanak.mealient.datasource.models.VersionResponse
|
||||
import javax.inject.Inject
|
||||
|
||||
class VersionDataSourceImpl @Inject constructor(
|
||||
private val v0Source: MealieDataSourceV0,
|
||||
private val v1Source: MealieDataSourceV1,
|
||||
private val modelMapper: ModelMapper,
|
||||
private val dataSource: MealieDataSource,
|
||||
) : VersionDataSource {
|
||||
|
||||
override suspend fun getVersionInfo(): VersionInfo {
|
||||
val responses = coroutineScope {
|
||||
val v0Deferred = async {
|
||||
runCatchingExceptCancel { modelMapper.toVersionInfo(v0Source.getVersionInfo()) }
|
||||
}
|
||||
val v1Deferred = async {
|
||||
runCatchingExceptCancel { modelMapper.toVersionInfo(v1Source.getVersionInfo()) }
|
||||
}
|
||||
listOf(v0Deferred, v1Deferred).awaitAll()
|
||||
}
|
||||
val firstSuccess = responses.firstNotNullOfOrNull { it.getOrNull() }
|
||||
if (firstSuccess == null) {
|
||||
throw responses.firstNotNullOf { it.exceptionOrNull() }
|
||||
} else {
|
||||
return firstSuccess
|
||||
override suspend fun requestVersion(): VersionResponse {
|
||||
return dataSource.getVersionInfo()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package gq.kirmanak.mealient.data.baseurl.impl
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoStorage
|
||||
import gq.kirmanak.mealient.data.storage.PreferencesStorage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class ServerInfoStorageImpl @Inject constructor(
|
||||
@@ -13,44 +12,15 @@ class ServerInfoStorageImpl @Inject constructor(
|
||||
private val baseUrlKey: Preferences.Key<String>
|
||||
get() = preferencesStorage.baseUrlKey
|
||||
|
||||
private val serverVersionKey: Preferences.Key<String>
|
||||
get() = preferencesStorage.serverVersionKey
|
||||
|
||||
override suspend fun getBaseURL(): String? = getValue(baseUrlKey)
|
||||
|
||||
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)
|
||||
override suspend fun storeBaseURL(baseURL: String?) {
|
||||
if (baseURL == null) {
|
||||
preferencesStorage.removeValues(baseUrlKey)
|
||||
} else {
|
||||
preferencesStorage.storeValues(Pair(baseUrlKey, baseURL))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getServerVersion(): String? = getValue(serverVersionKey)
|
||||
|
||||
override suspend fun storeServerVersion(version: String) {
|
||||
preferencesStorage.storeValues(Pair(serverVersionKey, version))
|
||||
}
|
||||
|
||||
override fun serverVersionUpdates(): Flow<String?> {
|
||||
return preferencesStorage.valueUpdates(serverVersionKey)
|
||||
}
|
||||
|
||||
private suspend fun <T> getValue(key: Preferences.Key<T>): T? = preferencesStorage.getValue(key)
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package gq.kirmanak.mealient.data.migration
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import gq.kirmanak.mealient.datastore.DataStoreModule
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class From30MigrationExecutor @Inject constructor(
|
||||
@Named(DataStoreModule.ENCRYPTED) private val sharedPreferences: SharedPreferences,
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
) : MigrationExecutor {
|
||||
|
||||
override val migratingFrom: Int = 30
|
||||
|
||||
override suspend fun executeMigration() {
|
||||
dataStore.edit { prefs ->
|
||||
prefs -= stringPreferencesKey("serverVersion")
|
||||
}
|
||||
val authHeader = sharedPreferences.getString("authHeader", null)
|
||||
if (authHeader != null) {
|
||||
sharedPreferences.edit {
|
||||
val authToken = authHeader.removePrefix("Bearer ")
|
||||
putString("authToken", authToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,92 +1,58 @@
|
||||
package gq.kirmanak.mealient.data.network
|
||||
|
||||
import gq.kirmanak.mealient.data.add.AddRecipeDataSource
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerVersion
|
||||
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
||||
import gq.kirmanak.mealient.data.share.ParseRecipeDataSource
|
||||
import gq.kirmanak.mealient.datasource.MealieDataSource
|
||||
import gq.kirmanak.mealient.datasource.models.AddRecipeInfo
|
||||
import gq.kirmanak.mealient.datasource.models.FullRecipeInfo
|
||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
|
||||
import gq.kirmanak.mealient.datasource.models.RecipeSummaryInfo
|
||||
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
|
||||
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
|
||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLRequest
|
||||
import gq.kirmanak.mealient.model_mapper.ModelMapper
|
||||
import javax.inject.Inject
|
||||
|
||||
class MealieDataSourceWrapper @Inject constructor(
|
||||
private val serverInfoRepo: ServerInfoRepo,
|
||||
private val v0Source: MealieDataSourceV0,
|
||||
private val v1Source: MealieDataSourceV1,
|
||||
private val dataSource: MealieDataSource,
|
||||
private val modelMapper: ModelMapper,
|
||||
) : AddRecipeDataSource, RecipeDataSource, ParseRecipeDataSource {
|
||||
|
||||
private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion()
|
||||
|
||||
override suspend fun addRecipe(recipe: AddRecipeInfo): String = when (getVersion()) {
|
||||
ServerVersion.V0 -> v0Source.addRecipe(modelMapper.toV0Request(recipe))
|
||||
ServerVersion.V1 -> {
|
||||
val slug = v1Source.createRecipe(modelMapper.toV1CreateRequest(recipe))
|
||||
v1Source.updateRecipe(slug, modelMapper.toV1UpdateRequest(recipe))
|
||||
slug
|
||||
}
|
||||
override suspend fun addRecipe(recipe: AddRecipeInfo): String {
|
||||
val slug = dataSource.createRecipe(modelMapper.toCreateRequest(recipe))
|
||||
dataSource.updateRecipe(slug, modelMapper.toUpdateRequest(recipe))
|
||||
return slug
|
||||
}
|
||||
|
||||
override suspend fun requestRecipes(
|
||||
start: Int,
|
||||
limit: Int,
|
||||
): List<RecipeSummaryInfo> = when (getVersion()) {
|
||||
ServerVersion.V0 -> {
|
||||
v0Source.requestRecipes(start, limit).map { modelMapper.toRecipeSummaryInfo(it) }
|
||||
}
|
||||
ServerVersion.V1 -> {
|
||||
): List<GetRecipeSummaryResponse> {
|
||||
// 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(page, limit).map { modelMapper.toRecipeSummaryInfo(it) }
|
||||
}
|
||||
return dataSource.requestRecipes(page, limit)
|
||||
}
|
||||
|
||||
override suspend fun requestRecipeInfo(slug: String): FullRecipeInfo = when (getVersion()) {
|
||||
ServerVersion.V0 -> modelMapper.toFullRecipeInfo(v0Source.requestRecipeInfo(slug))
|
||||
ServerVersion.V1 -> modelMapper.toFullRecipeInfo(v1Source.requestRecipeInfo(slug))
|
||||
override suspend fun requestRecipe(slug: String): GetRecipeResponse {
|
||||
return dataSource.requestRecipeInfo(slug)
|
||||
}
|
||||
|
||||
override suspend fun parseRecipeFromURL(
|
||||
parseRecipeURLInfo: ParseRecipeURLInfo,
|
||||
): String = when (getVersion()) {
|
||||
ServerVersion.V0 -> v0Source.parseRecipeFromURL(modelMapper.toV0Request(parseRecipeURLInfo))
|
||||
ServerVersion.V1 -> v1Source.parseRecipeFromURL(modelMapper.toV1Request(parseRecipeURLInfo))
|
||||
override suspend fun parseRecipeFromURL(parseRecipeURLInfo: ParseRecipeURLRequest): String {
|
||||
return dataSource.parseRecipeFromURL(parseRecipeURLInfo)
|
||||
}
|
||||
|
||||
override suspend fun getFavoriteRecipes(): List<String> = when (getVersion()) {
|
||||
ServerVersion.V0 -> v0Source.requestUserInfo().favoriteRecipes
|
||||
ServerVersion.V1 -> v1Source.requestUserInfo().favoriteRecipes
|
||||
override suspend fun getFavoriteRecipes(): List<String> {
|
||||
return dataSource.requestUserInfo().favoriteRecipes
|
||||
}
|
||||
|
||||
override suspend fun updateIsRecipeFavorite(
|
||||
recipeSlug: String,
|
||||
isFavorite: Boolean
|
||||
) = when (getVersion()) {
|
||||
ServerVersion.V0 -> {
|
||||
val userId = v0Source.requestUserInfo().id
|
||||
override suspend fun updateIsRecipeFavorite(recipeSlug: String, isFavorite: Boolean) {
|
||||
val userId = dataSource.requestUserInfo().id
|
||||
if (isFavorite) {
|
||||
v0Source.addFavoriteRecipe(userId, recipeSlug)
|
||||
dataSource.addFavoriteRecipe(userId, recipeSlug)
|
||||
} else {
|
||||
v0Source.removeFavoriteRecipe(userId, recipeSlug)
|
||||
}
|
||||
}
|
||||
ServerVersion.V1 -> {
|
||||
val userId = v1Source.requestUserInfo().id
|
||||
if (isFavorite) {
|
||||
v1Source.addFavoriteRecipe(userId, recipeSlug)
|
||||
} else {
|
||||
v1Source.removeFavoriteRecipe(userId, recipeSlug)
|
||||
}
|
||||
dataSource.removeFavoriteRecipe(userId, recipeSlug)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteRecipe(recipeSlug: String) = when (getVersion()) {
|
||||
ServerVersion.V0 -> v0Source.deleteRecipe(recipeSlug)
|
||||
ServerVersion.V1 -> v1Source.deleteRecipe(recipeSlug)
|
||||
override suspend fun deleteRecipe(recipeSlug: String) {
|
||||
dataSource.deleteRecipe(recipeSlug)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package gq.kirmanak.mealient.data.recipes.impl
|
||||
|
||||
import android.net.Uri
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import javax.inject.Inject
|
||||
|
||||
class RecipeImageUrlProviderImpl @Inject constructor(
|
||||
@@ -15,9 +15,11 @@ class RecipeImageUrlProviderImpl @Inject constructor(
|
||||
slug?.takeUnless { it.isBlank() } ?: return null
|
||||
val imagePath = IMAGE_PATH_FORMAT.format(slug)
|
||||
val baseUrl = serverInfoRepo.getUrl()?.takeUnless { it.isEmpty() }
|
||||
val result = baseUrl?.toHttpUrlOrNull()
|
||||
?.newBuilder()
|
||||
?.addPathSegments(imagePath)
|
||||
val result = baseUrl
|
||||
?.takeUnless { it.isBlank() }
|
||||
?.let { Uri.parse(it) }
|
||||
?.buildUpon()
|
||||
?.path(imagePath)
|
||||
?.build()
|
||||
?.toString()
|
||||
logger.v { "getRecipeImageUrl() returned: $result" }
|
||||
|
||||
@@ -45,7 +45,7 @@ class RecipeRepoImpl @Inject constructor(
|
||||
override suspend fun refreshRecipeInfo(recipeSlug: String): Result<Unit> {
|
||||
logger.v { "refreshRecipeInfo() called with: recipeSlug = $recipeSlug" }
|
||||
return runCatchingExceptCancel {
|
||||
val info = dataSource.requestRecipeInfo(recipeSlug)
|
||||
val info = dataSource.requestRecipe(recipeSlug)
|
||||
val entity = modelMapper.toRecipeEntity(info)
|
||||
val ingredients = info.recipeIngredients.map {
|
||||
modelMapper.toRecipeIngredientEntity(it, entity.remoteId)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network
|
||||
|
||||
import gq.kirmanak.mealient.datasource.models.FullRecipeInfo
|
||||
import gq.kirmanak.mealient.datasource.models.RecipeSummaryInfo
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
|
||||
|
||||
interface RecipeDataSource {
|
||||
suspend fun requestRecipes(start: Int, limit: Int): List<RecipeSummaryInfo>
|
||||
suspend fun requestRecipes(start: Int, limit: Int): List<GetRecipeSummaryResponse>
|
||||
|
||||
suspend fun requestRecipeInfo(slug: String): FullRecipeInfo
|
||||
suspend fun requestRecipe(slug: String): GetRecipeResponse
|
||||
|
||||
suspend fun getFavoriteRecipes(): List<String>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package gq.kirmanak.mealient.data.share
|
||||
|
||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
|
||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLRequest
|
||||
|
||||
interface ParseRecipeDataSource {
|
||||
|
||||
suspend fun parseRecipeFromURL(parseRecipeURLInfo: ParseRecipeURLInfo): String
|
||||
suspend fun parseRecipeFromURL(parseRecipeURLInfo: ParseRecipeURLRequest): String
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package gq.kirmanak.mealient.data.share
|
||||
|
||||
import androidx.core.util.PatternsCompat
|
||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
|
||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLRequest
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -15,7 +15,7 @@ class ShareRecipeRepoImpl @Inject constructor(
|
||||
val matcher = PatternsCompat.WEB_URL.matcher(url)
|
||||
require(matcher.find()) { "Can't find URL in the text" }
|
||||
val urlString = matcher.group()
|
||||
val request = ParseRecipeURLInfo(url = urlString, includeTags = true)
|
||||
val request = ParseRecipeURLRequest(url = urlString, includeTags = true)
|
||||
return parseRecipeDataSource.parseRecipeFromURL(request)
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@ interface PreferencesStorage {
|
||||
|
||||
val baseUrlKey: Preferences.Key<String>
|
||||
|
||||
val serverVersionKey: Preferences.Key<String>
|
||||
|
||||
val isDisclaimerAcceptedKey: Preferences.Key<Boolean>
|
||||
|
||||
val lastExecutedMigrationVersionKey: Preferences.Key<Int>
|
||||
|
||||
@@ -24,8 +24,6 @@ class PreferencesStorageImpl @Inject constructor(
|
||||
|
||||
override val baseUrlKey = stringPreferencesKey("baseUrl")
|
||||
|
||||
override val serverVersionKey = stringPreferencesKey("serverVersion")
|
||||
|
||||
override val isDisclaimerAcceptedKey = booleanPreferencesKey("isDisclaimedAccepted")
|
||||
|
||||
override val lastExecutedMigrationVersionKey: Preferences.Key<Int> =
|
||||
|
||||
@@ -6,6 +6,7 @@ import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import gq.kirmanak.mealient.data.migration.From24AuthMigrationExecutor
|
||||
import gq.kirmanak.mealient.data.migration.From30MigrationExecutor
|
||||
import gq.kirmanak.mealient.data.migration.MigrationDetector
|
||||
import gq.kirmanak.mealient.data.migration.MigrationDetectorImpl
|
||||
import gq.kirmanak.mealient.data.migration.MigrationExecutor
|
||||
@@ -18,6 +19,10 @@ interface MigrationModule {
|
||||
@IntoSet
|
||||
fun bindFrom24AuthMigrationExecutor(from24AuthMigrationExecutor: From24AuthMigrationExecutor): MigrationExecutor
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
fun bindFrom30MigrationExecutor(impl: From30MigrationExecutor): MigrationExecutor
|
||||
|
||||
@Binds
|
||||
fun bindMigrationDetector(migrationDetectorImpl: MigrationDetectorImpl): MigrationDetector
|
||||
}
|
||||
@@ -109,7 +109,6 @@ class MainActivity : BaseActivity<MainActivityBinding>(
|
||||
when (itemId) {
|
||||
R.id.logout -> menuItem.isVisible = uiState.canShowLogout
|
||||
R.id.login -> menuItem.isVisible = uiState.canShowLogin
|
||||
R.id.shopping_lists -> menuItem.isVisible = uiState.v1MenuItemsVisible
|
||||
}
|
||||
menuItem.isChecked = itemId == checkedMenuItem
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerVersion
|
||||
import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
@@ -47,10 +46,6 @@ class MainActivityViewModel @Inject constructor(
|
||||
.onEach { isAuthorized -> updateUiState { it.copy(isAuthorized = isAuthorized) } }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
serverInfoRepo.versionUpdates()
|
||||
.onEach { version -> updateUiState { it.copy(v1MenuItemsVisible = version == ServerVersion.V1) } }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
viewModelScope.launch {
|
||||
_startDestination.value = when {
|
||||
!disclaimerStorage.isDisclaimerAccepted() -> {
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
<item
|
||||
android:id="@+id/shopping_lists"
|
||||
android:visible="false"
|
||||
android:checkable="true"
|
||||
android:icon="@drawable/ic_shopping_cart"
|
||||
android:title="@string/menu_navigation_drawer_shopping_lists" />
|
||||
|
||||
@@ -9,15 +9,16 @@ import gq.kirmanak.mealient.datastore_test.PORRIDGE_RECIPE_DRAFT
|
||||
import gq.kirmanak.mealient.model_mapper.ModelMapper
|
||||
import gq.kirmanak.mealient.model_mapper.ModelMapperImpl
|
||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
import io.mockk.*
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class AddRecipeRepoTest : BaseUnitTest() {
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
|
||||
@@ -4,62 +4,59 @@ import com.google.common.truth.Truth.assertThat
|
||||
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
||||
import gq.kirmanak.mealient.datasource.SignOutHandler
|
||||
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_PASSWORD
|
||||
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_USERNAME
|
||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
import io.mockk.*
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.confirmVerified
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class AuthRepoImplTest : BaseUnitTest() {
|
||||
|
||||
@MockK
|
||||
lateinit var dataSource: AuthDataSource
|
||||
|
||||
@MockK
|
||||
lateinit var serverInfoRepo: ServerInfoRepo
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
lateinit var storage: AuthStorage
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
lateinit var signOutHandler: SignOutHandler
|
||||
|
||||
lateinit var subject: AuthRepo
|
||||
|
||||
@Before
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
subject = AuthRepoImpl(storage, dataSource, logger)
|
||||
subject = AuthRepoImpl(storage, dataSource, logger, signOutHandler)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when isAuthorizedFlow then reads from storage`() = runTest {
|
||||
every { storage.authHeaderFlow } returns flowOf("", null, "header")
|
||||
every { storage.authTokenFlow } returns flowOf("", null, "header")
|
||||
assertThat(subject.isAuthorizedFlow.toList()).isEqualTo(listOf(true, false, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
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 { dataSource.createApiToken(any()) } returns TEST_API_TOKEN
|
||||
subject.authenticate(TEST_USERNAME, TEST_PASSWORD)
|
||||
coVerify {
|
||||
dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD))
|
||||
storage.setAuthHeader(TEST_AUTH_HEADER)
|
||||
storage.setAuthToken(TEST_TOKEN)
|
||||
dataSource.createApiToken(eq("Mealient"))
|
||||
storage.setAuthHeader(TEST_API_AUTH_HEADER)
|
||||
storage.setAuthToken(TEST_API_TOKEN)
|
||||
}
|
||||
confirmVerified(storage)
|
||||
}
|
||||
@@ -74,7 +71,7 @@ class AuthRepoImplTest : BaseUnitTest() {
|
||||
@Test
|
||||
fun `when logout expect header removal`() = runTest {
|
||||
subject.logout()
|
||||
coVerify { storage.setAuthHeader(null) }
|
||||
coVerify { storage.setAuthToken(null) }
|
||||
confirmVerified(storage)
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,16 @@ import com.google.common.truth.Truth.assertThat
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.AUTH_HEADER_KEY
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
|
||||
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.AUTH_TOKEN_KEY
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN
|
||||
import gq.kirmanak.mealient.test.HiltRobolectricTest
|
||||
import io.mockk.MockKAnnotations
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@HiltAndroidTest
|
||||
class AuthStorageImplTest : HiltRobolectricTest() {
|
||||
|
||||
@@ -39,12 +37,12 @@ class AuthStorageImplTest : HiltRobolectricTest() {
|
||||
|
||||
@Test
|
||||
fun `when authHeaderFlow is observed then sends value immediately`() = runTest {
|
||||
sharedPreferences.edit(commit = true) { putString(AUTH_HEADER_KEY, TEST_AUTH_HEADER) }
|
||||
assertThat(subject.authHeaderFlow.first()).isEqualTo(TEST_AUTH_HEADER)
|
||||
sharedPreferences.edit(commit = true) { putString(AUTH_TOKEN_KEY, TEST_TOKEN) }
|
||||
assertThat(subject.authTokenFlow.first()).isEqualTo(TEST_TOKEN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when authHeader is observed then sends null if nothing saved`() = runTest {
|
||||
assertThat(subject.authHeaderFlow.first()).isEqualTo(null)
|
||||
assertThat(subject.authTokenFlow.first()).isEqualTo(null)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,15 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import gq.kirmanak.mealient.datasource.models.VersionInfo
|
||||
import gq.kirmanak.mealient.datasource_test.VERSION_INFO_V0
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_VERSION
|
||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ServerInfoRepoTest : BaseUnitTest() {
|
||||
|
||||
private lateinit var subject: ServerInfoRepo
|
||||
@@ -54,104 +48,10 @@ class ServerInfoRepoTest : BaseUnitTest() {
|
||||
|
||||
@Test
|
||||
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 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() } returns VERSION_INFO_V0
|
||||
subject.getVersion()
|
||||
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() } returns VersionInfo(TEST_VERSION)
|
||||
subject.getVersion()
|
||||
coVerify { storage.storeServerVersion(TEST_VERSION) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when data source has invalid value expect getVersion to return v1`() = runTest {
|
||||
coEvery { storage.getServerVersion() } returns null
|
||||
coEvery { storage.getBaseURL() } returns TEST_BASE_URL
|
||||
coEvery { dataSource.getVersionInfo() } returns VersionInfo("v2.0.0")
|
||||
assertThat(subject.getVersion()).isEqualTo(ServerVersion.V1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when data source has invalid value expect getVersion to save value`() = runTest {
|
||||
coEvery { storage.getServerVersion() } returns null
|
||||
coEvery { storage.getBaseURL() } returns TEST_BASE_URL
|
||||
coEvery { dataSource.getVersionInfo() } returns VersionInfo("v2.0.0")
|
||||
subject.getVersion()
|
||||
coVerify { storage.storeServerVersion("v2.0.0") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when storage has value expect getVersion to not get URL`() = runTest {
|
||||
coEvery { storage.getServerVersion() } returns TEST_VERSION
|
||||
subject.getVersion()
|
||||
coVerify(inverse = true) { storage.getBaseURL() }
|
||||
}
|
||||
|
||||
@Test
|
||||
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() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when storage has v0 value expect getVersion to return parsed`() = runTest {
|
||||
coEvery { storage.getServerVersion() } returns "v0.5.6"
|
||||
assertThat(subject.getVersion()).isEqualTo(ServerVersion.V0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when storage has v1 value expect getVersion to return parsed`() = runTest {
|
||||
coEvery { storage.getServerVersion() } returns "v1.0.0-beta05"
|
||||
assertThat(subject.getVersion()).isEqualTo(ServerVersion.V1)
|
||||
}
|
||||
|
||||
@Test
|
||||
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() } returns VersionInfo("v0.5.6")
|
||||
assertThat(subject.getVersion()).isEqualTo(ServerVersion.V0)
|
||||
}
|
||||
|
||||
@Test
|
||||
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() } returns VersionInfo("v1.0.0-beta05")
|
||||
assertThat(subject.getVersion()).isEqualTo(ServerVersion.V1)
|
||||
}
|
||||
}
|
||||
@@ -5,18 +5,15 @@ import com.google.common.truth.Truth.assertThat
|
||||
import gq.kirmanak.mealient.data.baseurl.impl.ServerInfoStorageImpl
|
||||
import gq.kirmanak.mealient.data.storage.PreferencesStorage
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_VERSION
|
||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ServerInfoStorageTest : BaseUnitTest() {
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
@@ -25,14 +22,12 @@ class ServerInfoStorageTest : BaseUnitTest() {
|
||||
lateinit var subject: ServerInfoStorage
|
||||
|
||||
private val baseUrlKey = stringPreferencesKey("baseUrlKey")
|
||||
private val serverVersionKey = stringPreferencesKey("serverVersionKey")
|
||||
|
||||
@Before
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
subject = ServerInfoStorageImpl(preferencesStorage)
|
||||
every { preferencesStorage.baseUrlKey } returns baseUrlKey
|
||||
every { preferencesStorage.serverVersionKey } returns serverVersionKey
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -49,30 +44,11 @@ class ServerInfoStorageTest : BaseUnitTest() {
|
||||
|
||||
@Test
|
||||
fun `when storeBaseURL expect call to preferences storage`() = runTest {
|
||||
subject.storeBaseURL(TEST_BASE_URL, TEST_VERSION)
|
||||
subject.storeBaseURL(TEST_BASE_URL)
|
||||
coVerify {
|
||||
preferencesStorage.storeValues(
|
||||
eq(Pair(baseUrlKey, TEST_BASE_URL)),
|
||||
eq(Pair(serverVersionKey, TEST_VERSION)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when preference storage is empty expect getServerVersion return null`() = runTest {
|
||||
coEvery { preferencesStorage.getValue(eq(serverVersionKey)) } returns null
|
||||
assertThat(subject.getServerVersion()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when preference storage has value expect getServerVersion return value`() = runTest {
|
||||
coEvery { preferencesStorage.getValue(eq(serverVersionKey)) } returns TEST_VERSION
|
||||
assertThat(subject.getServerVersion()).isEqualTo(TEST_VERSION)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when storeServerVersion then calls preferences storage`() = runTest {
|
||||
subject.storeServerVersion(TEST_VERSION)
|
||||
coVerify { preferencesStorage.storeValues(eq(Pair(serverVersionKey, TEST_VERSION))) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,10 @@ package gq.kirmanak.mealient.data.disclaimer
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import gq.kirmanak.mealient.test.HiltRobolectricTest
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@HiltAndroidTest
|
||||
class DisclaimerStorageImplTest : HiltRobolectricTest() {
|
||||
|
||||
|
||||
@@ -12,14 +12,12 @@ import io.mockk.MockKAnnotations
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@HiltAndroidTest
|
||||
class From24AuthMigrationExecutorTest : HiltRobolectricTest() {
|
||||
|
||||
|
||||
@@ -9,11 +9,9 @@ import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MigrationDetectorImplTest : BaseUnitTest() {
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
|
||||
@@ -2,50 +2,35 @@ package gq.kirmanak.mealient.data.network
|
||||
|
||||
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.datasource.v0.MealieDataSourceV0
|
||||
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
|
||||
import gq.kirmanak.mealient.datasource.MealieDataSource
|
||||
import gq.kirmanak.mealient.datasource_test.PORRIDGE_ADD_RECIPE_INFO
|
||||
import gq.kirmanak.mealient.datasource_test.PORRIDGE_ADD_RECIPE_REQUEST_V0
|
||||
import gq.kirmanak.mealient.datasource_test.PORRIDGE_CREATE_RECIPE_REQUEST_V1
|
||||
import gq.kirmanak.mealient.datasource_test.PORRIDGE_FULL_RECIPE_INFO
|
||||
import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_RESPONSE_V1
|
||||
import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0
|
||||
import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1
|
||||
import gq.kirmanak.mealient.datasource_test.PORRIDGE_UPDATE_RECIPE_REQUEST_V1
|
||||
import gq.kirmanak.mealient.datasource_test.RECIPE_SUMMARY_PORRIDGE_V0
|
||||
import gq.kirmanak.mealient.datasource_test.RECIPE_SUMMARY_PORRIDGE_V1
|
||||
import gq.kirmanak.mealient.datasource_test.PORRIDGE_CREATE_RECIPE_REQUEST
|
||||
import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_RESPONSE
|
||||
import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_SUMMARY_RESPONSE
|
||||
import gq.kirmanak.mealient.datasource_test.PORRIDGE_UPDATE_RECIPE_REQUEST
|
||||
import gq.kirmanak.mealient.datasource_test.RECIPE_SUMMARY_PORRIDGE
|
||||
import gq.kirmanak.mealient.model_mapper.ModelMapper
|
||||
import gq.kirmanak.mealient.model_mapper.ModelMapperImpl
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.FAVORITE_RECIPES_LIST
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
|
||||
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.USER_INFO_V0
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.USER_INFO_V1
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.USER_INFO
|
||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
import io.mockk.*
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.coVerifySequence
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MealieDataSourceWrapperTest : BaseUnitTest() {
|
||||
|
||||
@MockK
|
||||
lateinit var serverInfoRepo: ServerInfoRepo
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
lateinit var authRepo: AuthRepo
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
lateinit var v0Source: MealieDataSourceV0
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
lateinit var v1Source: MealieDataSourceV1
|
||||
lateinit var dataSource: MealieDataSource
|
||||
|
||||
private val modelMapper: ModelMapper = ModelMapperImpl()
|
||||
|
||||
@@ -54,111 +39,68 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
|
||||
@Before
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
subject = MealieDataSourceWrapper(serverInfoRepo, v0Source, v1Source, modelMapper)
|
||||
coEvery { v0Source.requestUserInfo() } returns USER_INFO_V0
|
||||
coEvery { v1Source.requestUserInfo() } returns USER_INFO_V1
|
||||
subject = MealieDataSourceWrapper(dataSource, modelMapper)
|
||||
coEvery { dataSource.requestUserInfo() } returns USER_INFO
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when server version v1 expect requestRecipeInfo to call v1`() = runTest {
|
||||
fun `when requestRecipeInfo expect a valid network call`() = runTest {
|
||||
val slug = "porridge"
|
||||
coEvery { v1Source.requestRecipeInfo(eq(slug)) } returns PORRIDGE_RECIPE_RESPONSE_V1
|
||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
|
||||
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
|
||||
coEvery { dataSource.requestRecipeInfo(eq(slug)) } returns PORRIDGE_RECIPE_RESPONSE
|
||||
coEvery { authRepo.getAuthToken() } returns TEST_TOKEN
|
||||
|
||||
val actual = subject.requestRecipeInfo(slug)
|
||||
val actual = subject.requestRecipe(slug)
|
||||
|
||||
coVerify { v1Source.requestRecipeInfo(eq(slug)) }
|
||||
coVerify { dataSource.requestRecipeInfo(eq(slug)) }
|
||||
|
||||
assertThat(actual).isEqualTo(PORRIDGE_FULL_RECIPE_INFO)
|
||||
assertThat(actual).isEqualTo(PORRIDGE_RECIPE_RESPONSE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when server version v1 expect requestRecipes to call v1`() = runTest {
|
||||
fun `when requestRecipes expect valid network request`() = runTest {
|
||||
coEvery {
|
||||
v1Source.requestRecipes(any(), any())
|
||||
} returns listOf(PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1)
|
||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
|
||||
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
|
||||
dataSource.requestRecipes(any(), any())
|
||||
} returns listOf(PORRIDGE_RECIPE_SUMMARY_RESPONSE)
|
||||
coEvery { authRepo.getAuthToken() } returns TEST_TOKEN
|
||||
|
||||
val actual = subject.requestRecipes(40, 10)
|
||||
|
||||
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(page), eq(perPage))
|
||||
dataSource.requestRecipes(eq(page), eq(perPage))
|
||||
}
|
||||
|
||||
assertThat(actual).isEqualTo(listOf(RECIPE_SUMMARY_PORRIDGE_V1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when server version v0 expect requestRecipes to call v0`() = runTest {
|
||||
coEvery {
|
||||
v0Source.requestRecipes(any(), any())
|
||||
} returns listOf(PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0)
|
||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
||||
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
|
||||
|
||||
val start = 40
|
||||
val limit = 10
|
||||
val actual = subject.requestRecipes(start, limit)
|
||||
|
||||
coVerify {
|
||||
v0Source.requestRecipes(eq(start), eq(limit))
|
||||
}
|
||||
|
||||
assertThat(actual).isEqualTo(listOf(RECIPE_SUMMARY_PORRIDGE_V0))
|
||||
assertThat(actual).isEqualTo(listOf(RECIPE_SUMMARY_PORRIDGE))
|
||||
}
|
||||
|
||||
@Test(expected = IOException::class)
|
||||
fun `when request fails expect addRecipe to rethrow`() = runTest {
|
||||
coEvery { v0Source.addRecipe(any()) } throws IOException()
|
||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
||||
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
|
||||
fun `when request fails expect createRecipe to rethrow`() = runTest {
|
||||
coEvery { dataSource.createRecipe(any()) } throws IOException()
|
||||
coEvery { authRepo.getAuthToken() } returns TEST_TOKEN
|
||||
subject.addRecipe(PORRIDGE_ADD_RECIPE_INFO)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when server version v0 expect addRecipe to call v0`() = runTest {
|
||||
fun `when create recipe expect createRecipe to call in sequence`() = runTest {
|
||||
val slug = "porridge"
|
||||
|
||||
coEvery { v0Source.addRecipe(any()) } returns slug
|
||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
||||
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
|
||||
|
||||
val actual = subject.addRecipe(PORRIDGE_ADD_RECIPE_INFO)
|
||||
|
||||
coVerify {
|
||||
v0Source.addRecipe(
|
||||
eq(PORRIDGE_ADD_RECIPE_REQUEST_V0),
|
||||
)
|
||||
}
|
||||
|
||||
assertThat(actual).isEqualTo(slug)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when server version v1 expect addRecipe to call v1`() = runTest {
|
||||
val slug = "porridge"
|
||||
|
||||
coEvery { v1Source.createRecipe(any()) } returns slug
|
||||
coEvery { dataSource.createRecipe(any()) } returns slug
|
||||
coEvery {
|
||||
v1Source.updateRecipe(any(), any())
|
||||
} returns PORRIDGE_RECIPE_RESPONSE_V1
|
||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
|
||||
coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER
|
||||
dataSource.updateRecipe(any(), any())
|
||||
} returns PORRIDGE_RECIPE_RESPONSE
|
||||
coEvery { authRepo.getAuthToken() } returns TEST_TOKEN
|
||||
|
||||
val actual = subject.addRecipe(PORRIDGE_ADD_RECIPE_INFO)
|
||||
|
||||
coVerifySequence {
|
||||
v1Source.createRecipe(
|
||||
eq(PORRIDGE_CREATE_RECIPE_REQUEST_V1),
|
||||
dataSource.createRecipe(
|
||||
eq(PORRIDGE_CREATE_RECIPE_REQUEST),
|
||||
)
|
||||
|
||||
v1Source.updateRecipe(
|
||||
dataSource.updateRecipe(
|
||||
eq(slug),
|
||||
eq(PORRIDGE_UPDATE_RECIPE_REQUEST_V1),
|
||||
eq(PORRIDGE_UPDATE_RECIPE_REQUEST),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -166,68 +108,31 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when remove favorite recipe info with v0 expect correct sequence`() = runTest {
|
||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
||||
fun `when remove favorite recipe info expect correct sequence`() = runTest {
|
||||
subject.updateIsRecipeFavorite(recipeSlug = "cake", isFavorite = false)
|
||||
coVerify {
|
||||
v0Source.requestUserInfo()
|
||||
v0Source.removeFavoriteRecipe(eq(3), eq("cake"))
|
||||
dataSource.requestUserInfo()
|
||||
dataSource.removeFavoriteRecipe(eq("userId"), eq("cake"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when remove favorite recipe info with v1 expect correct sequence`() = runTest {
|
||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
|
||||
subject.updateIsRecipeFavorite(recipeSlug = "cake", isFavorite = false)
|
||||
coVerify {
|
||||
v1Source.requestUserInfo()
|
||||
v1Source.removeFavoriteRecipe(eq("userId"), eq("cake"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when add favorite recipe info with v0 expect correct sequence`() = runTest {
|
||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
||||
fun `when add favorite recipe info expect correct sequence`() = runTest {
|
||||
subject.updateIsRecipeFavorite(recipeSlug = "cake", isFavorite = true)
|
||||
coVerify {
|
||||
v0Source.requestUserInfo()
|
||||
v0Source.addFavoriteRecipe(eq(3), eq("cake"))
|
||||
dataSource.requestUserInfo()
|
||||
dataSource.addFavoriteRecipe(eq("userId"), eq("cake"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when add favorite recipe info with v1 expect correct sequence`() = runTest {
|
||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
|
||||
subject.updateIsRecipeFavorite(recipeSlug = "cake", isFavorite = true)
|
||||
coVerify {
|
||||
v1Source.requestUserInfo()
|
||||
v1Source.addFavoriteRecipe(eq("userId"), eq("cake"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when get favorite recipes with v1 expect correct call`() = runTest {
|
||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
|
||||
fun `when get favorite recipes expect correct call`() = runTest {
|
||||
subject.getFavoriteRecipes()
|
||||
coVerify { v1Source.requestUserInfo() }
|
||||
coVerify { dataSource.requestUserInfo() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when get favorite recipes with v0 expect correct call`() = runTest {
|
||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
||||
subject.getFavoriteRecipes()
|
||||
coVerify { v0Source.requestUserInfo() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when get favorite recipes with v1 expect correct result`() = runTest {
|
||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V1
|
||||
assertThat(subject.getFavoriteRecipes()).isEqualTo(FAVORITE_RECIPES_LIST)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when get favorite recipes with v0 expect correct result`() = runTest {
|
||||
coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION_V0
|
||||
fun `when get favorite recipes expect correct result`() = runTest {
|
||||
assertThat(subject.getFavoriteRecipes()).isEqualTo(FAVORITE_RECIPES_LIST)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,26 @@
|
||||
package gq.kirmanak.mealient.data.recipes.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
|
||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerInfoStorage
|
||||
import gq.kirmanak.mealient.test.HiltRobolectricTest
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class RecipeImageUrlProviderImplTest : BaseUnitTest() {
|
||||
@HiltAndroidTest
|
||||
class RecipeImageUrlProviderImplTest : HiltRobolectricTest() {
|
||||
|
||||
@Inject
|
||||
lateinit var subject: RecipeImageUrlProvider
|
||||
|
||||
@MockK
|
||||
lateinit var serverInfoRepo: ServerInfoRepo
|
||||
@Inject
|
||||
lateinit var serverInfoStorage: ServerInfoStorage
|
||||
|
||||
@Before
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
subject = RecipeImageUrlProviderImpl(serverInfoRepo, logger)
|
||||
fun setUp() {
|
||||
prepareBaseURL("https://google.com/")
|
||||
}
|
||||
|
||||
@@ -76,7 +75,7 @@ class RecipeImageUrlProviderImplTest : BaseUnitTest() {
|
||||
assertThat(actual).isNull()
|
||||
}
|
||||
|
||||
private fun prepareBaseURL(baseURL: String?) {
|
||||
coEvery { serverInfoRepo.getUrl() } returns baseURL
|
||||
private fun prepareBaseURL(baseURL: String?) = runBlocking {
|
||||
serverInfoStorage.storeBaseURL(baseURL)
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,11 @@ import gq.kirmanak.mealient.database.TEST_RECIPE_SUMMARY_ENTITIES
|
||||
import gq.kirmanak.mealient.database.recipe.RecipeStorage
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.test.HiltRobolectricTest
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class RecipePagingSourceFactoryImplTest : HiltRobolectricTest() {
|
||||
|
||||
@Inject
|
||||
|
||||
@@ -13,7 +13,7 @@ import gq.kirmanak.mealient.database.FULL_CAKE_INFO_ENTITY
|
||||
import gq.kirmanak.mealient.database.MIX_CAKE_RECIPE_INSTRUCTION_ENTITY
|
||||
import gq.kirmanak.mealient.database.recipe.RecipeStorage
|
||||
import gq.kirmanak.mealient.datasource.NetworkError.Unauthorized
|
||||
import gq.kirmanak.mealient.datasource_test.CAKE_FULL_RECIPE_INFO
|
||||
import gq.kirmanak.mealient.datasource_test.CAKE_RECIPE_RESPONSE
|
||||
import gq.kirmanak.mealient.model_mapper.ModelMapper
|
||||
import gq.kirmanak.mealient.model_mapper.ModelMapperImpl
|
||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
@@ -22,13 +22,11 @@ import io.mockk.coVerify
|
||||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.verify
|
||||
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 RecipeRepoTest : BaseUnitTest() {
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
@@ -69,7 +67,7 @@ class RecipeRepoTest : BaseUnitTest() {
|
||||
|
||||
@Test
|
||||
fun `when refreshRecipeInfo expect call to storage`() = runTest {
|
||||
coEvery { dataSource.requestRecipeInfo(eq("cake")) } returns CAKE_FULL_RECIPE_INFO
|
||||
coEvery { dataSource.requestRecipe(eq("cake")) } returns CAKE_RECIPE_RESPONSE
|
||||
subject.refreshRecipeInfo("cake")
|
||||
coVerify {
|
||||
storage.saveRecipeInfo(
|
||||
|
||||
@@ -16,13 +16,11 @@ import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class RecipesRemoteMediatorTest : BaseUnitTest() {
|
||||
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package gq.kirmanak.mealient.data.share
|
||||
|
||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
|
||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLRequest
|
||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ShareRecipeRepoImplTest : BaseUnitTest() {
|
||||
|
||||
|
||||
@@ -32,7 +30,7 @@ class ShareRecipeRepoImplTest : BaseUnitTest() {
|
||||
@Test
|
||||
fun `when url is correct expect saveRecipeByURL saves it`() = runTest {
|
||||
subject.saveRecipeByURL("https://www.allrecipes.com/recipe/215447/dads-leftover-turkey-pot-pie/")
|
||||
val expected = ParseRecipeURLInfo(
|
||||
val expected = ParseRecipeURLRequest(
|
||||
url = "https://www.allrecipes.com/recipe/215447/dads-leftover-turkey-pot-pie/",
|
||||
includeTags = true
|
||||
)
|
||||
@@ -42,7 +40,7 @@ class ShareRecipeRepoImplTest : BaseUnitTest() {
|
||||
@Test
|
||||
fun `when url has prefix expect saveRecipeByURL removes it`() = runTest {
|
||||
subject.saveRecipeByURL("My favorite recipe: https://www.allrecipes.com/recipe/215447/dads-leftover-turkey-pot-pie/")
|
||||
val expected = ParseRecipeURLInfo(
|
||||
val expected = ParseRecipeURLRequest(
|
||||
url = "https://www.allrecipes.com/recipe/215447/dads-leftover-turkey-pot-pie/",
|
||||
includeTags = true
|
||||
)
|
||||
@@ -52,7 +50,7 @@ class ShareRecipeRepoImplTest : BaseUnitTest() {
|
||||
@Test
|
||||
fun `when url has suffix expect saveRecipeByURL removes it`() = runTest {
|
||||
subject.saveRecipeByURL("https://www.allrecipes.com/recipe/215447/dads-leftover-turkey-pot-pie/ is my favorite recipe")
|
||||
val expected = ParseRecipeURLInfo(
|
||||
val expected = ParseRecipeURLRequest(
|
||||
url = "https://www.allrecipes.com/recipe/215447/dads-leftover-turkey-pot-pie",
|
||||
includeTags = true
|
||||
)
|
||||
@@ -62,7 +60,7 @@ class ShareRecipeRepoImplTest : BaseUnitTest() {
|
||||
@Test
|
||||
fun `when url has prefix and suffix expect saveRecipeByURL removes them`() = runTest {
|
||||
subject.saveRecipeByURL("Actually, https://www.allrecipes.com/recipe/215447/dads-leftover-turkey-pot-pie/ is my favorite recipe")
|
||||
val expected = ParseRecipeURLInfo(
|
||||
val expected = ParseRecipeURLRequest(
|
||||
url = "https://www.allrecipes.com/recipe/215447/dads-leftover-turkey-pot-pie",
|
||||
includeTags = true
|
||||
)
|
||||
|
||||
@@ -3,13 +3,11 @@ package gq.kirmanak.mealient.data.storage
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import gq.kirmanak.mealient.test.HiltRobolectricTest
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@HiltAndroidTest
|
||||
class PreferencesStorageImplTest : HiltRobolectricTest() {
|
||||
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
package gq.kirmanak.mealient.test
|
||||
|
||||
import gq.kirmanak.mealient.data.baseurl.ServerVersion
|
||||
import gq.kirmanak.mealient.datasource.v0.models.GetUserInfoResponseV0
|
||||
import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1
|
||||
import gq.kirmanak.mealient.datasource.models.GetUserInfoResponse
|
||||
|
||||
object AuthImplTestData {
|
||||
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"
|
||||
const val TEST_AUTH_HEADER = "Bearer TEST_TOKEN"
|
||||
const val TEST_API_TOKEN = "TEST_API_TOKEN"
|
||||
const val TEST_API_AUTH_HEADER = "Bearer TEST_API_TOKEN"
|
||||
const val TEST_VERSION = "v0.5.6"
|
||||
val TEST_SERVER_VERSION_V0 = ServerVersion.V0
|
||||
val TEST_SERVER_VERSION_V1 = ServerVersion.V1
|
||||
|
||||
val FAVORITE_RECIPES_LIST = listOf("cake", "porridge")
|
||||
val USER_INFO_V1 = GetUserInfoResponseV1("userId", FAVORITE_RECIPES_LIST)
|
||||
val USER_INFO_V0 = GetUserInfoResponseV0(3, FAVORITE_RECIPES_LIST)
|
||||
val USER_INFO = GetUserInfoResponse("userId", FAVORITE_RECIPES_LIST)
|
||||
}
|
||||
@@ -45,7 +45,6 @@ class MainActivityViewModelTest : BaseUnitTest() {
|
||||
every { activityUiStateController.getUiStateFlow() } returns MutableStateFlow(
|
||||
ActivityUiState()
|
||||
)
|
||||
coEvery { serverInfoRepo.versionUpdates() } returns emptyFlow()
|
||||
subject = MainActivityViewModel(
|
||||
authRepo = authRepo,
|
||||
logger = logger,
|
||||
|
||||
@@ -7,7 +7,6 @@ import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
@@ -16,7 +15,6 @@ import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class AddRecipeViewModelTest : BaseUnitTest() {
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
|
||||
@@ -7,12 +7,10 @@ import gq.kirmanak.mealient.database.FULL_CAKE_INFO_ENTITY
|
||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class RecipeInfoViewModelTest : BaseUnitTest() {
|
||||
|
||||
@MockK
|
||||
|
||||
@@ -9,7 +9,6 @@ import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.toList
|
||||
@@ -17,7 +16,6 @@ import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ShareRecipeViewModelTest : BaseUnitTest() {
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
|
||||
@@ -2,13 +2,11 @@ package gq.kirmanak.mealient.architecture
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class FlowExtensionsKtTest : BaseUnitTest() {
|
||||
|
||||
@Test
|
||||
|
||||
@@ -5,13 +5,11 @@ import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import gq.kirmanak.mealient.database.recipe.RecipeDao
|
||||
import gq.kirmanak.mealient.database.recipe.RecipeStorageImpl
|
||||
import gq.kirmanak.mealient.test.HiltRobolectricTest
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class RecipeStorageImplTest : HiltRobolectricTest() {
|
||||
|
||||
@Inject
|
||||
|
||||
@@ -15,7 +15,7 @@ val CAKE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity(
|
||||
description = "A tasty cake",
|
||||
dateAdded = LocalDate.parse("2021-11-13"),
|
||||
dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13"),
|
||||
imageId = "cake",
|
||||
imageId = "1",
|
||||
isFavorite = false,
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ val PORRIDGE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity(
|
||||
description = "A tasty porridge",
|
||||
dateAdded = LocalDate.parse("2021-11-12"),
|
||||
dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"),
|
||||
imageId = "porridge",
|
||||
imageId = "2",
|
||||
isFavorite = false,
|
||||
)
|
||||
|
||||
|
||||
@@ -25,16 +25,19 @@ dependencies {
|
||||
|
||||
implementation(libs.jetbrains.kotlinx.serialization)
|
||||
|
||||
implementation(libs.squareup.retrofit)
|
||||
|
||||
implementation(libs.jakewharton.retrofitSerialization)
|
||||
implementation(libs.jetbrains.kotlinx.coroutinesAndroid)
|
||||
testImplementation(libs.jetbrains.kotlinx.coroutinesTest)
|
||||
|
||||
implementation(platform(libs.okhttp3.bom))
|
||||
implementation(libs.okhttp3.okhttp)
|
||||
debugImplementation(libs.okhttp3.loggingInterceptor)
|
||||
|
||||
implementation(libs.jetbrains.kotlinx.coroutinesAndroid)
|
||||
testImplementation(libs.jetbrains.kotlinx.coroutinesTest)
|
||||
implementation(libs.ktor.core)
|
||||
implementation(libs.ktor.auth)
|
||||
implementation(libs.ktor.encoding)
|
||||
implementation(libs.ktor.negotiation)
|
||||
implementation(libs.ktor.json)
|
||||
implementation(libs.ktor.okhttp)
|
||||
|
||||
testImplementation(libs.androidx.test.junit)
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ import okhttp3.logging.HttpLoggingInterceptor
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DebugModule {
|
||||
internal object DebugModule {
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideLoggingInterceptor(logger: Logger): Interceptor {
|
||||
|
||||
@@ -2,8 +2,7 @@ package gq.kirmanak.mealient.datasource
|
||||
|
||||
interface AuthenticationProvider {
|
||||
|
||||
suspend fun getAuthHeader(): String?
|
||||
suspend fun getAuthToken(): String?
|
||||
|
||||
suspend fun logout()
|
||||
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package gq.kirmanak.mealient.datasource
|
||||
|
||||
import okhttp3.Cache
|
||||
|
||||
interface CacheBuilder {
|
||||
|
||||
fun buildCache(): Cache
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
package gq.kirmanak.mealient.datasource
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import okhttp3.ResponseBody
|
||||
|
||||
/**
|
||||
* Like [runCatching] but rethrows [CancellationException] to support
|
||||
@@ -18,9 +14,6 @@ inline fun <T> runCatchingExceptCancel(block: () -> T): Result<T> = try {
|
||||
Result.failure(e)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
inline fun <reified R> ResponseBody.decode(json: Json): R = json.decodeFromStream(byteStream())
|
||||
|
||||
inline fun <reified T> Throwable.findCauseAsInstanceOf(): T? {
|
||||
var cause: Throwable? = this
|
||||
var previousCause: Throwable? = null
|
||||
|
||||
@@ -1,32 +1,17 @@
|
||||
package gq.kirmanak.mealient.datasource
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
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.MealieDataSourceImpl
|
||||
import gq.kirmanak.mealient.datasource.impl.MealieServiceKtor
|
||||
import gq.kirmanak.mealient.datasource.impl.NetworkRequestWrapperImpl
|
||||
import gq.kirmanak.mealient.datasource.impl.OkHttpBuilderImpl
|
||||
import gq.kirmanak.mealient.datasource.impl.RetrofitBuilder
|
||||
import gq.kirmanak.mealient.datasource.impl.TrustedCertificatesStoreImpl
|
||||
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
|
||||
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0Impl
|
||||
import gq.kirmanak.mealient.datasource.v0.MealieServiceV0
|
||||
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
|
||||
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.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -43,59 +28,22 @@ internal interface DataSourceModule {
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideConverterFactory(json: Json): Converter.Factory =
|
||||
json.asConverterFactory("application/json".toMediaType())
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttp(okHttpBuilder: OkHttpBuilder): OkHttpClient =
|
||||
fun provideOkHttp(okHttpBuilder: OkHttpBuilderImpl): OkHttpClient =
|
||||
okHttpBuilder.buildOkHttp()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
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
|
||||
fun provideMealieService(retrofit: Retrofit): MealieServiceV0 =
|
||||
retrofit.create()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMealieServiceV1(retrofit: Retrofit): MealieServiceV1 =
|
||||
retrofit.create()
|
||||
}
|
||||
|
||||
@Binds
|
||||
fun bindCacheBuilder(cacheBuilderImpl: CacheBuilderImpl): CacheBuilder
|
||||
fun bindMealieDataSource(mealientDataSourceImpl: MealieDataSourceImpl): MealieDataSource
|
||||
|
||||
@Binds
|
||||
fun bindOkHttpBuilder(okHttpBuilderImpl: OkHttpBuilderImpl): OkHttpBuilder
|
||||
|
||||
@Binds
|
||||
fun bindMealieDataSource(mealientDataSourceImpl: MealieDataSourceV0Impl): MealieDataSourceV0
|
||||
|
||||
@Binds
|
||||
fun bindMealieDataSourceV1(mealientDataSourceImpl: MealieDataSourceV1Impl): MealieDataSourceV1
|
||||
fun bindMealieService(impl: MealieServiceKtor): MealieService
|
||||
|
||||
@Binds
|
||||
fun bindNetworkRequestWrapper(networkRequestWrapperImpl: NetworkRequestWrapperImpl): NetworkRequestWrapper
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
fun bindAuthInterceptor(authInterceptor: AuthInterceptor): LocalInterceptor
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
fun bindBaseUrlInterceptor(baseUrlInterceptor: BaseUrlInterceptor): LocalInterceptor
|
||||
|
||||
@Binds
|
||||
fun bindTrustedCertificatesStore(impl: TrustedCertificatesStoreImpl): TrustedCertificatesStore
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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
|
||||
@@ -0,0 +1,78 @@
|
||||
package gq.kirmanak.mealient.datasource
|
||||
|
||||
import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
|
||||
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
|
||||
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
|
||||
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
||||
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListsResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetUnitsResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetUserInfoResponse
|
||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLRequest
|
||||
import gq.kirmanak.mealient.datasource.models.UpdateRecipeRequest
|
||||
import gq.kirmanak.mealient.datasource.models.VersionResponse
|
||||
|
||||
interface MealieDataSource {
|
||||
|
||||
suspend fun createRecipe(
|
||||
recipe: CreateRecipeRequest,
|
||||
): String
|
||||
|
||||
suspend fun updateRecipe(
|
||||
slug: String,
|
||||
recipe: UpdateRecipeRequest,
|
||||
): GetRecipeResponse
|
||||
|
||||
/**
|
||||
* Tries to acquire authentication token using the provided credentials
|
||||
*/
|
||||
suspend fun authenticate(
|
||||
username: String,
|
||||
password: String,
|
||||
): String
|
||||
|
||||
suspend fun getVersionInfo(): VersionResponse
|
||||
|
||||
suspend fun requestRecipes(
|
||||
page: Int,
|
||||
perPage: Int,
|
||||
): List<GetRecipeSummaryResponse>
|
||||
|
||||
suspend fun requestRecipeInfo(
|
||||
slug: String,
|
||||
): GetRecipeResponse
|
||||
|
||||
suspend fun parseRecipeFromURL(
|
||||
request: ParseRecipeURLRequest,
|
||||
): String
|
||||
|
||||
suspend fun createApiToken(
|
||||
request: CreateApiTokenRequest,
|
||||
): CreateApiTokenResponse
|
||||
|
||||
suspend fun requestUserInfo(): GetUserInfoResponse
|
||||
|
||||
suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String)
|
||||
|
||||
suspend fun addFavoriteRecipe(userId: String, recipeSlug: String)
|
||||
|
||||
suspend fun deleteRecipe(slug: String)
|
||||
|
||||
suspend fun getShoppingLists(page: Int, perPage: Int): GetShoppingListsResponse
|
||||
|
||||
suspend fun getShoppingList(id: String): GetShoppingListResponse
|
||||
|
||||
suspend fun deleteShoppingListItem(id: String)
|
||||
|
||||
suspend fun updateShoppingListItem(item: GetShoppingListItemResponse)
|
||||
|
||||
suspend fun getFoods(): GetFoodsResponse
|
||||
|
||||
suspend fun getUnits(): GetUnitsResponse
|
||||
|
||||
suspend fun addShoppingListItem(request: CreateShoppingListItemRequest)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package gq.kirmanak.mealient.datasource
|
||||
|
||||
import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
|
||||
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
|
||||
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
|
||||
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
||||
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipesResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListsResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetTokenResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetUnitsResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetUserInfoResponse
|
||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLRequest
|
||||
import gq.kirmanak.mealient.datasource.models.UpdateRecipeRequest
|
||||
import gq.kirmanak.mealient.datasource.models.VersionResponse
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
internal interface MealieService {
|
||||
|
||||
suspend fun getToken(username: String, password: String): GetTokenResponse
|
||||
|
||||
suspend fun createRecipe(addRecipeRequest: CreateRecipeRequest): String
|
||||
|
||||
suspend fun updateRecipe(
|
||||
addRecipeRequest: UpdateRecipeRequest,
|
||||
slug: String,
|
||||
): GetRecipeResponse
|
||||
|
||||
suspend fun getVersion(): VersionResponse
|
||||
|
||||
suspend fun getRecipeSummary(page: Int, perPage: Int): GetRecipesResponse
|
||||
|
||||
suspend fun getRecipe(slug: String): GetRecipeResponse
|
||||
|
||||
suspend fun createRecipeFromURL(request: ParseRecipeURLRequest): String
|
||||
|
||||
suspend fun createApiToken(request: CreateApiTokenRequest): CreateApiTokenResponse
|
||||
|
||||
suspend fun getUserSelfInfo(): GetUserInfoResponse
|
||||
|
||||
suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String)
|
||||
|
||||
suspend fun addFavoriteRecipe(userId: String, recipeSlug: String)
|
||||
|
||||
suspend fun deleteRecipe(slug: String)
|
||||
|
||||
suspend fun getShoppingLists(page: Int, perPage: Int): GetShoppingListsResponse
|
||||
|
||||
suspend fun getShoppingList(id: String): GetShoppingListResponse
|
||||
|
||||
suspend fun getShoppingListItem(id: String): JsonElement
|
||||
|
||||
suspend fun updateShoppingListItem(id: String, request: JsonElement)
|
||||
|
||||
suspend fun deleteShoppingListItem(id: String)
|
||||
|
||||
suspend fun getFoods(perPage: Int): GetFoodsResponse
|
||||
|
||||
suspend fun getUnits(perPage: Int): GetUnitsResponse
|
||||
|
||||
suspend fun createShoppingListItem(request: CreateShoppingListItemRequest)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package gq.kirmanak.mealient.datasource
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
interface OkHttpBuilder {
|
||||
|
||||
fun buildOkHttp(): OkHttpClient
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package gq.kirmanak.mealient.datasource
|
||||
|
||||
interface SignOutHandler {
|
||||
|
||||
fun signOut()
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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
|
||||
import okhttp3.Response
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
internal class AuthInterceptor @Inject constructor(
|
||||
private val logger: Logger,
|
||||
private val authenticationProviderProvider: Provider<AuthenticationProvider>,
|
||||
) : LocalInterceptor {
|
||||
|
||||
private val authenticationProvider: AuthenticationProvider
|
||||
get() = authenticationProviderProvider.get()
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
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()
|
||||
}
|
||||
logger.d { "Sending header $HEADER_NAME=${request.header(HEADER_NAME)}" }
|
||||
return chain.proceed(request).also {
|
||||
logger.v { "Response code is ${it.code}" }
|
||||
if (it.code == 401 && header != null) logout()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAuthHeader() = runBlocking { authenticationProvider.getAuthHeader() }
|
||||
|
||||
private fun logout() = runBlocking { authenticationProvider.logout() }
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
const val HEADER_NAME = "Authorization"
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
internal class BaseUrlInterceptor @Inject constructor(
|
||||
private val logger: Logger,
|
||||
private val serverUrlProviderProvider: Provider<ServerUrlProvider>,
|
||||
) : 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)
|
||||
.port(baseUrl.port)
|
||||
.build()
|
||||
val newRequest = oldRequest.newBuilder().url(correctUrl).build()
|
||||
logger.d { "Replaced ${oldRequest.url} with ${newRequest.url}" }
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
|
||||
private fun getBaseUrl() = runBlocking {
|
||||
val url = serverUrlProvider.getUrl() ?: throw IOException("Base URL is unknown")
|
||||
url.runCatching {
|
||||
toHttpUrl()
|
||||
}.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { throw IOException(it.message, it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package gq.kirmanak.mealient.datasource.impl
|
||||
import android.content.Context
|
||||
import android.os.StatFs
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import gq.kirmanak.mealient.datasource.CacheBuilder
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import okhttp3.Cache
|
||||
import java.io.File
|
||||
@@ -12,9 +11,9 @@ import javax.inject.Inject
|
||||
internal class CacheBuilderImpl @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val logger: Logger,
|
||||
) : CacheBuilder {
|
||||
) {
|
||||
|
||||
override fun buildCache(): Cache {
|
||||
fun buildCache(): Cache {
|
||||
val dir = findCacheDir()
|
||||
return Cache(dir, calculateDiskCacheSize(dir))
|
||||
}
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
package gq.kirmanak.mealient.datasource.v1
|
||||
package gq.kirmanak.mealient.datasource.impl
|
||||
|
||||
import gq.kirmanak.mealient.datasource.MealieDataSource
|
||||
import gq.kirmanak.mealient.datasource.MealieService
|
||||
import gq.kirmanak.mealient.datasource.NetworkError
|
||||
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
|
||||
import gq.kirmanak.mealient.datasource.decode
|
||||
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.CreateShoppingListItemRequestV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.ErrorDetailV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.GetFoodsResponseV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.GetUnitsResponseV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
|
||||
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
|
||||
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
|
||||
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
||||
import gq.kirmanak.mealient.datasource.models.ErrorDetail
|
||||
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListsResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetUnitsResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetUserInfoResponse
|
||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLRequest
|
||||
import gq.kirmanak.mealient.datasource.models.UpdateRecipeRequest
|
||||
import gq.kirmanak.mealient.datasource.models.VersionResponse
|
||||
import io.ktor.client.call.NoTransformationFoundException
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.ResponseException
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import retrofit2.HttpException
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketException
|
||||
import java.net.SocketTimeoutException
|
||||
import javax.inject.Inject
|
||||
|
||||
class MealieDataSourceV1Impl @Inject constructor(
|
||||
internal class MealieDataSourceImpl @Inject constructor(
|
||||
private val networkRequestWrapper: NetworkRequestWrapper,
|
||||
private val service: MealieServiceV1,
|
||||
private val json: Json,
|
||||
) : MealieDataSourceV1 {
|
||||
private val service: MealieService,
|
||||
) : MealieDataSource {
|
||||
|
||||
override suspend fun createRecipe(
|
||||
recipe: CreateRecipeRequestV1
|
||||
recipe: CreateRecipeRequest
|
||||
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.createRecipe(recipe) },
|
||||
logMethod = { "createRecipe" },
|
||||
logParameters = { "recipe = $recipe" }
|
||||
)
|
||||
).trim('"')
|
||||
|
||||
override suspend fun updateRecipe(
|
||||
slug: String,
|
||||
recipe: UpdateRecipeRequestV1
|
||||
): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
recipe: UpdateRecipeRequest
|
||||
): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.updateRecipe(recipe, slug) },
|
||||
logMethod = { "updateRecipe" },
|
||||
logParameters = { "slug = $slug, recipe = $recipe" }
|
||||
@@ -61,18 +61,17 @@ class MealieDataSourceV1Impl @Inject constructor(
|
||||
logMethod = { "authenticate" },
|
||||
logParameters = { "username = $username, password = $password" }
|
||||
).map { it.accessToken }.getOrElse {
|
||||
val errorBody = (it as? HttpException)?.response()?.errorBody() ?: throw it
|
||||
val errorDetailV0 = errorBody.decode<ErrorDetailV1>(json)
|
||||
throw if (errorDetailV0.detail == "Unauthorized") NetworkError.Unauthorized(it) else it
|
||||
val errorDetail = (it as? ResponseException)?.response?.body<ErrorDetail>() ?: throw it
|
||||
throw if (errorDetail.detail == "Unauthorized") NetworkError.Unauthorized(it) else it
|
||||
}
|
||||
|
||||
override suspend fun getVersionInfo(): VersionResponseV1 = networkRequestWrapper.makeCall(
|
||||
override suspend fun getVersionInfo(): VersionResponse = networkRequestWrapper.makeCall(
|
||||
block = { service.getVersion() },
|
||||
logMethod = { "getVersionInfo" },
|
||||
).getOrElse {
|
||||
throw when (it) {
|
||||
is HttpException, is SerializationException -> NetworkError.NotMealie(it)
|
||||
is SocketTimeoutException, is ConnectException -> NetworkError.NoServerConnection(it)
|
||||
is ResponseException, is NoTransformationFoundException -> NetworkError.NotMealie(it)
|
||||
is SocketTimeoutException, is SocketException -> NetworkError.NoServerConnection(it)
|
||||
else -> NetworkError.MalformedUrl(it)
|
||||
}
|
||||
}
|
||||
@@ -80,7 +79,7 @@ class MealieDataSourceV1Impl @Inject constructor(
|
||||
override suspend fun requestRecipes(
|
||||
page: Int,
|
||||
perPage: Int
|
||||
): List<GetRecipeSummaryResponseV1> = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
): List<GetRecipeSummaryResponse> = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.getRecipeSummary(page, perPage) },
|
||||
logMethod = { "requestRecipes" },
|
||||
logParameters = { "page = $page, perPage = $perPage" }
|
||||
@@ -88,14 +87,14 @@ class MealieDataSourceV1Impl @Inject constructor(
|
||||
|
||||
override suspend fun requestRecipeInfo(
|
||||
slug: String
|
||||
): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.getRecipe(slug) },
|
||||
logMethod = { "requestRecipeInfo" },
|
||||
logParameters = { "slug = $slug" }
|
||||
)
|
||||
|
||||
override suspend fun parseRecipeFromURL(
|
||||
request: ParseRecipeURLRequestV1
|
||||
request: ParseRecipeURLRequest
|
||||
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.createRecipeFromURL(request) },
|
||||
logMethod = { "parseRecipeFromURL" },
|
||||
@@ -103,14 +102,14 @@ class MealieDataSourceV1Impl @Inject constructor(
|
||||
)
|
||||
|
||||
override suspend fun createApiToken(
|
||||
request: CreateApiTokenRequestV1
|
||||
): CreateApiTokenResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
request: CreateApiTokenRequest
|
||||
): CreateApiTokenResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.createApiToken(request) },
|
||||
logMethod = { "createApiToken" },
|
||||
logParameters = { "request = $request" }
|
||||
)
|
||||
|
||||
override suspend fun requestUserInfo(): GetUserInfoResponseV1 {
|
||||
override suspend fun requestUserInfo(): GetUserInfoResponse {
|
||||
return networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.getUserSelfInfo() },
|
||||
logMethod = { "requestUserInfo" },
|
||||
@@ -146,7 +145,7 @@ class MealieDataSourceV1Impl @Inject constructor(
|
||||
override suspend fun getShoppingLists(
|
||||
page: Int,
|
||||
perPage: Int,
|
||||
): GetShoppingListsResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
): GetShoppingListsResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.getShoppingLists(page, perPage) },
|
||||
logMethod = { "getShoppingLists" },
|
||||
logParameters = { "page = $page, perPage = $perPage" }
|
||||
@@ -154,7 +153,7 @@ class MealieDataSourceV1Impl @Inject constructor(
|
||||
|
||||
override suspend fun getShoppingList(
|
||||
id: String
|
||||
): GetShoppingListResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
): GetShoppingListResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.getShoppingList(id) },
|
||||
logMethod = { "getShoppingList" },
|
||||
logParameters = { "id = $id" }
|
||||
@@ -186,7 +185,7 @@ class MealieDataSourceV1Impl @Inject constructor(
|
||||
)
|
||||
|
||||
override suspend fun updateShoppingListItem(
|
||||
item: ShoppingListItemInfo
|
||||
item: GetShoppingListItemResponse
|
||||
) {
|
||||
// Has to be done in two steps because we can't specify only the changed fields
|
||||
val remoteItem = getShoppingListItem(item.id)
|
||||
@@ -203,14 +202,14 @@ class MealieDataSourceV1Impl @Inject constructor(
|
||||
updateShoppingListItem(item.id, JsonObject(updatedItem))
|
||||
}
|
||||
|
||||
override suspend fun getFoods(): GetFoodsResponseV1 {
|
||||
override suspend fun getFoods(): GetFoodsResponse {
|
||||
return networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.getFoods(perPage = -1) },
|
||||
logMethod = { "getFoods" },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getUnits(): GetUnitsResponseV1 {
|
||||
override suspend fun getUnits(): GetUnitsResponse {
|
||||
return networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.getUnits(perPage = -1) },
|
||||
logMethod = { "getUnits" },
|
||||
@@ -218,7 +217,7 @@ class MealieDataSourceV1Impl @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun addShoppingListItem(
|
||||
request: CreateShoppingListItemRequestV1
|
||||
request: CreateShoppingListItemRequest
|
||||
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.createShoppingListItem(request) },
|
||||
logMethod = { "addShoppingListItem" },
|
||||
@@ -0,0 +1,210 @@
|
||||
package gq.kirmanak.mealient.datasource.impl
|
||||
|
||||
import gq.kirmanak.mealient.datasource.MealieService
|
||||
import gq.kirmanak.mealient.datasource.ServerUrlProvider
|
||||
import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
|
||||
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
|
||||
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
|
||||
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
||||
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipesResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListsResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetTokenResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetUnitsResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetUserInfoResponse
|
||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLRequest
|
||||
import gq.kirmanak.mealient.datasource.models.UpdateRecipeRequest
|
||||
import gq.kirmanak.mealient.datasource.models.VersionResponse
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.delete
|
||||
import io.ktor.client.request.forms.FormDataContent
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.patch
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.put
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.URLBuilder
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.http.parameters
|
||||
import io.ktor.http.path
|
||||
import io.ktor.http.takeFrom
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
internal class MealieServiceKtor @Inject constructor(
|
||||
private val httpClient: HttpClient,
|
||||
private val serverUrlProviderProvider: Provider<ServerUrlProvider>,
|
||||
) : MealieService {
|
||||
|
||||
private val serverUrlProvider: ServerUrlProvider
|
||||
get() = serverUrlProviderProvider.get()
|
||||
|
||||
override suspend fun getToken(username: String, password: String): GetTokenResponse {
|
||||
val formParameters = parameters {
|
||||
append("username", username)
|
||||
append("password", password)
|
||||
}
|
||||
|
||||
return httpClient.post {
|
||||
endpoint("/api/auth/token")
|
||||
setBody(FormDataContent(formParameters))
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun createRecipe(addRecipeRequest: CreateRecipeRequest): String {
|
||||
return httpClient.post {
|
||||
endpoint("/api/recipes")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(addRecipeRequest)
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun updateRecipe(
|
||||
addRecipeRequest: UpdateRecipeRequest,
|
||||
slug: String,
|
||||
): GetRecipeResponse {
|
||||
return httpClient.patch {
|
||||
endpoint("/api/recipes/$slug")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(addRecipeRequest)
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun getVersion(): VersionResponse {
|
||||
return httpClient.get {
|
||||
endpoint("/api/app/about")
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun getRecipeSummary(page: Int, perPage: Int): GetRecipesResponse {
|
||||
return httpClient.get {
|
||||
endpoint("/api/recipes") {
|
||||
parameters.append("page", page.toString())
|
||||
parameters.append("perPage", perPage.toString())
|
||||
}
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun getRecipe(slug: String): GetRecipeResponse {
|
||||
return httpClient.get {
|
||||
endpoint("/api/recipes/$slug")
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun createRecipeFromURL(request: ParseRecipeURLRequest): String {
|
||||
return httpClient.post {
|
||||
endpoint("/api/recipes/create-url")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun createApiToken(request: CreateApiTokenRequest): CreateApiTokenResponse {
|
||||
return httpClient.post {
|
||||
endpoint("/api/users/api-tokens")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun getUserSelfInfo(): GetUserInfoResponse {
|
||||
return httpClient.get {
|
||||
endpoint("/api/users/self")
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String) {
|
||||
httpClient.delete {
|
||||
endpoint("/api/users/$userId/favorites/$recipeSlug")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun addFavoriteRecipe(userId: String, recipeSlug: String) {
|
||||
httpClient.post {
|
||||
endpoint("/api/users/$userId/favorites/$recipeSlug")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteRecipe(slug: String) {
|
||||
httpClient.delete {
|
||||
endpoint("/api/recipes/$slug")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getShoppingLists(page: Int, perPage: Int): GetShoppingListsResponse {
|
||||
return httpClient.get {
|
||||
endpoint("/api/groups/shopping/lists") {
|
||||
parameters.append("page", page.toString())
|
||||
parameters.append("perPage", perPage.toString())
|
||||
}
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun getShoppingList(id: String): GetShoppingListResponse {
|
||||
return httpClient.get {
|
||||
endpoint("/api/groups/shopping/lists/$id")
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun getShoppingListItem(id: String): JsonElement {
|
||||
return httpClient.get {
|
||||
endpoint("/api/groups/shopping/items/$id")
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun updateShoppingListItem(id: String, request: JsonElement) {
|
||||
httpClient.put {
|
||||
endpoint("/api/groups/shopping/items/$id")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteShoppingListItem(id: String) {
|
||||
httpClient.delete {
|
||||
endpoint("/api/groups/shopping/items/$id")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getFoods(perPage: Int): GetFoodsResponse {
|
||||
return httpClient.get {
|
||||
endpoint("/api/foods") {
|
||||
parameters.append("perPage", perPage.toString())
|
||||
}
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun getUnits(perPage: Int): GetUnitsResponse {
|
||||
return httpClient.get {
|
||||
endpoint("/api/units") {
|
||||
parameters.append("perPage", perPage.toString())
|
||||
}
|
||||
}.body()
|
||||
}
|
||||
|
||||
override suspend fun createShoppingListItem(request: CreateShoppingListItemRequest) {
|
||||
httpClient.post {
|
||||
endpoint("/api/groups/shopping/items")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun HttpRequestBuilder.endpoint(
|
||||
path: String,
|
||||
block: URLBuilder.() -> Unit = {}
|
||||
) {
|
||||
val baseUrl = checkNotNull(serverUrlProvider.getUrl()) { "Server URL is not set" }
|
||||
url {
|
||||
takeFrom(baseUrl)
|
||||
path(path)
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import gq.kirmanak.mealient.datasource.NetworkError
|
||||
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
|
||||
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import retrofit2.HttpException
|
||||
import io.ktor.client.plugins.ResponseException
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class NetworkRequestWrapperImpl @Inject constructor(
|
||||
@@ -49,7 +49,8 @@ internal class NetworkRequestWrapperImpl @Inject constructor(
|
||||
logMethod: () -> String,
|
||||
logParameters: (() -> String)?
|
||||
): T = makeCall(block, logMethod, logParameters).getOrElse {
|
||||
throw if (it is HttpException && it.code() in listOf(401, 403)) {
|
||||
val code = (it as? ResponseException)?.response?.status?.value
|
||||
throw if (code in listOf(401, 403)) {
|
||||
NetworkError.Unauthorized(it)
|
||||
} else {
|
||||
it
|
||||
|
||||
@@ -1,55 +1,28 @@
|
||||
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 okhttp3.TlsVersion
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
|
||||
internal class OkHttpBuilderImpl @Inject constructor(
|
||||
private val cacheBuilder: CacheBuilder,
|
||||
private val cacheBuilder: CacheBuilderImpl,
|
||||
// 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 advancedX509TrustManager: AdvancedX509TrustManager,
|
||||
private val sslSocketFactoryFactory: SslSocketFactoryFactory,
|
||||
private val logger: Logger,
|
||||
) : OkHttpBuilder {
|
||||
) {
|
||||
|
||||
override fun buildOkHttp(): OkHttpClient {
|
||||
logger.v { "buildOkHttp() was called with cacheBuilder = $cacheBuilder, interceptors = $interceptors, localInterceptors = $localInterceptors" }
|
||||
fun buildOkHttp(): OkHttpClient {
|
||||
logger.v { "buildOkHttp() was called with cacheBuilder = $cacheBuilder, interceptors = $interceptors" }
|
||||
|
||||
val sslContext = buildSSLContext()
|
||||
sslContext.init(null, arrayOf<TrustManager>(advancedX509TrustManager), null)
|
||||
val sslSocketFactory = sslContext.socketFactory
|
||||
val sslSocketFactory = sslSocketFactoryFactory.create()
|
||||
|
||||
return OkHttpClient.Builder().apply {
|
||||
localInterceptors.forEach(::addInterceptor)
|
||||
interceptors.forEach(::addNetworkInterceptor)
|
||||
sslSocketFactory(sslSocketFactory, advancedX509TrustManager)
|
||||
cache(cacheBuilder.buildCache())
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun buildSSLContext(): SSLContext {
|
||||
return runCatching {
|
||||
SSLContext.getInstance(TlsVersion.TLS_1_3.javaName)
|
||||
}.recoverCatching {
|
||||
logger.w { "TLSv1.3 is not supported in this device; falling through TLSv1.2" }
|
||||
SSLContext.getInstance(TlsVersion.TLS_1_2.javaName)
|
||||
}.recoverCatching {
|
||||
logger.w { "TLSv1.2 is not supported in this device; falling through TLSv1.1" }
|
||||
SSLContext.getInstance(TlsVersion.TLS_1_1.javaName)
|
||||
}.recoverCatching {
|
||||
logger.w { "TLSv1.1 is not supported in this device; falling through TLSv1.0" }
|
||||
// should be available in any device; see reference of supported protocols in
|
||||
// http://developer.android.com/reference/javax/net/ssl/SSLSocket.html
|
||||
SSLContext.getInstance(TlsVersion.TLS_1_0.javaName)
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package gq.kirmanak.mealient.datasource.impl
|
||||
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Converter.Factory
|
||||
import retrofit2.Retrofit
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RetrofitBuilder @Inject constructor(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val converterFactory: Factory,
|
||||
private val logger: Logger,
|
||||
) {
|
||||
|
||||
fun buildRetrofit(baseUrl: String): Retrofit {
|
||||
logger.v { "buildRetrofit() called with: baseUrl = $baseUrl" }
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(converterFactory)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package gq.kirmanak.mealient.datasource.impl
|
||||
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.net.ssl.TrustManager
|
||||
|
||||
internal class SslSocketFactoryFactory @Inject constructor(
|
||||
private val advancedX509TrustManager: AdvancedX509TrustManager,
|
||||
private val logger: Logger,
|
||||
) {
|
||||
|
||||
fun create(): SSLSocketFactory {
|
||||
val sslContext = buildSSLContext()
|
||||
sslContext.init(null, arrayOf<TrustManager>(advancedX509TrustManager), null)
|
||||
return sslContext.socketFactory
|
||||
}
|
||||
|
||||
private fun buildSSLContext(): SSLContext {
|
||||
return runCatching {
|
||||
SSLContext.getInstance("TLSv1.3")
|
||||
}.recoverCatching {
|
||||
logger.w { "TLSv1.3 is not supported in this device; falling through TLSv1.2" }
|
||||
SSLContext.getInstance("TLSv1.2")
|
||||
}.recoverCatching {
|
||||
logger.w { "TLSv1.2 is not supported in this device; falling through TLSv1.1" }
|
||||
SSLContext.getInstance("TLSv1.1")
|
||||
}.recoverCatching {
|
||||
logger.w { "TLSv1.1 is not supported in this device; falling through TLSv1.0" }
|
||||
// should be available in any device; see reference of supported protocols in
|
||||
// http://developer.android.com/reference/javax/net/ssl/SSLSocket.html
|
||||
SSLContext.getInstance("TLSv1")
|
||||
}.getOrThrow()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package gq.kirmanak.mealient.datasource.ktor
|
||||
|
||||
import gq.kirmanak.mealient.datasource.AuthenticationProvider
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.engine.HttpClientEngineConfig
|
||||
import io.ktor.client.plugins.auth.Auth
|
||||
import io.ktor.client.plugins.auth.providers.BearerTokens
|
||||
import io.ktor.client.plugins.auth.providers.bearer
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
internal class AuthKtorConfiguration @Inject constructor(
|
||||
private val authenticationProviderProvider: Provider<AuthenticationProvider>,
|
||||
private val logger: Logger,
|
||||
) : KtorConfiguration {
|
||||
|
||||
private val authenticationProvider: AuthenticationProvider
|
||||
get() = authenticationProviderProvider.get()
|
||||
|
||||
override fun <T : HttpClientEngineConfig> configure(config: HttpClientConfig<T>) {
|
||||
config.install(Auth) {
|
||||
bearer {
|
||||
loadTokens {
|
||||
getTokens()
|
||||
}
|
||||
|
||||
refreshTokens {
|
||||
val newTokens = getTokens()
|
||||
val sameAccessToken = newTokens?.accessToken == oldTokens?.accessToken
|
||||
if (sameAccessToken && response.status == HttpStatusCode.Unauthorized) {
|
||||
authenticationProvider.logout()
|
||||
null
|
||||
} else {
|
||||
newTokens
|
||||
}
|
||||
}
|
||||
|
||||
sendWithoutRequest { true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTokens(): BearerTokens? {
|
||||
val token = authenticationProvider.getAuthToken()
|
||||
logger.v { "getTokens(): token = $token" }
|
||||
return token?.let { BearerTokens(accessToken = it, refreshToken = "") }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package gq.kirmanak.mealient.datasource.ktor
|
||||
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.engine.HttpClientEngineConfig
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ContentNegotiationConfiguration @Inject constructor(
|
||||
private val json: Json,
|
||||
) : KtorConfiguration {
|
||||
|
||||
override fun <T : HttpClientEngineConfig> configure(config: HttpClientConfig<T>) {
|
||||
config.install(ContentNegotiation) {
|
||||
json(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package gq.kirmanak.mealient.datasource.ktor
|
||||
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.engine.HttpClientEngineConfig
|
||||
import io.ktor.client.plugins.compression.ContentEncoding
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class EncodingKtorConfiguration @Inject constructor() : KtorConfiguration {
|
||||
|
||||
override fun <T : HttpClientEngineConfig> configure(config: HttpClientConfig<T>) {
|
||||
config.install(ContentEncoding) {
|
||||
gzip()
|
||||
deflate()
|
||||
identity()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package gq.kirmanak.mealient.datasource.ktor
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
|
||||
internal interface KtorClientBuilder {
|
||||
|
||||
fun buildKtorClient(): HttpClient
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package gq.kirmanak.mealient.datasource.ktor
|
||||
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class KtorClientBuilderImpl @Inject constructor(
|
||||
private val configurators: Set<@JvmSuppressWildcards KtorConfiguration>,
|
||||
private val logger: Logger,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
) : KtorClientBuilder {
|
||||
|
||||
override fun buildKtorClient(): HttpClient {
|
||||
logger.v { "buildKtorClient() called" }
|
||||
|
||||
val client = HttpClient(OkHttp) {
|
||||
expectSuccess = true
|
||||
|
||||
configurators.forEach {
|
||||
it.configure(config = this)
|
||||
}
|
||||
|
||||
engine {
|
||||
preconfigured = okHttpClient
|
||||
}
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package gq.kirmanak.mealient.datasource.ktor
|
||||
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.engine.HttpClientEngineConfig
|
||||
|
||||
internal interface KtorConfiguration {
|
||||
|
||||
fun <T : HttpClientEngineConfig> configure(config: HttpClientConfig<T>)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package gq.kirmanak.mealient.datasource.ktor
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import gq.kirmanak.mealient.datasource.SignOutHandler
|
||||
import io.ktor.client.HttpClient
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface KtorModule {
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideClient(builder: KtorClientBuilder): HttpClient = builder.buildKtorClient()
|
||||
}
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
fun bindAuthKtorConfiguration(impl: AuthKtorConfiguration) : KtorConfiguration
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
fun bindEncodingKtorConfiguration(impl: EncodingKtorConfiguration) : KtorConfiguration
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
fun bindContentNegotiationConfiguration(impl: ContentNegotiationConfiguration) : KtorConfiguration
|
||||
|
||||
@Binds
|
||||
fun bindKtorClientBuilder(impl: KtorClientBuilderImpl) : KtorClientBuilder
|
||||
|
||||
@Binds
|
||||
fun bindSignOutHandler(impl: SignOutHandlerKtor) : SignOutHandler
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package gq.kirmanak.mealient.datasource.ktor
|
||||
|
||||
import gq.kirmanak.mealient.datasource.SignOutHandler
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.auth.Auth
|
||||
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
|
||||
import io.ktor.client.plugins.plugin
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SignOutHandlerKtor @Inject constructor(
|
||||
private val httpClient: HttpClient,
|
||||
) : SignOutHandler {
|
||||
|
||||
override fun signOut() {
|
||||
httpClient.plugin(Auth)
|
||||
.providers
|
||||
.filterIsInstance<BearerAuthProvider>()
|
||||
.forEach { it.clearToken() }
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CreateRecipeRequestV1(
|
||||
data class CreateApiTokenRequest(
|
||||
@SerialName("name") val name: String,
|
||||
)
|
||||
@@ -1,9 +1,9 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CreateApiTokenResponseV1(
|
||||
data class CreateApiTokenResponse(
|
||||
@SerialName("token") val token: String,
|
||||
)
|
||||
@@ -1,9 +1,9 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CreateApiTokenRequestV1(
|
||||
data class CreateRecipeRequest(
|
||||
@SerialName("name") val name: String,
|
||||
)
|
||||
@@ -1,10 +1,10 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CreateShoppingListItemRequestV1(
|
||||
data class CreateShoppingListItemRequest(
|
||||
@SerialName("shopping_list_id") val shoppingListId: String,
|
||||
@SerialName("checked") val checked: Boolean,
|
||||
@SerialName("position") val position: Int?,
|
||||
@@ -1,9 +1,9 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ErrorDetailV1(
|
||||
data class ErrorDetail(
|
||||
@SerialName("detail") val detail: String? = null,
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
data class FoodInfo(
|
||||
val name: String,
|
||||
val id: String
|
||||
)
|
||||
@@ -1,26 +0,0 @@
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
data class FullRecipeInfo(
|
||||
val remoteId: String,
|
||||
val name: String,
|
||||
val recipeYield: String,
|
||||
val recipeIngredients: List<RecipeIngredientInfo>,
|
||||
val recipeInstructions: List<RecipeInstructionInfo>,
|
||||
val settings: RecipeSettingsInfo,
|
||||
)
|
||||
|
||||
data class RecipeSettingsInfo(
|
||||
val disableAmounts: Boolean,
|
||||
)
|
||||
|
||||
data class RecipeIngredientInfo(
|
||||
val note: String,
|
||||
val quantity: Double?,
|
||||
val unit: String?,
|
||||
val food: String?,
|
||||
val title: String?,
|
||||
)
|
||||
|
||||
data class RecipeInstructionInfo(
|
||||
val text: String,
|
||||
)
|
||||
@@ -1,28 +0,0 @@
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
data class FullShoppingListInfo(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val items: List<ShoppingListItemInfo>,
|
||||
)
|
||||
|
||||
data class ShoppingListItemInfo(
|
||||
val shoppingListId: String,
|
||||
val id: String,
|
||||
val checked: Boolean,
|
||||
val position: Int,
|
||||
val isFood: Boolean,
|
||||
val note: String,
|
||||
val quantity: Double,
|
||||
val unit: UnitInfo?,
|
||||
val food: FoodInfo?,
|
||||
val recipeReferences: List<ShoppingListItemRecipeReferenceInfo>,
|
||||
)
|
||||
|
||||
data class ShoppingListItemRecipeReferenceInfo(
|
||||
val recipeId: String,
|
||||
val recipeQuantity: Double,
|
||||
val id: String,
|
||||
val shoppingListId: String,
|
||||
val recipe: FullRecipeInfo,
|
||||
)
|
||||
@@ -1,15 +1,15 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetFoodsResponseV1(
|
||||
@SerialName("items") val items: List<GetFoodResponseV1>,
|
||||
data class GetFoodsResponse(
|
||||
@SerialName("items") val items: List<GetFoodResponse>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GetFoodResponseV1(
|
||||
data class GetFoodResponse(
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("id") val id: String,
|
||||
)
|
||||
@@ -1,33 +1,33 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetRecipeResponseV1(
|
||||
data class GetRecipeResponse(
|
||||
@SerialName("id") val remoteId: String,
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("recipeYield") val recipeYield: String = "",
|
||||
@SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponseV1> = emptyList(),
|
||||
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponseV1> = emptyList(),
|
||||
@SerialName("settings") val settings: GetRecipeSettingsResponseV1? = null,
|
||||
@SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponse> = emptyList(),
|
||||
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponse> = emptyList(),
|
||||
@SerialName("settings") val settings: GetRecipeSettingsResponse? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GetRecipeSettingsResponseV1(
|
||||
data class GetRecipeSettingsResponse(
|
||||
@SerialName("disableAmount") val disableAmount: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GetRecipeIngredientResponseV1(
|
||||
data class GetRecipeIngredientResponse(
|
||||
@SerialName("note") val note: String = "",
|
||||
@SerialName("unit") val unit: GetRecipeIngredientUnitResponseV1?,
|
||||
@SerialName("food") val food: GetRecipeIngredientFoodResponseV1?,
|
||||
@SerialName("unit") val unit: GetUnitResponse?,
|
||||
@SerialName("food") val food: GetFoodResponse?,
|
||||
@SerialName("quantity") val quantity: Double?,
|
||||
@SerialName("title") val title: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GetRecipeInstructionResponseV1(
|
||||
data class GetRecipeInstructionResponse(
|
||||
@SerialName("text") val text: String,
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
@@ -6,7 +6,7 @@ import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetRecipeSummaryResponseV1(
|
||||
data class GetRecipeSummaryResponse(
|
||||
@SerialName("id") val remoteId: String,
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("slug") val slug: String,
|
||||
@@ -1,9 +1,9 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetRecipesResponseV1(
|
||||
@SerialName("items") val items: List<GetRecipeSummaryResponseV1>,
|
||||
data class GetRecipesResponse(
|
||||
@SerialName("items") val items: List<GetRecipeSummaryResponse>,
|
||||
)
|
||||
@@ -1,19 +1,19 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetShoppingListResponseV1(
|
||||
data class GetShoppingListResponse(
|
||||
@SerialName("id") val id: String,
|
||||
@SerialName("groupId") val groupId: String,
|
||||
@SerialName("name") val name: String = "",
|
||||
@SerialName("listItems") val listItems: List<GetShoppingListItemResponseV1> = emptyList(),
|
||||
@SerialName("recipeReferences") val recipeReferences: List<GetShoppingListItemRecipeReferenceFullResponseV1>,
|
||||
@SerialName("listItems") val listItems: List<GetShoppingListItemResponse> = emptyList(),
|
||||
@SerialName("recipeReferences") val recipeReferences: List<GetShoppingListItemRecipeReferenceFullResponse>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GetShoppingListItemResponseV1(
|
||||
data class GetShoppingListItemResponse(
|
||||
@SerialName("shoppingListId") val shoppingListId: String,
|
||||
@SerialName("id") val id: String,
|
||||
@SerialName("checked") val checked: Boolean = false,
|
||||
@@ -21,22 +21,22 @@ data class GetShoppingListItemResponseV1(
|
||||
@SerialName("isFood") val isFood: Boolean = false,
|
||||
@SerialName("note") val note: String = "",
|
||||
@SerialName("quantity") val quantity: Double = 0.0,
|
||||
@SerialName("unit") val unit: GetRecipeIngredientUnitResponseV1? = null,
|
||||
@SerialName("food") val food: GetRecipeIngredientFoodResponseV1? = null,
|
||||
@SerialName("recipeReferences") val recipeReferences: List<GetShoppingListItemRecipeReferenceResponseV1> = emptyList(),
|
||||
@SerialName("unit") val unit: GetUnitResponse? = null,
|
||||
@SerialName("food") val food: GetFoodResponse? = null,
|
||||
@SerialName("recipeReferences") val recipeReferences: List<GetShoppingListItemRecipeReferenceResponse> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GetShoppingListItemRecipeReferenceResponseV1(
|
||||
data class GetShoppingListItemRecipeReferenceResponse(
|
||||
@SerialName("recipeId") val recipeId: String,
|
||||
@SerialName("recipeQuantity") val recipeQuantity: Double = 0.0
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GetShoppingListItemRecipeReferenceFullResponseV1(
|
||||
data class GetShoppingListItemRecipeReferenceFullResponse(
|
||||
@SerialName("id") val id: String,
|
||||
@SerialName("shoppingListId") val shoppingListId: String,
|
||||
@SerialName("recipeId") val recipeId: String,
|
||||
@SerialName("recipeQuantity") val recipeQuantity: Double = 0.0,
|
||||
@SerialName("recipe") val recipe: GetRecipeResponseV1,
|
||||
@SerialName("recipe") val recipe: GetRecipeResponse,
|
||||
)
|
||||
@@ -1,13 +1,13 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetShoppingListsResponseV1(
|
||||
data class GetShoppingListsResponse(
|
||||
@SerialName("page") val page: Int,
|
||||
@SerialName("per_page") val perPage: Int,
|
||||
@SerialName("total") val total: Int,
|
||||
@SerialName("total_pages") val totalPages: Int,
|
||||
@SerialName("items") val items: List<GetShoppingListsSummaryResponseV1>,
|
||||
@SerialName("items") val items: List<GetShoppingListsSummaryResponse>,
|
||||
)
|
||||
@@ -1,10 +1,10 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetShoppingListsSummaryResponseV1(
|
||||
data class GetShoppingListsSummaryResponse(
|
||||
@SerialName("id") val id: String,
|
||||
@SerialName("name") val name: String?,
|
||||
)
|
||||
@@ -1,9 +1,9 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetTokenResponseV1(
|
||||
data class GetTokenResponse(
|
||||
@SerialName("access_token") val accessToken: String,
|
||||
)
|
||||
@@ -1,15 +1,15 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetUnitsResponseV1(
|
||||
@SerialName("items") val items: List<GetUnitResponseV1>
|
||||
data class GetUnitsResponse(
|
||||
@SerialName("items") val items: List<GetUnitResponse>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GetUnitResponseV1(
|
||||
data class GetUnitResponse(
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("id") val id: String
|
||||
)
|
||||
@@ -1,10 +1,10 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetUserInfoResponseV1(
|
||||
data class GetUserInfoResponse(
|
||||
@SerialName("id") val id: String,
|
||||
@SerialName("favoriteRecipes") val favoriteRecipes: List<String> = emptyList(),
|
||||
)
|
||||
@@ -1,11 +0,0 @@
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
data class NewShoppingListItemInfo(
|
||||
val shoppingListId: String,
|
||||
val isFood: Boolean,
|
||||
val note: String,
|
||||
val quantity: Double,
|
||||
val unit: UnitInfo?,
|
||||
val food: FoodInfo?,
|
||||
val position: Int,
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
data class ParseRecipeURLInfo(
|
||||
val url: String,
|
||||
val includeTags: Boolean
|
||||
)
|
||||
@@ -1,10 +1,10 @@
|
||||
package gq.kirmanak.mealient.datasource.v1.models
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ParseRecipeURLRequestV1(
|
||||
data class ParseRecipeURLRequest(
|
||||
@SerialName("url") val url: String,
|
||||
@SerialName("includeTags") val includeTags: Boolean
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
|
||||
data class RecipeSummaryInfo(
|
||||
val remoteId: String,
|
||||
val name: String,
|
||||
val slug: String,
|
||||
val description: String = "",
|
||||
val imageId: String,
|
||||
val dateAdded: LocalDate,
|
||||
val dateUpdated: LocalDateTime
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user