Add Kotlinx Kover test coverage calculator (#199)
* Add Kotlin Kover * Add AuthKtorConfiguration tests * Ensure at least 25% code coverage * Exclude Previews from code coverage * Specify Kover report path for SonarQube * Add Kover xml report task * Extract sonar to a separate step * Add some exclusions and minimum coverage * Exclude Hilt-generated classes * Add shopping list view model tests * Reduce the coverage requirement
This commit is contained in:
7
.github/workflows/check.yml
vendored
7
.github/workflows/check.yml
vendored
@@ -23,10 +23,13 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a
|
||||
|
||||
- name: Run tests
|
||||
- name: Checks
|
||||
run: ./gradlew check :app:koverXmlReportRelease :app:koverVerifyRelease
|
||||
|
||||
- name: SonarCloud
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
run: ./gradlew check sonar --no-configuration-cache --no-daemon
|
||||
run: ./gradlew sonar
|
||||
|
||||
- name: Publish test reports
|
||||
uses: mikepenz/action-junit-report@0a8a5ba57593d67b2e45de2c543b438412382b7b
|
||||
|
||||
@@ -84,71 +84,63 @@ ksp {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(project(":architecture"))
|
||||
implementation(project(":database"))
|
||||
testImplementation(project(":database_test"))
|
||||
implementation(project(":datastore"))
|
||||
testImplementation(project(":datastore_test"))
|
||||
implementation(project(":datasource"))
|
||||
testImplementation(project(":datasource_test"))
|
||||
implementation(project(":logging"))
|
||||
implementation(project(":ui"))
|
||||
implementation(project(":features:shopping_lists"))
|
||||
implementation(project(":model_mapper"))
|
||||
testImplementation(project(":testing"))
|
||||
|
||||
implementation(libs.android.material.material)
|
||||
|
||||
implementation(libs.androidx.coreKtx)
|
||||
implementation(libs.androidx.splashScreen)
|
||||
|
||||
implementation(libs.androidx.appcompat)
|
||||
|
||||
implementation(libs.androidx.lifecycle.viewmodelKtx)
|
||||
|
||||
implementation(libs.androidx.shareTarget)
|
||||
|
||||
implementation(libs.androidx.compose.materialIconsExtended)
|
||||
|
||||
implementation(libs.google.dagger.hiltAndroid)
|
||||
kapt(libs.google.dagger.hiltCompiler)
|
||||
kaptTest(libs.google.dagger.hiltAndroidCompiler)
|
||||
testImplementation(libs.google.dagger.hiltAndroidTesting)
|
||||
kaptAndroidTest(libs.google.dagger.hiltAndroidCompiler)
|
||||
androidTestImplementation(libs.google.dagger.hiltAndroidTesting)
|
||||
|
||||
implementation(libs.androidx.paging.runtimeKtx)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
testImplementation(libs.androidx.paging.commonKtx)
|
||||
|
||||
implementation(libs.jetbrains.kotlinx.datetime)
|
||||
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
|
||||
implementation(libs.coil)
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
implementation(libs.androidx.compose.animation)
|
||||
|
||||
implementation(libs.androidx.hilt.navigationCompose)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
|
||||
implementation(libs.jetbrains.kotlinx.coroutinesAndroid)
|
||||
testImplementation(libs.jetbrains.kotlinx.coroutinesTest)
|
||||
|
||||
testImplementation(libs.robolectric)
|
||||
|
||||
testImplementation(libs.androidx.test.junit)
|
||||
testImplementation(libs.androidx.coreTesting)
|
||||
|
||||
testImplementation(libs.google.truth)
|
||||
|
||||
testImplementation(libs.io.mockk)
|
||||
|
||||
debugImplementation(libs.squareup.leakcanary)
|
||||
|
||||
kover(project(":model_mapper"))
|
||||
kover(project(":features:shopping_lists"))
|
||||
kover(project(":ui"))
|
||||
kover(project(":logging"))
|
||||
kover(project(":architecture"))
|
||||
kover(project(":database"))
|
||||
kover(project(":datastore"))
|
||||
kover(project(":datasource"))
|
||||
|
||||
kapt(libs.google.dagger.hiltCompiler)
|
||||
|
||||
kaptTest(libs.google.dagger.hiltAndroidCompiler)
|
||||
|
||||
kaptAndroidTest(libs.google.dagger.hiltAndroidCompiler)
|
||||
|
||||
testImplementation(project(":datasource_test"))
|
||||
testImplementation(project(":database_test"))
|
||||
testImplementation(project(":datastore_test"))
|
||||
testImplementation(project(":testing"))
|
||||
testImplementation(libs.androidx.paging.commonKtx)
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.jetbrains.kotlinx.coroutinesTest)
|
||||
testImplementation(libs.robolectric)
|
||||
testImplementation(libs.androidx.test.junit)
|
||||
testImplementation(libs.androidx.coreTesting)
|
||||
testImplementation(libs.google.truth)
|
||||
testImplementation(libs.io.mockk)
|
||||
testImplementation(libs.google.dagger.hiltAndroidTesting)
|
||||
|
||||
androidTestImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.test.junit)
|
||||
androidTestImplementation(libs.kaspersky.kaspresso)
|
||||
@@ -157,5 +149,41 @@ dependencies {
|
||||
androidTestImplementation(libs.androidx.test.core)
|
||||
androidTestImplementation(libs.androidx.test.rules)
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
androidTestImplementation(libs.google.dagger.hiltAndroidTesting)
|
||||
|
||||
androidTestUtil(libs.androidx.test.orchestrator)
|
||||
}
|
||||
|
||||
koverReport {
|
||||
filters {
|
||||
excludes {
|
||||
classes(
|
||||
"gq.kirmanak.mealient.datastore.recipe.AddRecipeInput*", // generated by data store
|
||||
"*ComposableSingletons*", // generated by Compose
|
||||
"gq.kirmanak.mealient.database.AppDb_Impl*", // generated by Room
|
||||
"*Dao_Impl*", // generated by Room
|
||||
"*Hilt_*", // generated by Hilt
|
||||
)
|
||||
packages(
|
||||
"gq.kirmanak.mealient*.destinations", // generated by Compose destinations
|
||||
)
|
||||
annotatedBy(
|
||||
"androidx.compose.ui.tooling.preview.Preview",
|
||||
"gq.kirmanak.mealient.ui.preview.ColorSchemePreview",
|
||||
"androidx.compose.runtime.Composable",
|
||||
"dagger.Module",
|
||||
"dagger.internal.DaggerGenerated",
|
||||
)
|
||||
}
|
||||
includes {
|
||||
packages("gq.kirmanak.mealient")
|
||||
}
|
||||
}
|
||||
androidReports("release") {
|
||||
verify {
|
||||
rule {
|
||||
minBound(30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
|
||||
with(pluginManager) {
|
||||
apply("com.android.application")
|
||||
apply("org.jetbrains.kotlin.android")
|
||||
apply("org.jetbrains.kotlinx.kover")
|
||||
}
|
||||
|
||||
extensions.configure<BaseAppModuleExtension> {
|
||||
|
||||
@@ -11,6 +11,7 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
|
||||
with(pluginManager) {
|
||||
apply("com.android.library")
|
||||
apply("org.jetbrains.kotlin.android")
|
||||
apply("org.jetbrains.kotlinx.kover")
|
||||
}
|
||||
|
||||
extensions.configure<LibraryExtension> {
|
||||
|
||||
@@ -16,6 +16,7 @@ buildscript {
|
||||
plugins {
|
||||
alias(libs.plugins.sonarqube)
|
||||
alias(libs.plugins.ksp) apply false
|
||||
alias(libs.plugins.kover) apply false
|
||||
}
|
||||
|
||||
sonarqube {
|
||||
@@ -23,6 +24,10 @@ sonarqube {
|
||||
property("sonar.projectKey", "kirmanak_Mealient")
|
||||
property("sonar.organization", "kirmanak")
|
||||
property("sonar.host.url", "https://sonarcloud.io")
|
||||
property(
|
||||
"sonar.coverage.jacoco.xmlReportPaths",
|
||||
"${projectDir.path}/app/build/reports/kover/reportRelease.xml"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +38,7 @@ subprojects {
|
||||
"sonar.androidLint.reportPaths",
|
||||
"${projectDir.path}/build/reports/lint-results-debug.xml"
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package gq.kirmanak.mealient.datasource.ktor
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
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.RefreshTokensParams
|
||||
import io.ktor.client.plugins.auth.providers.bearer
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import javax.inject.Inject
|
||||
@@ -27,14 +29,7 @@ internal class AuthKtorConfiguration @Inject constructor(
|
||||
}
|
||||
|
||||
refreshTokens {
|
||||
val newTokens = getTokens()
|
||||
val sameAccessToken = newTokens?.accessToken == oldTokens?.accessToken
|
||||
if (sameAccessToken && response.status == HttpStatusCode.Unauthorized) {
|
||||
authenticationProvider.logout()
|
||||
null
|
||||
} else {
|
||||
newTokens
|
||||
}
|
||||
refreshTokens()
|
||||
}
|
||||
|
||||
sendWithoutRequest { true }
|
||||
@@ -42,7 +37,20 @@ internal class AuthKtorConfiguration @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTokens(): BearerTokens? {
|
||||
@VisibleForTesting
|
||||
suspend fun RefreshTokensParams.refreshTokens(): BearerTokens? {
|
||||
val newTokens = getTokens()
|
||||
val sameAccessToken = newTokens?.accessToken == oldTokens?.accessToken
|
||||
return if (sameAccessToken && response.status == HttpStatusCode.Unauthorized) {
|
||||
authenticationProvider.logout()
|
||||
null
|
||||
} else {
|
||||
newTokens
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
suspend fun getTokens(): BearerTokens? {
|
||||
val token = authenticationProvider.getAuthToken()
|
||||
logger.v { "getTokens(): token = $token" }
|
||||
return token?.let { BearerTokens(accessToken = it, refreshToken = "") }
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package gq.kirmanak.mealient.datasource
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import gq.kirmanak.mealient.datasource.ktor.AuthKtorConfiguration
|
||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
import io.ktor.client.plugins.auth.providers.BearerTokens
|
||||
import io.ktor.client.plugins.auth.providers.RefreshTokensParams
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
private const val AUTH_TOKEN = "token"
|
||||
|
||||
internal class AuthKtorConfigurationTest : BaseUnitTest() {
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
lateinit var authenticationProvider: AuthenticationProvider
|
||||
|
||||
private lateinit var subject: AuthKtorConfiguration
|
||||
|
||||
@Before
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
coEvery { authenticationProvider.getAuthToken() } returns AUTH_TOKEN
|
||||
subject = AuthKtorConfiguration(FakeProvider(authenticationProvider), logger)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getTokens returns BearerTokens with auth token`() = runTest {
|
||||
val bearerTokens = subject.getTokens()
|
||||
assertThat(bearerTokens?.accessToken).isEqualTo(AUTH_TOKEN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getTokens returns BearerTokens without refresh token`() = runTest {
|
||||
val bearerTokens = subject.getTokens()
|
||||
assertThat(bearerTokens?.refreshToken).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokens returns new auth token if it doesn't match old`() = runTest {
|
||||
val refreshTokensParams = mockRefreshTokenParams(HttpStatusCode.Unauthorized, "old token")
|
||||
val actual = with(subject) { refreshTokensParams.refreshTokens() }
|
||||
assertThat(actual?.accessToken).isEqualTo(AUTH_TOKEN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokens returns empty refresh token if auth token doesn't match old`() = runTest {
|
||||
val refreshTokensParams = mockRefreshTokenParams(HttpStatusCode.Unauthorized, "old token")
|
||||
val actual = with(subject) { refreshTokensParams.refreshTokens() }
|
||||
assertThat(actual?.refreshToken).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokens returns null if auth token matches old`() = runTest {
|
||||
val refreshTokensParams = mockRefreshTokenParams(HttpStatusCode.Unauthorized, AUTH_TOKEN)
|
||||
val actual = with(subject) { refreshTokensParams.refreshTokens() }
|
||||
assertThat(actual).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokens calls logout if auth token matches old`() = runTest {
|
||||
val refreshTokensParams = mockRefreshTokenParams(HttpStatusCode.Unauthorized, AUTH_TOKEN)
|
||||
with(subject) { refreshTokensParams.refreshTokens() }
|
||||
coVerify { authenticationProvider.logout() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokens does not logout if status code is not found`() = runTest {
|
||||
val refreshTokensParams = mockRefreshTokenParams(HttpStatusCode.NotFound, AUTH_TOKEN)
|
||||
with(subject) { refreshTokensParams.refreshTokens() }
|
||||
coVerify(inverse = true) { authenticationProvider.logout() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokens returns same access token if status code is not found`() = runTest {
|
||||
val refreshTokensParams = mockRefreshTokenParams(HttpStatusCode.NotFound, AUTH_TOKEN)
|
||||
val actual = with(subject) { refreshTokensParams.refreshTokens() }
|
||||
assertThat(actual?.accessToken).isEqualTo(AUTH_TOKEN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokens returns empty refresh token if status code is not found`() = runTest {
|
||||
val refreshTokensParams = mockRefreshTokenParams(HttpStatusCode.NotFound, AUTH_TOKEN)
|
||||
val actual = with(subject) { refreshTokensParams.refreshTokens() }
|
||||
assertThat(actual?.refreshToken).isEmpty()
|
||||
}
|
||||
|
||||
private fun mockRefreshTokenParams(
|
||||
responseStatusCode: HttpStatusCode,
|
||||
oldAccessToken: String,
|
||||
): RefreshTokensParams {
|
||||
val notFoundResponse = mockk<HttpResponse> {
|
||||
every { status } returns responseStatusCode
|
||||
}
|
||||
val refreshTokensParams = mockk<RefreshTokensParams> {
|
||||
every { response } returns notFoundResponse
|
||||
every { oldTokens } returns BearerTokens(oldAccessToken, "")
|
||||
}
|
||||
return refreshTokensParams
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package gq.kirmanak.mealient.datasource
|
||||
|
||||
import javax.inject.Provider
|
||||
|
||||
data class FakeProvider<T>(
|
||||
val value: T,
|
||||
) : Provider<T> {
|
||||
|
||||
override fun get(): T = value
|
||||
}
|
||||
@@ -23,25 +23,22 @@ dependencies {
|
||||
implementation(project(":database"))
|
||||
implementation(project(":ui"))
|
||||
implementation(project(":model_mapper"))
|
||||
|
||||
implementation(libs.android.material.material)
|
||||
implementation(libs.androidx.compose.material)
|
||||
implementation(libs.androidx.compose.materialIconsExtended)
|
||||
|
||||
implementation(libs.google.dagger.hiltAndroid)
|
||||
kapt(libs.google.dagger.hiltCompiler)
|
||||
kaptTest(libs.google.dagger.hiltAndroidCompiler)
|
||||
testImplementation(libs.google.dagger.hiltAndroidTesting)
|
||||
|
||||
implementation(libs.androidx.hilt.navigationCompose)
|
||||
|
||||
implementation(libs.jetbrains.kotlinx.coroutinesAndroid)
|
||||
|
||||
kapt(libs.google.dagger.hiltCompiler)
|
||||
|
||||
kaptTest(libs.google.dagger.hiltAndroidCompiler)
|
||||
|
||||
testImplementation(project(":testing"))
|
||||
testImplementation(libs.google.dagger.hiltAndroidTesting)
|
||||
testImplementation(libs.jetbrains.kotlinx.coroutinesTest)
|
||||
|
||||
testImplementation(libs.androidx.test.junit)
|
||||
|
||||
testImplementation(libs.google.truth)
|
||||
|
||||
testImplementation(libs.io.mockk)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package gq.kirmanak.mealient.shopping_lists.ui.details
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemRecipeReferenceResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetUnitResponse
|
||||
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsAuthRepo
|
||||
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepo
|
||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||
import gq.kirmanak.mealient.ui.util.LoadingHelper
|
||||
import gq.kirmanak.mealient.ui.util.LoadingHelperFactory
|
||||
import gq.kirmanak.mealient.ui.util.LoadingState
|
||||
import gq.kirmanak.mealient.ui.util.LoadingStateNoData
|
||||
import gq.kirmanak.mealient.ui.util.LoadingStateWithData
|
||||
import io.mockk.CapturingSlot
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.slot
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
|
||||
internal class ShoppingListViewModelTest : BaseUnitTest() {
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
lateinit var shoppingListsRepo: ShoppingListsRepo
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
lateinit var authRepo: ShoppingListsAuthRepo
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
lateinit var loadingHelperFactory: LoadingHelperFactory
|
||||
|
||||
@MockK(relaxUnitFun = true)
|
||||
lateinit var loadingHelper: LoadingHelper<ShoppingListData>
|
||||
|
||||
lateinit var subject: ShoppingListViewModel
|
||||
|
||||
private val loadingState = MutableStateFlow<LoadingState<ShoppingListData>>(
|
||||
LoadingStateNoData.InitialLoad
|
||||
)
|
||||
|
||||
private val isAuthorized = MutableStateFlow(false)
|
||||
|
||||
@Test
|
||||
fun `when view model is created then the list is refreshed`() {
|
||||
createViewModel()
|
||||
coVerify { loadingHelper.refresh() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when user authenticates then the list is refreshed`() {
|
||||
createViewModel()
|
||||
isAuthorized.value = true
|
||||
coVerify {
|
||||
loadingHelper.refresh() // On create
|
||||
loadingHelper.refresh() // On authentication
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when refresh fails then snackbar is shown`() {
|
||||
val error = IOException()
|
||||
createViewModel(
|
||||
refreshResult = Result.failure(error)
|
||||
)
|
||||
assertSame(error, subject.errorToShowInSnackbar)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `when refresh succeeds then no snackbar shown`() {
|
||||
createViewModel()
|
||||
assertNull(subject.errorToShowInSnackbar)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when loading starts then state is initial load`() {
|
||||
createViewModel()
|
||||
assertEquals(LoadingStateNoData.InitialLoad, subject.loadingState.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when loading succeeds then data is shown`() {
|
||||
createViewModel()
|
||||
loadingState.value = LoadingStateWithData.Success(shoppingListData)
|
||||
assertEquals(LoadingStateWithData.Success(shoppingListScreen), subject.loadingState.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when load data is requested then repo is queried`() = runTest {
|
||||
val lambdaSlot = slot<suspend () -> Result<ShoppingListData>>()
|
||||
createViewModel(
|
||||
lambdaSlot = lambdaSlot
|
||||
)
|
||||
val lambda = lambdaSlot.captured
|
||||
val actualResult = lambda()
|
||||
assertEquals(Result.success(shoppingListData), actualResult)
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
shoppingListId: String = "shoppingListId",
|
||||
refreshResult: Result<ShoppingListData> = Result.success(shoppingListData),
|
||||
lambdaSlot: CapturingSlot<suspend () -> Result<ShoppingListData>> = slot<suspend () -> Result<ShoppingListData>>(),
|
||||
) {
|
||||
val savedStateHandle = SavedStateHandle().also {
|
||||
it["shoppingListId"] = shoppingListId
|
||||
}
|
||||
every { loadingHelperFactory.create(any(), capture(lambdaSlot)) } returns loadingHelper
|
||||
every { loadingHelper.loadingState } returns loadingState
|
||||
coEvery { loadingHelper.refresh() } returns refreshResult
|
||||
every { authRepo.isAuthorizedFlow } returns isAuthorized
|
||||
coEvery { shoppingListsRepo.getFoods() } returns listOf(milkFood)
|
||||
coEvery { shoppingListsRepo.getUnits() } returns listOf(mlUnit)
|
||||
coEvery { shoppingListsRepo.getShoppingList(any()) } returns shoppingListResponse
|
||||
subject = ShoppingListViewModel(
|
||||
shoppingListsRepo = shoppingListsRepo,
|
||||
logger = logger,
|
||||
authRepo = authRepo,
|
||||
loadingHelperFactory = loadingHelperFactory,
|
||||
savedStateHandle = savedStateHandle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val mlUnit = GetUnitResponse("ml", "")
|
||||
|
||||
private val milkFood = GetFoodResponse("Milk", "")
|
||||
|
||||
private val blackTeaBags = GetShoppingListItemResponse(
|
||||
id = "1",
|
||||
shoppingListId = "1",
|
||||
checked = false,
|
||||
position = 0,
|
||||
isFood = false,
|
||||
note = "Black tea bags",
|
||||
quantity = 30.0,
|
||||
unit = null,
|
||||
food = null,
|
||||
recipeReferences = listOf(
|
||||
GetShoppingListItemRecipeReferenceResponse(
|
||||
recipeId = "1",
|
||||
recipeQuantity = 1.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val milk = GetShoppingListItemResponse(
|
||||
id = "2",
|
||||
shoppingListId = "1",
|
||||
checked = true,
|
||||
position = 0,
|
||||
isFood = true,
|
||||
note = "Cold",
|
||||
quantity = 500.0,
|
||||
unit = mlUnit,
|
||||
food = milkFood,
|
||||
recipeReferences = listOf(
|
||||
GetShoppingListItemRecipeReferenceResponse(
|
||||
recipeId = "1",
|
||||
recipeQuantity = 500.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val shoppingListResponse = GetShoppingListResponse(
|
||||
id = "shoppingListId",
|
||||
groupId = "shoppingListGroupId",
|
||||
name = "shoppingListName",
|
||||
listItems = listOf(blackTeaBags, milk),
|
||||
recipeReferences = listOf()
|
||||
)
|
||||
|
||||
private val shoppingListData = ShoppingListData(
|
||||
foods = listOf(milkFood),
|
||||
units = listOf(mlUnit),
|
||||
shoppingList = shoppingListResponse
|
||||
)
|
||||
|
||||
private val shoppingListScreen = ShoppingListScreenState(
|
||||
name = "shoppingListName",
|
||||
listId = "shoppingListId",
|
||||
items = listOf(
|
||||
ShoppingListItemState.ExistingItem(
|
||||
item = blackTeaBags,
|
||||
isEditing = false
|
||||
),
|
||||
ShoppingListItemState.ExistingItem(
|
||||
item = milk,
|
||||
isEditing = false
|
||||
)
|
||||
),
|
||||
foods = listOf(milkFood),
|
||||
units = listOf(mlUnit)
|
||||
)
|
||||
@@ -83,6 +83,8 @@ androidxHilt = "1.1.0"
|
||||
ktor = "2.3.7"
|
||||
# https://github.com/coil-kt/coil/releases
|
||||
coil = "2.5.0"
|
||||
# https://github.com/Kotlin/kotlinx-kover/releases
|
||||
kover = "0.7.5"
|
||||
|
||||
[libraries]
|
||||
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
|
||||
@@ -191,3 +193,4 @@ sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
|
||||
appsweep = { id = "com.guardsquare.appsweep", version.ref = "appsweep" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "kspPlugin" }
|
||||
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
|
||||
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
|
||||
|
||||
@@ -23,6 +23,8 @@ dependencyResolutionManagement {
|
||||
|
||||
rootProject.name = "Mealient"
|
||||
|
||||
System.setProperty("sonar.gradle.skipCompile", "true")
|
||||
|
||||
include(":app")
|
||||
include(":architecture")
|
||||
include(":database")
|
||||
|
||||
Reference in New Issue
Block a user