Implement proper loading of recipe summaries

This commit is contained in:
Kirill Kamakin
2021-11-16 22:24:27 +03:00
parent b9f31ebbc7
commit c2129c763e
20 changed files with 505 additions and 111 deletions

View File

@@ -0,0 +1,111 @@
package gq.kirmanak.mealie.data.recipes
import gq.kirmanak.mealie.data.recipes.db.RecipeEntity
import gq.kirmanak.mealie.data.recipes.network.GetRecipeSummaryResponse
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
object RecipeImplTestData {
val RECIPE_SUMMARY_CAKE = GetRecipeSummaryResponse(
remoteId = 1,
name = "Cake",
slug = "cake",
image = "86",
description = "A tasty cake",
recipeCategories = listOf("dessert", "tasty"),
tags = listOf("gluten", "allergic"),
rating = 4,
dateAdded = LocalDate.parse("2021-11-13"),
dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13"),
)
val RECIPE_SUMMARY_PORRIDGE = GetRecipeSummaryResponse(
remoteId = 2,
name = "Porridge",
slug = "porridge",
image = "89",
description = "A tasty porridge",
recipeCategories = listOf("porridge", "tasty"),
tags = listOf("gluten", "milk"),
rating = 5,
dateAdded = LocalDate.parse("2021-11-12"),
dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"),
)
val TEST_RECIPE_SUMMARIES = listOf(RECIPE_SUMMARY_CAKE, RECIPE_SUMMARY_PORRIDGE)
const val RECIPE_SUMMARY_SUCCESSFUL = """[
{
"id": 1,
"name": "Cake",
"slug": "cake",
"image": "86",
"description": "A tasty cake",
"recipeCategory": ["dessert", "tasty"],
"tags": ["gluten", "allergic"],
"rating": 4,
"dateAdded": "2021-11-13",
"dateUpdated": "2021-11-13T15:30:13"
},
{
"id": 2,
"name": "Porridge",
"slug": "porridge",
"image": "89",
"description": "A tasty porridge",
"recipeCategory": ["porridge", "tasty"],
"tags": ["gluten", "milk"],
"rating": 5,
"dateAdded": "2021-11-12",
"dateUpdated": "2021-10-13T17:35:23"
}
]"""
const val RECIPE_SUMMARY_UNSUCCESSFUL = """
{"detail":"Unauthorized"}
"""
val CAKE_RECIPE_ENTITY = RecipeEntity(
localId = 1,
remoteId = 1,
name = "Cake",
slug = "cake",
image = "86",
description = "A tasty cake",
rating = 4,
dateAdded = LocalDate.parse("2021-11-13"),
dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13")
)
val PORRIDGE_RECIPE_ENTITY = RecipeEntity(
localId = 2,
remoteId = 2,
name = "Porridge",
slug = "porridge",
image = "89",
description = "A tasty porridge",
rating = 5,
dateAdded = LocalDate.parse("2021-11-12"),
dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"),
)
val TEST_RECIPE_ENTITIES = listOf(CAKE_RECIPE_ENTITY, PORRIDGE_RECIPE_ENTITY)
fun MockWebServer.enqueueSuccessfulRecipeSummaryResponse() {
val response = MockResponse()
.setBody(RECIPE_SUMMARY_SUCCESSFUL)
.setHeader("Content-Type", "application/json")
.setResponseCode(200)
enqueue(response)
}
fun MockWebServer.enqueueUnsuccessfulRecipeSummaryResponse() {
val response = MockResponse()
.setBody(RECIPE_SUMMARY_UNSUCCESSFUL)
.setHeader("Content-Type", "application/json")
.setResponseCode(401)
enqueue(response)
}
}

View File

