Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/6619 limit stats refresh #2526

Merged
merged 21 commits into from
Oct 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
96c3cf7
Migrate product leaderboards to Room: create DAO and Entity
JorgeMucientes Sep 8, 2022
9d7113a
Add new TopPerformerProductsDao to database
JorgeMucientes Sep 8, 2022
dbc9d4e
Add migration policy for new TopPerformerProductEntity
JorgeMucientes Sep 8, 2022
8e3d793
Fetch product leaderboard and save them in new Room DB
JorgeMucientes Sep 8, 2022
2b96316
Fix tests
JorgeMucientes Sep 8, 2022
9659ea2
Use correct id to save product
JorgeMucientes Sep 8, 2022
24341fe
Add missing database scheme for v20
JorgeMucientes Sep 8, 2022
f4db396
Add function to retrieve top performers from DB
JorgeMucientes Sep 9, 2022
ef2b626
Remove old functions to fetch top performer products
JorgeMucientes Sep 9, 2022
0115a50
Invalidate top performers cache when new order is placed
JorgeMucientes Sep 12, 2022
eed7650
Merge branch 'trunk' into feature/6619-limit-stats-refresh
JorgeMucientes Sep 16, 2022
db5fd52
Fix tests and add new ones
JorgeMucientes Sep 16, 2022
76a07ca
Remove unused WCLeaderboardsSqlUtils
JorgeMucientes Sep 16, 2022
5584ac9
Remove unused methods from WCLeaderboardsStore
JorgeMucientes Sep 16, 2022
224cbfb
Fix detekt and checkstyle issues
JorgeMucientes Sep 17, 2022
74af7f6
Make query functions suspendable
JorgeMucientes Oct 5, 2022
c306e06
Remove unneeded transactional Room operations
JorgeMucientes Oct 5, 2022
4de12d9
Remove unused function
JorgeMucientes Oct 5, 2022
68ae311
Convert to interface, abstract class not needed
JorgeMucientes Oct 5, 2022
3771cb2
Add missing suspend function
JorgeMucientes Oct 5, 2022
680ada3
Init StoreUnderTest in a more consistent way
JorgeMucientes Oct 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import org.wordpress.android.fluxc.example.R
import org.wordpress.android.fluxc.example.prependToLog
import org.wordpress.android.fluxc.example.ui.StoreSelectingFragment
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.model.leaderboards.WCTopPerformerProductModel
import org.wordpress.android.fluxc.persistence.entity.TopPerformerProductEntity
import org.wordpress.android.fluxc.store.WCLeaderboardsStore
import org.wordpress.android.fluxc.store.WCStatsStore.StatsGranularity
import org.wordpress.android.fluxc.store.WCStatsStore.StatsGranularity.DAYS
Expand Down Expand Up @@ -82,7 +82,7 @@ class WooLeaderboardsFragment : StoreSelectingFragment() {
coroutineScope.launch {
try {
takeAsyncRequestWithValidSite {
wcLeaderboardsStore.fetchProductLeaderboards(
wcLeaderboardsStore.fetchTopPerformerProducts(
it,
unit,
forceRefresh = false
Expand All @@ -101,7 +101,7 @@ class WooLeaderboardsFragment : StoreSelectingFragment() {
private fun launchProductLeaderboardsCacheRetrieval(unit: StatsGranularity) {
coroutineScope.launch {
try {
takeAsyncRequestWithValidSite { wcLeaderboardsStore.fetchCachedProductLeaderboards(it, unit) }
takeAsyncRequestWithValidSite { wcLeaderboardsStore.fetchTopPerformerProducts(it, unit) }
?.model
?.let { logLeaderboardResponse(it, unit) }
?: prependToLog("Couldn't fetch Products Leaderboards.")
Expand All @@ -111,14 +111,13 @@ class WooLeaderboardsFragment : StoreSelectingFragment() {
}
}

private fun logLeaderboardResponse(model: List<WCTopPerformerProductModel>, unit: StatsGranularity) {
private fun logLeaderboardResponse(model: List<TopPerformerProductEntity>, unit: StatsGranularity) {
model.forEach {
prependToLog(" Top Performer Product id: ${it.product.remoteProductId ?: "Product id not available"}")
prependToLog(" Top Performer Product name: ${it.product.name ?: "Product name not available"}")
prependToLog(" Top Performer Product id: ${it.productId ?: "Product id not available"}")
prependToLog(" Top Performer Product name: ${it.name ?: "Product name not available"}")
prependToLog(" Top Performer currency: ${it.currency ?: "Currency not available"}")
prependToLog(" Top Performer quantity: ${it.quantity ?: "Quantity not available"}")
prependToLog(" Top Performer total: ${it.total ?: "total not available"}")
prependToLog(" Top Performer id: ${it.id ?: "ID not available"}")
prependToLog(" --------- Product ---------")
}
prependToLog("========== Top Performers of the $unit =========")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import com.yarolegovich.wellsql.WellSql
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
Expand All @@ -20,142 +19,223 @@ import org.wordpress.android.fluxc.model.WCProductModel
import org.wordpress.android.fluxc.model.leaderboards.WCProductLeaderboardsMapper
import org.wordpress.android.fluxc.model.leaderboards.WCTopPerformerProductModel
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooPayload
import org.wordpress.android.fluxc.network.rest.wpcom.wc.leaderboards.LeaderboardsApiResponse
import org.wordpress.android.fluxc.network.rest.wpcom.wc.leaderboards.LeaderboardsApiResponse.Type.PRODUCTS
import org.wordpress.android.fluxc.network.rest.wpcom.wc.leaderboards.LeaderboardsRestClient
import org.wordpress.android.fluxc.persistence.WellSqlConfig
import org.wordpress.android.fluxc.persistence.dao.TopPerformerProductsDao
import org.wordpress.android.fluxc.persistence.entity.TopPerformerProductEntity
import org.wordpress.android.fluxc.store.WCLeaderboardsStore
import org.wordpress.android.fluxc.store.WCProductStore
import org.wordpress.android.fluxc.store.WCStatsStore.StatsGranularity.DAYS
import org.wordpress.android.fluxc.test
import org.wordpress.android.fluxc.tools.initCoroutineEngine
import org.wordpress.android.fluxc.wc.leaderboards.WCLeaderboardsTestFixtures.duplicatedTopPerformersList
import org.wordpress.android.fluxc.wc.leaderboards.WCLeaderboardsTestFixtures.generateSampleLeaderboardsApiResponse
import org.wordpress.android.fluxc.wc.leaderboards.WCLeaderboardsTestFixtures.stubSite
import org.wordpress.android.fluxc.wc.leaderboards.WCLeaderboardsTestFixtures.stubbedTopPerformersList

@Config(manifest = Config.NONE)
@RunWith(RobolectricTestRunner::class)
class WCLeaderboardsStoreTest {
private val restClient: LeaderboardsRestClient = mock()
private val productStore: WCProductStore = mock()
private var mapper: WCProductLeaderboardsMapper = mock()
private val topPerformersDao: TopPerformerProductsDao = mock()

private lateinit var storeUnderTest: WCLeaderboardsStore
private lateinit var restClient: LeaderboardsRestClient
private lateinit var productStore: WCProductStore
private lateinit var mapper: WCProductLeaderboardsMapper

@Before
fun setUp() {
fun setup(prepareMocks: () -> Unit = {}) {
prepareMocks()
createStoreUnderTest()
val appContext = RuntimeEnvironment.application.applicationContext
val config = SingleStoreWellSqlConfigForTests(
appContext,
listOf(SiteModel::class.java, WCTopPerformerProductModel::class.java, WCProductModel::class.java),
WellSqlConfig.ADDON_WOOCOMMERCE
appContext,
listOf(
SiteModel::class.java,
WCTopPerformerProductModel::class.java,
WCProductModel::class.java
),
WellSqlConfig.ADDON_WOOCOMMERCE
)
WellSql.init(config)
config.reset()
initMocks()
createStoreUnderTest()
}

@Test
fun `fetch product leaderboards with empty result should return WooError`() = test {
whenever(restClient.fetchLeaderboards(stubSite, DAYS, null, null, forceRefresh = false))
.thenReturn(WooPayload(emptyArray()))
fun `fetch top performer products with empty result should return WooError`() = test {
setup()
givenFetchLeaderBoardsReturns(emptyArray())

val result = storeUnderTest.fetchTopPerformerProducts(stubSite)

val result = storeUnderTest.fetchProductLeaderboards(stubSite)
assertThat(result.model).isNull()
assertThat(result.error).isNotNull
}

@Test
fun `fetch product leaderboards should filter leaderboards by PRODUCTS type`() = test {
mapper = spy()
createStoreUnderTest()
fun `fetch top performer products should filter leaderboards by PRODUCTS type`() = test {
setup { mapper = spy() }
val response = generateSampleLeaderboardsApiResponse()
val filteredResponse = response?.firstOrNull { it.type == PRODUCTS }
givenFetchLeaderBoardsReturns(response)

whenever(restClient.fetchLeaderboards(stubSite, DAYS, null, null, forceRefresh = false))
.thenReturn(WooPayload(response))
storeUnderTest.fetchTopPerformerProducts(stubSite)

storeUnderTest.fetchProductLeaderboards(stubSite)
verify(mapper).map(filteredResponse!!, stubSite, productStore, DAYS)
verify(mapper).mapTopPerformerProductsEntity(
response?.firstOrNull { it.type == PRODUCTS }!!,
stubSite,
productStore,
DAYS
)
}

@Test
fun `fetch product leaderboards should call mapper once`() = test {
mapper = spy()
createStoreUnderTest()
fun `fetch top performer products should call mapper once`() = test {
setup()
val response = generateSampleLeaderboardsApiResponse()
givenFetchLeaderBoardsReturns(response)

whenever(restClient.fetchLeaderboards(stubSite, DAYS, null, null, forceRefresh = false))
.thenReturn(WooPayload(response))
storeUnderTest.fetchTopPerformerProducts(stubSite)

storeUnderTest.fetchProductLeaderboards(stubSite)
verify(mapper, times(1)).map(any(), any(), any(), any())
verify(mapper, times(1)).mapTopPerformerProductsEntity(any(), any(), any(), any())
}

@Test
fun `fetch product leaderboards should return WooResult correctly`() = test {
val response = generateSampleLeaderboardsApiResponse()
val filteredResponse = response?.firstOrNull { it.type == PRODUCTS }

whenever(restClient.fetchLeaderboards(stubSite, DAYS, null, null, forceRefresh = false))
.thenReturn(WooPayload(response))

whenever(mapper.map(filteredResponse!!, stubSite, productStore, DAYS)).thenReturn(stubbedTopPerformersList)

val result = storeUnderTest.fetchProductLeaderboards(stubSite)
assertThat(result.model).isNotNull
assertThat(result.model).isEqualTo(stubbedTopPerformersList)
assertThat(result.error).isNull()
}
fun `fetch top performer products should return mapped top performer entities correctly`() =
test {
setup()
val response = generateSampleLeaderboardsApiResponse()
givenFetchLeaderBoardsReturns(response)
givenTopPerformersMapperReturns(
givenResponse = response?.firstOrNull { it.type == PRODUCTS }!!,
returnedTopPerformersList = TOP_PERFORMER_ENTITY_LIST
)

val result = storeUnderTest.fetchTopPerformerProducts(stubSite)

assertThat(result.model).isNotNull
assertThat(result.model).isEqualTo(TOP_PERFORMER_ENTITY_LIST)
assertThat(result.error).isNull()
}

@Test
fun `fetch product leaderboards from a invalid site ID should return WooResult with error`() = test {
val response = generateSampleLeaderboardsApiResponse()
val filteredResponse = response?.firstOrNull { it.type == PRODUCTS }
fun `fetch top performer products from a invalid site ID should return WooResult with error`() =
test {
setup()
val response = generateSampleLeaderboardsApiResponse()
givenFetchLeaderBoardsReturns(response)
givenTopPerformersMapperReturns(
givenResponse = response?.firstOrNull { it.type == PRODUCTS }!!,
returnedTopPerformersList = TOP_PERFORMER_ENTITY_LIST,
SiteModel().apply { id = 100 },
)

whenever(restClient.fetchLeaderboards(stubSite, DAYS, null, null, forceRefresh = false))
.thenReturn(WooPayload(response))
val result = storeUnderTest.fetchTopPerformerProducts(stubSite)

whenever(mapper.map(
filteredResponse!!,
SiteModel().apply { id = 100 },
productStore,
DAYS))
.thenReturn(stubbedTopPerformersList)
assertThat(result.model).isNull()
assertThat(result.error).isNotNull
}

val result = storeUnderTest.fetchProductLeaderboards(stubSite)
assertThat(result.model).isNull()
assertThat(result.error).isNotNull
}
@Test
fun `fetching top performer products should update database with new data`() =
test {
setup()
val response = generateSampleLeaderboardsApiResponse()
givenFetchLeaderBoardsReturns(response)
givenTopPerformersMapperReturns(
givenResponse = response?.firstOrNull { it.type == PRODUCTS }!!,
returnedTopPerformersList = TOP_PERFORMER_ENTITY_LIST
)

storeUnderTest.fetchTopPerformerProducts(stubSite)

verify(topPerformersDao, times(1))
.updateTopPerformerProductsFor(
stubSite.siteId,
DAYS.toString(),
TOP_PERFORMER_ENTITY_LIST
)
}

@Test
fun `fetch product leaderboards should distinct duplicate items`() = test {
val response = generateSampleLeaderboardsApiResponse()
val filteredResponse = response?.firstOrNull { it.type == PRODUCTS }
fun `invalidating top performer products should update database`() =
test {
setup()
givenCachedTopPerformers()

storeUnderTest.invalidateTopPerformers(stubSite.siteId)

verify(topPerformersDao, times(1))
.getTopPerformerProductsForSite(stubSite.siteId)
verify(topPerformersDao, times(1))
.updateTopPerformerProductsForSite(
stubSite.siteId,
INVALIDATED_TOP_PERFORMER_ENTITY_LIST
)
}

private suspend fun givenCachedTopPerformers() {
whenever(
topPerformersDao.getTopPerformerProductsForSite(stubSite.siteId)
).thenReturn(TOP_PERFORMER_ENTITY_LIST)
}

private suspend fun givenFetchLeaderBoardsReturns(response: Array<LeaderboardsApiResponse>?) {
whenever(restClient.fetchLeaderboards(stubSite, DAYS, null, null, forceRefresh = false))
.thenReturn(WooPayload(response))

whenever(mapper.map(filteredResponse!!, stubSite, productStore, DAYS)).thenReturn(duplicatedTopPerformersList)
.thenReturn(WooPayload(response))
}

val result = storeUnderTest.fetchProductLeaderboards(stubSite)
assertThat(result.model).isNotNull
assertThat(result.model!!.size).isEqualTo(1)
assertThat(result.model).isNotEqualTo(stubbedTopPerformersList)
assertThat(result.error).isNull()
private fun createStoreUnderTest() {
storeUnderTest = WCLeaderboardsStore(
restClient,
productStore,
mapper,
initCoroutineEngine(),
topPerformersDao
)
}

private fun initMocks() {
restClient = mock()
productStore = mock()
mapper = mock()
private suspend fun givenTopPerformersMapperReturns(
givenResponse: LeaderboardsApiResponse,
returnedTopPerformersList: List<TopPerformerProductEntity>,
siteModel: SiteModel = stubSite
) {
whenever(
mapper.mapTopPerformerProductsEntity(
givenResponse,
siteModel,
productStore,
DAYS
)
).thenReturn(returnedTopPerformersList)
}

private fun createStoreUnderTest() =
WCLeaderboardsStore(
restClient,
productStore,
mapper,
initCoroutineEngine()
).apply { storeUnderTest = this }
companion object {
val TOP_PERFORMER_ENTITY_LIST =
listOf(
TopPerformerProductEntity(
siteId = 1,
granularity = "Today",
productId = 123,
name = "product",
imageUrl = null,
quantity = 5,
currency = "USD",
total = 10.5,
millisSinceLastUpdated = 100
)
)
val INVALIDATED_TOP_PERFORMER_ENTITY_LIST =
listOf(
TopPerformerProductEntity(
siteId = 1,
granularity = "Today",
productId = 123,
name = "product",
imageUrl = null,
quantity = 5,
currency = "USD",
total = 10.5,
millisSinceLastUpdated = 0
)
)
}
}
Loading