Implement proper loading of recipe summaries
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user