@@ -4,10 +4,11 @@ import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidTest
import gq.kirmanak.mealie.data.MealieDb
import gq.kirmanak.mealie.data.auth.impl.HiltRobolectricTest
import gq.kirmanak.mealie.data.recipes.network.GetRecipeSummaryResponse
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.CAKE_RECIPE_ENTITY
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.PORRIDGE_RECIPE_ENTITY
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.RECIPE_SUMMARY_CAKE
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.TEST_RECIPE_SUMMARIES
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import org.junit.Test
import javax.inject.Inject
@@ -46,30 +47,7 @@ class RecipeStorageImplTest : HiltRobolectricTest() {
fun `when saveRecipes then saves recipes`(): Unit = runBlocking {
subject.saveRecipes(TEST_RECIPE_SUMMARIES)
val actualTags = mealieDb.recipeDao().queryAllRecipes()
assertThat(actualTags).containsExactly(
RecipeEntity(
localId = 1,
remoteId = 1,
name = "Cake",
slug = "cake",
image = "86",
description = "A tasty cake",
rating = 4,
dateAdded = LocalDate.parse("2021-11-13"),
dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13")
),
RecipeEntity(
localId = 2,
remoteId = 2,
name = "Porridge",
slug = "porridge",
image = "89",
description = "A tasty porridge",
rating = 5,
dateAdded = LocalDate.parse("2021-11-12"),
dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"),
)
)
assertThat(actualTags).containsExactly(CAKE_RECIPE_ENTITY, PORRIDGE_RECIPE_ENTITY)
}
@Test
@@ -96,33 +74,59 @@ class RecipeStorageImplTest : HiltRobolectricTest() {
)
}
companion object {
private val RECIPE_SUMMARY_CAKE = GetRecipeSummaryResponse(
remoteId = 1,
name = "Cake",
slug = "cake",
image = "86",
description = "A tasty cake",
recipeCategories = listOf("dessert", "tasty"),
tags = listOf("gluten", "allergic"),
rating = 4,
dateAdded = LocalDate.parse("2021-11-13"),
dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13"),
@Test
fun `when refreshAll then old recipes aren't preserved`(): Unit = runBlocking {
subject.saveRecipes(TEST_RECIPE_SUMMARIES)
subject.refreshAll(listOf(RECIPE_SUMMARY_CAKE))
val actual = mealieDb.recipeDao().queryAllRecipes()
assertThat(actual).containsExactly(
CAKE_RECIPE_ENTITY.copy(localId = 3),
)
}
private val RECIPE_SUMMARY_PORRIDGE = GetRecipeSummaryResponse(
remoteId = 2,
name = "Porridge",
slug = "porridge",
image = "89",
description = "A tasty porridge",
recipeCategories = listOf("porridge", "tasty"),
tags = listOf("gluten", "milk"),
rating = 5,
dateAdded = LocalDate.parse("2021-11-12"),
dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"),
@Test
fun `when refreshAll then old category recipes aren't preserved`(): Unit = runBlocking {
subject.saveRecipes(TEST_RECIPE_SUMMARIES)
subject.refreshAll(listOf(RECIPE_SUMMARY_CAKE))
val actual = mealieDb.recipeDao().queryAllCategoryRecipes()
assertThat(actual).containsExactly(
CategoryRecipeEntity(categoryId = 1, recipeId = 3),
CategoryRecipeEntity(categoryId = 2, recipeId = 3),
)
}
private val TEST_RECIPE_SUMMARIES = listOf(RECIPE_SUMMARY_CAKE, RECIPE_SUMMARY_PORRIDGE)
@Test
fun `when refreshAll then old tag recipes aren't preserved`(): Unit = runBlocking {
subject.saveRecipes(TEST_RECIPE_SUMMARIES)
subject.refreshAll(listOf(RECIPE_SUMMARY_CAKE))
val actual = mealieDb.recipeDao().queryAllTagRecipes()
assertThat(actual).containsExactly(
TagRecipeEntity(tagId = 1, recipeId = 3),
TagRecipeEntity(tagId = 2, recipeId = 3),
)
}
@Test
fun `when clearAllLocalData then recipes aren't preserved`(): Unit = runBlocking {
subject.saveRecipes(TEST_RECIPE_SUMMARIES)
subject.clearAllLocalData()
val actual = mealieDb.recipeDao().queryAllRecipes()
assertThat(actual).isEmpty()
}
@Test
fun `when clearAllLocalData then categories aren't preserved`(): Unit = runBlocking {
subject.saveRecipes(TEST_RECIPE_SUMMARIES)
subject.clearAllLocalData()
val actual = mealieDb.recipeDao().queryAllCategories()
assertThat(actual).isEmpty()
}
@Test
fun `when clearAllLocalData then tags aren't preserved`(): Unit = runBlocking {
subject.saveRecipes(TEST_RECIPE_SUMMARIES)
subject.clearAllLocalData()
val actual = mealieDb.recipeDao().queryAllTags()
assertThat(actual).isEmpty()
}
}

View File

@@ -0,0 +1,138 @@
package gq.kirmanak.mealie.data.recipes.impl
import androidx.paging.*
import androidx.paging.LoadType.*
import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidTest
import gq.kirmanak.mealie.data.MealieDb
import gq.kirmanak.mealie.data.auth.AuthRepo
import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.TEST_PASSWORD
import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.TEST_USERNAME
import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.enqueueSuccessfulAuthResponse
import gq.kirmanak.mealie.data.auth.impl.MockServerTest
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.CAKE_RECIPE_ENTITY
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.PORRIDGE_RECIPE_ENTITY
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.TEST_RECIPE_ENTITIES
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.enqueueSuccessfulRecipeSummaryResponse
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.enqueueUnsuccessfulRecipeSummaryResponse
import gq.kirmanak.mealie.data.recipes.db.RecipeEntity
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import javax.inject.Inject
@ExperimentalPagingApi
@HiltAndroidTest
class RecipesRemoteMediatorTest : MockServerTest() {
private val pagingConfig = PagingConfig(
pageSize = 2,
prefetchDistance = 5,
enablePlaceholders = false
)
@Inject
lateinit var subject: RecipesRemoteMediator
@Inject
lateinit var authRepo: AuthRepo
@Inject
lateinit var mealieDb: MealieDb
@Before
fun authenticate(): Unit = runBlocking {
mockServer.enqueueSuccessfulAuthResponse()
authRepo.authenticate(TEST_USERNAME, TEST_PASSWORD, serverUrl)
mockServer.takeRequest()
}
@Test
fun `when first load with refresh successful then result success`(): Unit = runBlocking {
mockServer.enqueueSuccessfulRecipeSummaryResponse()
val result = subject.load(REFRESH, pagingState())
assertThat(result).isInstanceOf(RemoteMediator.MediatorResult.Success::class.java)
}
@Test
fun `when first load with refresh successful then recipes stored`(): Unit = runBlocking {
mockServer.enqueueSuccessfulRecipeSummaryResponse()
subject.load(REFRESH, pagingState())
val actual = mealieDb.recipeDao().queryAllRecipes()
assertThat(actual).containsExactly(CAKE_RECIPE_ENTITY, PORRIDGE_RECIPE_ENTITY)
}
@Test
fun `when load state prepend then success`(): Unit = runBlocking {
val result = subject.load(PREPEND, pagingState())
assertThat(result).isInstanceOf(RemoteMediator.MediatorResult.Success::class.java)
}
@Test
fun `when load state prepend then end is reached`(): Unit = runBlocking {
val result = subject.load(PREPEND, pagingState())
assertThat((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached).isTrue()
}
@Test
fun `when load successful then lastRequestEnd updated`(): Unit = runBlocking {
mockServer.enqueueSuccessfulRecipeSummaryResponse()
subject.load(REFRESH, pagingState())
val actual = subject.lastRequestEnd
assertThat(actual).isEqualTo(2)
}
@Test
fun `when load fails then lastRequestEnd still 0`(): Unit = runBlocking {
mockServer.enqueueUnsuccessfulRecipeSummaryResponse()
subject.load(REFRESH, pagingState())
val actual = subject.lastRequestEnd
assertThat(actual).isEqualTo(0)
}
@Test
fun `when load fails then result is error`(): Unit = runBlocking {
mockServer.enqueueUnsuccessfulRecipeSummaryResponse()
val actual = subject.load(REFRESH, pagingState())
assertThat(actual).isInstanceOf(RemoteMediator.MediatorResult.Error::class.java)
}
@Test
fun `when refresh then request params correct`(): Unit = runBlocking {
mockServer.enqueueUnsuccessfulRecipeSummaryResponse()
subject.load(REFRESH, pagingState())
val actual = mockServer.takeRequest().path
assertThat(actual).isEqualTo("/api/recipes/summary?start=0&limit=6")
}
@Test
fun `when append then request params correct`(): Unit = runBlocking {
mockServer.enqueueSuccessfulRecipeSummaryResponse()
subject.load(REFRESH, pagingState())
mockServer.takeRequest()
mockServer.enqueueSuccessfulRecipeSummaryResponse()
subject.load(APPEND, pagingState())
val actual = mockServer.takeRequest().path
assertThat(actual).isEqualTo("/api/recipes/summary?start=2&limit=2")
}
@Test
fun `when append fails then recipes aren't removed`(): Unit = runBlocking {
mockServer.enqueueSuccessfulRecipeSummaryResponse()
subject.load(REFRESH, pagingState())
mockServer.takeRequest()
mockServer.enqueueUnsuccessfulRecipeSummaryResponse()
subject.load(APPEND, pagingState())
val actual = mealieDb.recipeDao().queryAllRecipes()
assertThat(actual).isEqualTo(TEST_RECIPE_ENTITIES)
}
private fun pagingState(
pages: List<PagingSource.LoadResult.Page<Int, RecipeEntity>> = emptyList(),
anchorPosition: Int? = null
): PagingState<Int, RecipeEntity> = PagingState(
pages = pages,
anchorPosition = anchorPosition,
config = pagingConfig,
leadingPlaceholderCount = 0
)
}