diff --git a/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClientTest.kt b/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClientTest.kt index 44849397ab..66d7395faf 100644 --- a/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClientTest.kt +++ b/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClientTest.kt @@ -21,6 +21,8 @@ import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.UnitTestUtils import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.dashboard.CardModel +import org.wordpress.android.fluxc.model.dashboard.CardModel.PostsCardModel +import org.wordpress.android.fluxc.model.dashboard.CardModel.TodaysStatsCardModel import org.wordpress.android.fluxc.network.BaseRequest.BaseNetworkError import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType import org.wordpress.android.fluxc.network.UserAgent @@ -35,6 +37,10 @@ import org.wordpress.android.fluxc.network.rest.wpcom.dashboard.CardsRestClient. import org.wordpress.android.fluxc.network.rest.wpcom.dashboard.CardsRestClient.TodaysStatsResponse import org.wordpress.android.fluxc.store.dashboard.CardsStore.CardsErrorType import org.wordpress.android.fluxc.store.dashboard.CardsStore.CardsPayload +import org.wordpress.android.fluxc.store.dashboard.CardsStore.PostCardError +import org.wordpress.android.fluxc.store.dashboard.CardsStore.PostCardErrorType +import org.wordpress.android.fluxc.store.dashboard.CardsStore.TodaysStatsCardError +import org.wordpress.android.fluxc.store.dashboard.CardsStore.TodaysStatsCardErrorType import org.wordpress.android.fluxc.test /* DATE */ @@ -45,6 +51,10 @@ private const val DATE_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss" private val CARD_TYPES = listOf(CardModel.Type.TODAYS_STATS, CardModel.Type.POSTS) +/* ERRORS */ +private const val JETPACK_DISABLED = "jetpack_disabled" +private const val UNAUTHORIZED = "unauthorized" + /* RESPONSE */ private val TODAYS_STATS_RESPONSE = TodaysStatsResponse( @@ -188,6 +198,62 @@ class CardsRestClientTest { assertError(CardsErrorType.GENERIC_ERROR, result) } + /* TODAY'S STATS CARD ERRORS */ + @Test + fun `given jetpack disconn, when fetch cards triggered, then returns todays stats jetpack disconn card error`() = + test { + val json = UnitTestUtils.getStringFromResourceFile(javaClass, DASHBOARD_CARDS_WITH_ERRORS_JSON) + initFetchCards(data = getCardsResponseFromJsonString(json)) + + val result = restClient.fetchCards(site, CARD_TYPES) + + assertSuccessWithTodaysStatsError(TodaysStatsCardErrorType.JETPACK_DISCONNECTED, result) + } + + @Test + fun `given jetpack disabled, when fetch cards triggered, then returns todays stats jetpack disabled card error`() = + test { + val json = UnitTestUtils.getStringFromResourceFile(javaClass, DASHBOARD_CARDS_WITH_ERRORS_JSON) + val data = getCardsResponseFromJsonString(json) + .copy(todaysStats = TodaysStatsResponse(error = JETPACK_DISABLED)) + initFetchCards(data = data) + + val result = restClient.fetchCards(site, CARD_TYPES) + + assertSuccessWithTodaysStatsError(TodaysStatsCardErrorType.JETPACK_DISABLED, result) + } + + @Test + fun `given stats unauthorized, when fetch cards triggered, then returns todays stats unauthorized card error`() = + test { + val json = UnitTestUtils.getStringFromResourceFile(javaClass, DASHBOARD_CARDS_WITH_ERRORS_JSON) + val data = getCardsResponseFromJsonString(json) + .copy(todaysStats = TodaysStatsResponse(error = UNAUTHORIZED)) + initFetchCards(data = data) + + val result = restClient.fetchCards(site, CARD_TYPES) + + assertSuccessWithTodaysStatsError(TodaysStatsCardErrorType.UNAUTHORIZED, result) + } + + /* POST CARD ERROR */ + @Test + fun `given posts unauthorized error, when fetch cards triggered, then returns post card card error`() = + test { + val json = UnitTestUtils.getStringFromResourceFile(javaClass, DASHBOARD_CARDS_WITH_ERRORS_JSON) + initFetchCards(data = getCardsResponseFromJsonString(json)) + + val result = restClient.fetchCards(site, CARD_TYPES) + + assertSuccessWithPostCardError(result) + } + + private fun CardsPayload.findTodaysStatsCardError(): TodaysStatsCardError? = + this.response?.toCards()?.filterIsInstance(TodaysStatsCardModel::class.java)?.firstOrNull()?.error + + private fun CardsPayload.findPostCardError(): PostCardError? = + this.response?.toCards()?.filterIsInstance(PostsCardModel::class.java)?.firstOrNull()?.error + private fun getCardsResponseFromJsonString(json: String): CardsResponse { val responseType = object : TypeToken() {}.type return GsonBuilder().setDateFormat(DATE_FORMAT_PATTERN) @@ -239,11 +305,33 @@ class CardsRestClientTest { } } + private fun assertSuccessWithTodaysStatsError( + expected: TodaysStatsCardErrorType, + actual: CardsPayload + ) { + with(actual) { + assertEquals(site, this@CardsRestClientTest.site) + assertFalse(isError) + assertEquals(expected, findTodaysStatsCardError()?.type) + } + } + + private fun assertSuccessWithPostCardError( + actual: CardsPayload + ) { + with(actual) { + assertEquals(site, this@CardsRestClientTest.site) + assertFalse(isError) + assertEquals(PostCardErrorType.UNAUTHORIZED, findPostCardError()?.type) + } + } + companion object { private const val API_BASE_PATH = "https://public-api.wordpress.com/wpcom/v2" private const val API_SITE_PATH = "$API_BASE_PATH/sites" private const val API_DASHBOARD_CARDS_PATH = "dashboard/cards-data/" private const val DASHBOARD_CARDS_JSON = "wp/dashboard/cards.json" + private const val DASHBOARD_CARDS_WITH_ERRORS_JSON = "wp/dashboard/cards_with_errors.json" } } diff --git a/example/src/test/java/org/wordpress/android/fluxc/store/dashboard/CardsStoreTest.kt b/example/src/test/java/org/wordpress/android/fluxc/store/dashboard/CardsStoreTest.kt index 20a77065da..bf8c4bfc7b 100644 --- a/example/src/test/java/org/wordpress/android/fluxc/store/dashboard/CardsStoreTest.kt +++ b/example/src/test/java/org/wordpress/android/fluxc/store/dashboard/CardsStoreTest.kt @@ -27,6 +27,10 @@ import org.wordpress.android.fluxc.store.dashboard.CardsStore.CardsError import org.wordpress.android.fluxc.store.dashboard.CardsStore.CardsErrorType import org.wordpress.android.fluxc.store.dashboard.CardsStore.CardsPayload import org.wordpress.android.fluxc.store.dashboard.CardsStore.CardsResult +import org.wordpress.android.fluxc.store.dashboard.CardsStore.PostCardError +import org.wordpress.android.fluxc.store.dashboard.CardsStore.PostCardErrorType +import org.wordpress.android.fluxc.store.dashboard.CardsStore.TodaysStatsCardError +import org.wordpress.android.fluxc.store.dashboard.CardsStore.TodaysStatsCardErrorType import org.wordpress.android.fluxc.test import org.wordpress.android.fluxc.tools.initCoroutineEngine import kotlin.test.assertEquals @@ -91,6 +95,10 @@ private val TODAYS_STATS_MODEL = TodaysStatsCardModel( comments = TODAYS_STATS_COMMENTS ) +private val TODAYS_STATS_WITH_ERROR_MODEL = TodaysStatsCardModel( + error = TodaysStatsCardError(TodaysStatsCardErrorType.JETPACK_DISCONNECTED) +) + private val POST_MODEL = PostCardModel( id = POST_ID, title = POST_TITLE, @@ -105,6 +113,10 @@ private val POSTS_MODEL = PostsCardModel( scheduled = listOf(POST_MODEL) ) +private val POSTS_WITH_ERROR_MODEL = PostsCardModel( + error = PostCardError(PostCardErrorType.UNAUTHORIZED) +) + private val CARDS_MODEL = listOf( TODAYS_STATS_MODEL, POSTS_MODEL @@ -118,6 +130,13 @@ private val TODAYS_STATS_ENTITY = CardEntity( json = CardsUtils.GSON.toJson(TODAYS_STATS_MODEL) ) +private val TODAY_STATS_WITH_ERROR_ENTITY = CardEntity( + siteLocalId = SITE_LOCAL_ID, + type = CardModel.Type.TODAYS_STATS.name, + date = CardsUtils.getInsertDate(), + json = CardsUtils.GSON.toJson(TODAYS_STATS_WITH_ERROR_MODEL) +) + private val POSTS_ENTITY = CardEntity( siteLocalId = SITE_LOCAL_ID, type = CardModel.Type.POSTS.name, @@ -125,6 +144,13 @@ private val POSTS_ENTITY = CardEntity( json = CardsUtils.GSON.toJson(POSTS_MODEL) ) +private val POSTS_WITH_ERROR_ENTITY = CardEntity( + siteLocalId = SITE_LOCAL_ID, + type = CardModel.Type.POSTS.name, + date = CardsUtils.getInsertDate(), + json = CardsUtils.GSON.toJson(POSTS_WITH_ERROR_MODEL) +) + private val CARDS_ENTITY = listOf( TODAYS_STATS_ENTITY, POSTS_ENTITY @@ -135,6 +161,7 @@ class CardsStoreTest { @Mock private lateinit var siteModel: SiteModel @Mock private lateinit var restClient: CardsRestClient @Mock private lateinit var dao: CardsDao + @Mock private lateinit var cardsRespone: CardsResponse private lateinit var cardsStore: CardsStore @@ -282,4 +309,81 @@ class CardsStoreTest { assertThat(result).isEqualTo(CardsResult(listOf(POSTS_MODEL))) } + + /* TODAYS STATS CARD WITH ERROR */ + + @Test + fun `given todays stats card with error, when fetch cards triggered, then card with error inserted into db`() = + test { + whenever(restClient.fetchCards(siteModel, CARD_TYPES)).thenReturn(CardsPayload(cardsRespone)) + whenever(cardsRespone.toCards()).thenReturn(listOf(TODAYS_STATS_WITH_ERROR_MODEL)) + + cardsStore.fetchCards(siteModel, CARD_TYPES) + + verify(dao).insertWithDate(siteModel.id, listOf(TODAYS_STATS_WITH_ERROR_MODEL)) + } + + @Test + fun `given today's stats jetpack disconn error, when get cards triggered, then error exists in the card`() = test { + whenever(dao.get(SITE_LOCAL_ID, CARD_TYPES)) + .thenReturn( + flowOf(listOf(getTodaysStatsErrorCardEntity(TodaysStatsCardErrorType.JETPACK_DISCONNECTED))) + ) + + val result = cardsStore.getCards(siteModel, CARD_TYPES).single() + + assertThat(result.findTodaysStatsCardError()?.type).isEqualTo(TodaysStatsCardErrorType.JETPACK_DISCONNECTED) + } + + @Test + fun `given today's stats jetpack disabled error, when get cards triggered, then error exists in the card`() = test { + whenever(dao.get(SITE_LOCAL_ID, CARD_TYPES)) + .thenReturn(flowOf(listOf(getTodaysStatsErrorCardEntity(TodaysStatsCardErrorType.JETPACK_DISABLED)))) + + val result = cardsStore.getCards(siteModel, CARD_TYPES).single() + + assertThat(result.findTodaysStatsCardError()?.type).isEqualTo(TodaysStatsCardErrorType.JETPACK_DISABLED) + } + + @Test + fun `given today's stats jetpack unauth error, when get cards triggered, then error exists in the card`() = test { + whenever(dao.get(SITE_LOCAL_ID, CARD_TYPES)) + .thenReturn(flowOf(listOf(getTodaysStatsErrorCardEntity(TodaysStatsCardErrorType.UNAUTHORIZED)))) + + val result = cardsStore.getCards(siteModel, CARD_TYPES).single() + + assertThat(result.findTodaysStatsCardError()?.type).isEqualTo(TodaysStatsCardErrorType.UNAUTHORIZED) + } + + /* POSTS CARD WITH ERROR */ + + @Test + fun `given posts card with error, when fetch cards triggered, then card with error inserted into db`() = test { + whenever(restClient.fetchCards(siteModel, CARD_TYPES)).thenReturn(CardsPayload(cardsRespone)) + whenever(cardsRespone.toCards()).thenReturn(listOf(POSTS_WITH_ERROR_MODEL)) + + cardsStore.fetchCards(siteModel, CARD_TYPES) + + verify(dao).insertWithDate(siteModel.id, listOf(POSTS_WITH_ERROR_MODEL)) + } + + @Test + fun `given posts card unauth error, when get cards triggered, then error exists in the card`() = test { + whenever(dao.get(SITE_LOCAL_ID, CARD_TYPES)).thenReturn(flowOf(listOf(POSTS_WITH_ERROR_ENTITY))) + + val result = cardsStore.getCards(siteModel, CARD_TYPES).single() + + assertThat(result.findPostsCardError()?.type).isEqualTo(PostCardErrorType.UNAUTHORIZED) + } + + private fun CardsResult>.findTodaysStatsCardError(): TodaysStatsCardError? = + model?.filterIsInstance(TodaysStatsCardModel::class.java)?.firstOrNull()?.error + + private fun CardsResult>.findPostsCardError(): PostCardError? = + model?.filterIsInstance(PostsCardModel::class.java)?.firstOrNull()?.error + + private fun getTodaysStatsErrorCardEntity(type: TodaysStatsCardErrorType) = + TODAY_STATS_WITH_ERROR_ENTITY.copy( + json = CardsUtils.GSON.toJson(TodaysStatsCardModel(error = TodaysStatsCardError(type))) + ) } diff --git a/example/src/test/resources/wp/dashboard/cards_with_errors.json b/example/src/test/resources/wp/dashboard/cards_with_errors.json new file mode 100644 index 0000000000..ffa40898a5 --- /dev/null +++ b/example/src/test/resources/wp/dashboard/cards_with_errors.json @@ -0,0 +1,8 @@ +{ + "todays_stats": { + "error": "jetpack_disconnected" + }, + "posts": { + "error": "unauthorized" + } +} diff --git a/fluxc/src/main/java/org/wordpress/android/fluxc/model/dashboard/CardModel.kt b/fluxc/src/main/java/org/wordpress/android/fluxc/model/dashboard/CardModel.kt index 7cf3a0911c..1f90c631aa 100644 --- a/fluxc/src/main/java/org/wordpress/android/fluxc/model/dashboard/CardModel.kt +++ b/fluxc/src/main/java/org/wordpress/android/fluxc/model/dashboard/CardModel.kt @@ -1,5 +1,7 @@ package org.wordpress.android.fluxc.model.dashboard +import org.wordpress.android.fluxc.store.dashboard.CardsStore.PostCardError +import org.wordpress.android.fluxc.store.dashboard.CardsStore.TodaysStatsCardError import java.util.Date sealed class CardModel( @@ -14,16 +16,18 @@ sealed class CardModel( } data class TodaysStatsCardModel( - val views: Int, - val visitors: Int, - val likes: Int, - val comments: Int + val views: Int = 0, + val visitors: Int = 0, + val likes: Int = 0, + val comments: Int = 0, + val error: TodaysStatsCardError? = null ) : CardModel(Type.TODAYS_STATS) data class PostsCardModel( - val hasPublished: Boolean, - val draft: List, - val scheduled: List + val hasPublished: Boolean = false, + val draft: List = emptyList(), + val scheduled: List = emptyList(), + val error: PostCardError? = null ) : CardModel(Type.POSTS) { data class PostCardModel( val id: Int, diff --git a/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClient.kt b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClient.kt index 8803b80d9a..2b95949b27 100644 --- a/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClient.kt +++ b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClient.kt @@ -21,6 +21,10 @@ import org.wordpress.android.fluxc.network.rest.wpcom.auth.AccessToken import org.wordpress.android.fluxc.store.dashboard.CardsStore.CardsError import org.wordpress.android.fluxc.store.dashboard.CardsStore.CardsErrorType import org.wordpress.android.fluxc.store.dashboard.CardsStore.CardsPayload +import org.wordpress.android.fluxc.store.dashboard.CardsStore.PostCardError +import org.wordpress.android.fluxc.store.dashboard.CardsStore.PostCardErrorType +import org.wordpress.android.fluxc.store.dashboard.CardsStore.TodaysStatsCardError +import org.wordpress.android.fluxc.store.dashboard.CardsStore.TodaysStatsCardErrorType import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton @@ -66,26 +70,48 @@ class CardsRestClient @Inject constructor( @SerializedName("views") val views: Int? = null, @SerializedName("visitors") val visitors: Int? = null, @SerializedName("likes") val likes: Int? = null, - @SerializedName("comments") val comments: Int? = null + @SerializedName("comments") val comments: Int? = null, + @SerializedName("error") val error: String? = null ) { fun toTodaysStatsCard() = TodaysStatsCardModel( views = views ?: 0, visitors = visitors ?: 0, likes = likes ?: 0, - comments = comments ?: 0 + comments = comments ?: 0, + error = error?.let { toTodaysStatsCardsError(it) } ) + + private fun toTodaysStatsCardsError(error: String): TodaysStatsCardError { + val errorType = when (error) { + JETPACK_DISCONNECTED -> TodaysStatsCardErrorType.JETPACK_DISCONNECTED + JETPACK_DISABLED -> TodaysStatsCardErrorType.JETPACK_DISABLED + UNAUTHORIZED -> TodaysStatsCardErrorType.UNAUTHORIZED + else -> TodaysStatsCardErrorType.GENERIC_ERROR + } + return TodaysStatsCardError(errorType, error) + } } data class PostsResponse( - @SerializedName("has_published") val hasPublished: Boolean, - @SerializedName("draft") val draft: List, - @SerializedName("scheduled") val scheduled: List + @SerializedName("has_published") val hasPublished: Boolean? = null, + @SerializedName("draft") val draft: List? = null, + @SerializedName("scheduled") val scheduled: List? = null, + @SerializedName("error") val error: String? = null ) { fun toPosts() = PostsCardModel( - hasPublished = hasPublished, - draft = draft.map { it.toPost() }, - scheduled = scheduled.map { it.toPost() } + hasPublished = hasPublished ?: false, + draft = draft?.map { it.toPost() } ?: emptyList(), + scheduled = scheduled?.map { it.toPost() } ?: emptyList(), + error = error?.let { toPostCardError(it) } ) + + private fun toPostCardError(error: String): PostCardError { + val errorType = when (error) { + UNAUTHORIZED -> PostCardErrorType.UNAUTHORIZED + else -> PostCardErrorType.GENERIC_ERROR + } + return PostCardError(errorType, error) + } } data class PostResponse( @@ -106,6 +132,9 @@ class CardsRestClient @Inject constructor( companion object { private const val CARDS = "cards" + private const val JETPACK_DISCONNECTED = "jetpack_disconnected" + private const val JETPACK_DISABLED = "jetpack_disabled" + private const val UNAUTHORIZED = "unauthorized" } } diff --git a/fluxc/src/main/java/org/wordpress/android/fluxc/store/dashboard/CardsStore.kt b/fluxc/src/main/java/org/wordpress/android/fluxc/store/dashboard/CardsStore.kt index efcb9e4c5c..0cce72da6d 100644 --- a/fluxc/src/main/java/org/wordpress/android/fluxc/store/dashboard/CardsStore.kt +++ b/fluxc/src/main/java/org/wordpress/android/fluxc/store/dashboard/CardsStore.kt @@ -87,6 +87,28 @@ class CardsStore @Inject constructor( /* ERRORS */ + enum class TodaysStatsCardErrorType { + JETPACK_DISCONNECTED, + JETPACK_DISABLED, + UNAUTHORIZED, + GENERIC_ERROR + } + + class TodaysStatsCardError( + val type: TodaysStatsCardErrorType, + val message: String? = null + ) : OnChangedError + + enum class PostCardErrorType { + UNAUTHORIZED, + GENERIC_ERROR + } + + class PostCardError( + val type: PostCardErrorType, + val message: String? = null + ) : OnChangedError + enum class CardsErrorType { GENERIC_ERROR, AUTHORIZATION_REQUIRED,