diff --git a/.changeset/friendly-cats-beg.md b/.changeset/friendly-cats-beg.md new file mode 100644 index 0000000..ccdc70f --- /dev/null +++ b/.changeset/friendly-cats-beg.md @@ -0,0 +1,8 @@ +--- +"magicbell-android": major +--- + +Updated topic and category filtering APIs in `StorePredicate` + +Previously one was able to filter for multiple topics and categories, but this functionality is now deprecated in favor of only filtering for a single topic and category. +Please reach out to us via the [Community](http://www.magicbell.com/community) if you need the previous functionality. diff --git a/.changeset/spotty-eels-hope.md b/.changeset/spotty-eels-hope.md new file mode 100644 index 0000000..c17d9b9 --- /dev/null +++ b/.changeset/spotty-eels-hope.md @@ -0,0 +1,5 @@ +--- +"magicbell-android": minor +--- + +Fixed a bug where a topic filter value would be passed as a category diff --git a/README.md b/README.md index 21a3ca5..86d4416 100644 --- a/README.md +++ b/README.md @@ -250,9 +250,9 @@ val unreadNotifications = user.store.build(read = false) val archivedNotifications = user.store.build(archived = true) -val billingNotifications = user.store.build(categories = ["billing"]) +val billingNotifications = user.store.build(category = "billing") -val firstOrderNotifications = user.store.build(topics = ["order:001"]) +val firstOrderNotifications = user.store.build(topic = "order:001") ``` These are the attributes of a notification store: @@ -328,16 +328,16 @@ These are the available options: | Param | Options | Default | Description | | ------------ | ---------------------------------- | -------------- | ------------------------------ | -| `read` | `true`, `false`, `nil` | `nil` | Filter by the `read` state (`nil` means unspecified) | -| `seen` | `true`, `false`, `nil` | `nil` | Filter by the `seen` state (`nil` means unspecified) | -| `archived` | `true`, `false` | `false` | Filter by the `archived` state | -| `categories` | `[String]` | `[]` | Filter by categories | -| `topics` | `[String]` | `[]` | Filter by topics | +| `read` | `true`, `false`, `null` | `null` | Filter by the `read` state (`null` means unspecified) | +| `seen` | `true`, `false`, `null` | `null` | Filter by the `seen` state (`null` means unspecified) | +| `archived` | `true`, `false` | `false` | Filter by the `archived` state | +| `category` | `String` | `null` | Filter by category | +| `topic` | `String` | `null` | Filter by topic | For example, use this predicate to fetch unread notifications of the `"important"` category: ```kotlin -val predicate = StorePredicate(read = true, categories = ["important"]) +val predicate = StorePredicate(read = true, category = "important") val store = user.store.build(predicate) ``` @@ -392,7 +392,7 @@ for (notification in store) { } // As an array -val notifications = store.notifications() +val notifications = store.notifications ``` Enumeration is also available: diff --git a/sdk/src/main/assets/NotificationFragment b/sdk/src/main/assets/NotificationFragment deleted file mode 100644 index 11a96ac..0000000 --- a/sdk/src/main/assets/NotificationFragment +++ /dev/null @@ -1,27 +0,0 @@ -fragment notification on NotificationsConnection { - edges { - cursor - node { - id - title - content - actionUrl - archivedAt - category - topic - customAttributes - readAt - seenAt - sentAt - } - } - pageInfo { - endCursor - hasNextPage - hasPreviousPage - startCursor - } - totalCount - unreadCount - unseenCount -} diff --git a/sdk/src/main/java/com/magicbell/sdk/SDKProvider.kt b/sdk/src/main/java/com/magicbell/sdk/SDKProvider.kt index d405cd7..0aa9c7a 100644 --- a/sdk/src/main/java/com/magicbell/sdk/SDKProvider.kt +++ b/sdk/src/main/java/com/magicbell/sdk/SDKProvider.kt @@ -85,7 +85,6 @@ internal class DefaultSDKModule( coroutinesComponent.coroutineDispatcher, coroutineScope, MainThreadExecutor(Handler(Looper.getMainLooper())), - context, notificationComponent, storeRealTimeComponent, configComponent diff --git a/sdk/src/main/java/com/magicbell/sdk/common/network/HttpClient.kt b/sdk/src/main/java/com/magicbell/sdk/common/network/HttpClient.kt index f8afa85..cf27ad6 100644 --- a/sdk/src/main/java/com/magicbell/sdk/common/network/HttpClient.kt +++ b/sdk/src/main/java/com/magicbell/sdk/common/network/HttpClient.kt @@ -4,28 +4,31 @@ import com.magicbell.sdk.common.environment.Environment import com.magicbell.sdk.common.error.NetworkErrorEntity import com.magicbell.sdk.common.error.NetworkException import kotlinx.serialization.json.Json +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody typealias HttpHeaders = Array typealias HeaderNameAndValue = Pair +typealias QueryParameters = List> + internal interface HttpClient { fun prepareRequest( path: String, externalId: String?, email: String?, hmac: String?, - httpMethod: HttpMethod = HttpMethod.Get, + httpMethod: HttpMethod = HttpMethod.Get(), additionalHeaders: HttpHeaders = emptyArray() ): Request suspend fun performRequest(request: Request): String? - sealed class HttpMethod(val name: String, val body: String? = null) { - object Get: HttpMethod("GET") - class Post(body: String = ""): HttpMethod("POST", body) - class Put(body: String = ""): HttpMethod("PUT", body) + sealed class HttpMethod(val name: String, val queryParams: QueryParameters? = null, val body: String? = null) { + class Get(queryParams: QueryParameters = listOf()) : HttpMethod("GET", queryParams = queryParams) + class Post(body: String = ""): HttpMethod("POST", body = body) + class Put(body: String = ""): HttpMethod("PUT", body = body) object Delete: HttpMethod("DELETE") } } @@ -37,8 +40,16 @@ internal class DefaultHttpClient( ) : HttpClient { override fun prepareRequest(path: String, externalId: String?, email: String?, hmac: String?, httpMethod: HttpClient.HttpMethod, additionalHeaders: HttpHeaders): Request { + val urlBuilder = "${environment.baseUrl}/$path".toHttpUrlOrNull()!!.newBuilder() + + httpMethod.queryParams?.let { + for ((key, value) in it) { + urlBuilder.addQueryParameter(key, value) + } + } + val request = Request.Builder() - .url("${environment.baseUrl}/$path") + .url(urlBuilder.build()) .addHeader("X-MAGICBELL-API-KEY", environment.apiKey) hmac?.also { diff --git a/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/CursorPredicate.kt b/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/CursorPredicate.kt deleted file mode 100644 index 3357452..0000000 --- a/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/CursorPredicate.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.magicbell.sdk.common.network.graphql - -internal class CursorPredicate(val cursor: Cursor = Cursor.Unspecified, val size: Int? = null) : GraphQLRepresentable { - sealed class Cursor { - class Next(val value: String) : Cursor() - class Previous(val value: String) : Cursor() - object Unspecified : Cursor() - } - - override val graphQLValue: String - get() { - val cursorParams = mutableListOf() - - when (cursor) { - is Cursor.Next -> { - cursorParams.add("after: \"${cursor.value}\"") - } - is Cursor.Previous -> { - cursorParams.add("before: \"${cursor.value}\"") - } - Cursor.Unspecified -> { - // Do nothing - } - } - - size?.also { - cursorParams.add("first: $it") - } - - return cursorParams.joinToString(", ") - } -} \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/Edge.kt b/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/Edge.kt deleted file mode 100644 index 38667dc..0000000 --- a/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/Edge.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.magicbell.sdk.common.network.graphql - -import kotlinx.serialization.Serializable - -@Serializable -internal class Edge(val cursor: String, var node: T) \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/GraphQLFragment.kt b/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/GraphQLFragment.kt deleted file mode 100644 index 933bd42..0000000 --- a/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/GraphQLFragment.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.magicbell.sdk.common.network.graphql - -import android.content.Context - -internal class GraphQLFragment(filename: String, context: Context) : GraphQLRepresentable { - override val graphQLValue: String = context.assets.open(filename).bufferedReader().use { it.readText().replace("\n", "") } -} \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/GraphQLRepresentable.kt b/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/GraphQLRepresentable.kt deleted file mode 100644 index b75e749..0000000 --- a/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/GraphQLRepresentable.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.magicbell.sdk.common.network.graphql - -interface GraphQLRepresentable { - val graphQLValue: String -} \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/GraphQLRequest.kt b/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/GraphQLRequest.kt deleted file mode 100644 index d1881fa..0000000 --- a/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/GraphQLRequest.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.magicbell.sdk.common.network.graphql - -import kotlinx.serialization.Serializable - - -@Serializable -internal class GraphQLRequestEntity( - val query: String, -) - -internal class GraphQLRequest( - private val fragment: GraphQLFragment, - private val predicates: List, -) : GraphQLRepresentable { - - constructor(fragment: GraphQLFragment, predicate: GraphQLRepresentable) : this(fragment, listOf(predicate)) - - override val graphQLValue: String - get() { - var query = "query {" - query += predicates.joinToString("\n ") { it.graphQLValue } - query += "} " - query += fragment.graphQLValue - return query - } -} \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/GraphQLResponse.kt b/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/GraphQLResponse.kt deleted file mode 100644 index ca48c31..0000000 --- a/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/GraphQLResponse.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.magicbell.sdk.common.network.graphql - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal class GraphQLResponse( - @SerialName("data") - val response: Map, -) \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/PageInfo.kt b/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/PageInfo.kt deleted file mode 100644 index 01c2a05..0000000 --- a/sdk/src/main/java/com/magicbell/sdk/common/network/graphql/PageInfo.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.magicbell.sdk.common.network.graphql - -import kotlinx.serialization.Serializable - -@Serializable -internal class PageInfo( - val endCursor: String? = null, - val hasNextPage: Boolean, - val hasPreviousPage: Boolean, - val startCursor: String? = null, -) \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/notificationpreferences/data/NotificationPreferencesNetworkDataSource.kt b/sdk/src/main/java/com/magicbell/sdk/feature/notificationpreferences/data/NotificationPreferencesNetworkDataSource.kt index 026cffa..094184d 100644 --- a/sdk/src/main/java/com/magicbell/sdk/feature/notificationpreferences/data/NotificationPreferencesNetworkDataSource.kt +++ b/sdk/src/main/java/com/magicbell/sdk/feature/notificationpreferences/data/NotificationPreferencesNetworkDataSource.kt @@ -23,7 +23,7 @@ internal class NotificationPreferencesNetworkDataSource( query.externalId, query.email, query.hmac, - HttpClient.HttpMethod.Get, + HttpClient.HttpMethod.Get(), arrayOf(Pair("accept-version", "v2")) ) diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/NotificationStore.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/NotificationStore.kt index f4be798..386d8a5 100644 --- a/sdk/src/main/java/com/magicbell/sdk/feature/store/NotificationStore.kt +++ b/sdk/src/main/java/com/magicbell/sdk/feature/store/NotificationStore.kt @@ -2,9 +2,6 @@ package com.magicbell.sdk.feature.store import androidx.annotation.VisibleForTesting import com.magicbell.sdk.common.error.MagicBellError -import com.magicbell.sdk.common.network.graphql.CursorPredicate -import com.magicbell.sdk.common.network.graphql.CursorPredicate.Cursor.Next -import com.magicbell.sdk.common.network.graphql.Edge import com.magicbell.sdk.common.query.UserQuery import com.magicbell.sdk.common.threading.MainThread import com.magicbell.sdk.feature.notification.Notification @@ -49,28 +46,28 @@ class NotificationStore internal constructor( } override fun notifyDeleteNotification(id: String) { - val notificationIndex = edges.indexOfFirst { it.node.id == id } + val notificationIndex = notifications.indexOfFirst { it.id == id } if (notificationIndex != -1) { - updateCountersWhenDelete(edges[notificationIndex].node, predicate) - edges.removeAt(notificationIndex) + updateCountersWhenDelete(notifications[notificationIndex], predicate) + (notifications as MutableList).removeAt(notificationIndex) notifyObserversDeleted(listOf(notificationIndex)) } } override fun notifyNotificationChange(id: String, change: StoreRealTimeNotificationChange) { - val notificationIndex = edges.indexOfFirst { it.node.id == id } + val notificationIndex = notifications.indexOfFirst { it.id == id } if (notificationIndex != -1) { - val notification = edges[notificationIndex].node + val notification = notifications[notificationIndex] when (change) { StoreRealTimeNotificationChange.READ -> markNotificationAsRead(notification, predicate) StoreRealTimeNotificationChange.UNREAD -> markNotificationAsUnread(notification, predicate) StoreRealTimeNotificationChange.ARCHIVED -> archiveNotification(notification, predicate) } if (predicate.match(notification)) { - edges[notificationIndex].node = notification + (notifications as MutableList)[notificationIndex] = notification notifyObserversChanged(listOf(notificationIndex)) } else { - edges.removeAt(notificationIndex) + (notifications as MutableList).removeAt(notificationIndex) notifyObserversDeleted(listOf(notificationIndex)) } } else { @@ -107,8 +104,6 @@ class NotificationStore internal constructor( private val pageSize = 20 - private val edges: MutableList> = mutableListOf() - var totalCount: Int = 0 private set var unreadCount: Int = 0 @@ -118,7 +113,7 @@ class NotificationStore internal constructor( var hasNextPage: Boolean = true private set - private var nextPageCursor: String? = null + private var nextPage: Int = 1 private val mutableContentFlow = MutableSharedFlow() @@ -182,45 +177,40 @@ class NotificationStore internal constructor( } /** - * Number of notifications loaded in the store + * Returns a list containing all notifications */ - val count: Int = edges.count() + val notifications: List = mutableListOf() /** - * Returns a list containing all notifications + * Number of notifications loaded in the store */ - val notifications: List - get() { - return edges.map { - it.node - } - } + val count: Int = notifications.count() override val size: Int get() { - return edges.size + return notifications.size } override fun get(index: Int): Notification { - return edges[index].node + return notifications[index] } override fun contains(element: Notification): Boolean { - return edges.firstOrNull { it.node.id == element.id } != null + return notifications.firstOrNull { it.id == element.id } != null } override fun containsAll(elements: Collection): Boolean = notifications.containsAll(elements) - override fun isEmpty(): Boolean = edges.isEmpty() + override fun isEmpty(): Boolean = notifications.isEmpty() override fun iterator(): Iterator = notifications.iterator() override fun indexOf(element: Notification): Int { - return edges.indexOfFirst { it.node.id == element.id } + return notifications.indexOfFirst { it.id == element.id } } override fun lastIndexOf(element: Notification): Int { - return edges.indexOfLast { it.node.id == element.id } + return notifications.indexOfLast { it.id == element.id } } override fun listIterator(): ListIterator = notifications.listIterator() @@ -228,7 +218,7 @@ class NotificationStore internal constructor( override fun listIterator(index: Int): ListIterator = notifications.listIterator(index) override fun subList(fromIndex: Int, toIndex: Int): List { - return edges.subList(fromIndex, toIndex).map { it.node } + return notifications.subList(fromIndex, toIndex) } //region Observers @@ -328,16 +318,15 @@ class NotificationStore internal constructor( suspend fun refresh(): Result> { return runCatching { withContext(coroutineContext) { - val cursorPredicate = CursorPredicate(size = pageSize) - val storePage = fetchStorePageInteractor(predicate, cursorPredicate, userQuery) + val storePagePredicate = StorePagePredicate(1, pageSize) + val storePage = fetchStorePageInteractor(predicate, storePagePredicate, userQuery) clear(false) configurePagination(storePage) configureCount(storePage) - val newEdges = storePage.edges - edges.addAll(newEdges) - val notifications = newEdges.map { it.node } + val newNotifications = storePage.notifications + (notifications as MutableList).addAll(newNotifications) notifyObserversReloadStore() - notifications + newNotifications } } } @@ -354,23 +343,17 @@ class NotificationStore internal constructor( if (!hasNextPage) { return@withContext listOf() } - val cursorPredicate: CursorPredicate = nextPageCursor?.let { after -> - CursorPredicate(Next(after), pageSize) - } ?: run { - CursorPredicate(size = pageSize) - } - - val storePage = fetchStorePageInteractor(predicate, cursorPredicate, userQuery) + val storePagePredicate = StorePagePredicate(nextPage, pageSize) + val storePage = fetchStorePageInteractor(predicate, storePagePredicate, userQuery) configurePagination(storePage) configureCount(storePage) - val oldCount = edges.count() - val newEdges = storePage.edges - edges.addAll(newEdges) - val notifications = newEdges.map { it.node } - val indexes = oldCount until edges.size + val oldCount = notifications.count() + val newNotifications = storePage.notifications + (notifications as MutableList).addAll(newNotifications) + val indexes = oldCount until notifications.size notifyObserversInserted(indexes.toList()) - notifications + newNotifications } } } @@ -385,10 +368,10 @@ class NotificationStore internal constructor( return runCatching { withContext(coroutineContext) { deleteNotificationInteractor(notification.id, userQuery) - val notificationIndex = edges.indexOfFirst { it.node.id == notification.id } + val notificationIndex = notifications.indexOfFirst { it.id == notification.id } if (notificationIndex != -1) { - updateCountersWhenDelete(edges[notificationIndex].node, predicate) - edges.removeAt(notificationIndex) + updateCountersWhenDelete(notifications[notificationIndex], predicate) + (notifications as MutableList).removeAt(notificationIndex) notifyObserversDeleted(listOf(notificationIndex)) } } @@ -405,8 +388,8 @@ class NotificationStore internal constructor( return executeNotificationAction( notification, MARK_AS_READ, - modifications = { notification -> - markNotificationAsRead(notification, predicate) + modifications = { + markNotificationAsRead(it, predicate) }) } @@ -420,8 +403,8 @@ class NotificationStore internal constructor( return executeNotificationAction( notification, MARK_AS_UNREAD, - modifications = { notification -> - markNotificationAsUnread(notification, predicate) + modifications = { + markNotificationAsUnread(it, predicate) }) } @@ -435,8 +418,8 @@ class NotificationStore internal constructor( return executeNotificationAction( notification, ARCHIVE, - modifications = { notification -> - archiveNotification(notification, predicate) + modifications = { + archiveNotification(it, predicate) }) } @@ -450,8 +433,8 @@ class NotificationStore internal constructor( return executeNotificationAction( notification, UNARCHIVE, - modifications = { notification -> - notification.archivedAt = null + modifications = { + it.archivedAt = null }) } @@ -463,8 +446,8 @@ class NotificationStore internal constructor( suspend fun markAllNotificationAsRead(): Result { return executeAllNotificationsAction( MARK_ALL_AS_READ, - modifications = { notification -> - markNotificationAsRead(notification, predicate) + modifications = { + markNotificationAsRead(it, predicate) }) } @@ -476,9 +459,9 @@ class NotificationStore internal constructor( suspend fun markAllNotificationAsSeen(): Result { return executeAllNotificationsAction( MARK_ALL_AS_SEEN, - modifications = { notification -> - if (notification.seenAt == null) { - notification.seenAt = Date() + modifications = { + if (it.seenAt == null) { + it.seenAt = Date() unseenCount -= 1 } }) @@ -488,11 +471,11 @@ class NotificationStore internal constructor( //region NotificationActions private fun clear(notifyChanges: Boolean) { val notificationCount = size - edges.clear() + (notifications as MutableList).clear() setTotalCount(0, notifyChanges) setUnreadCount(0, notifyChanges) setUnseenCount(0, notifyChanges) - nextPageCursor = null + nextPage = 1 setHasNextPage(true) if (notifyChanges) { val indexes = 0 until notificationCount @@ -508,7 +491,7 @@ class NotificationStore internal constructor( return runCatching { withContext(coroutineContext) { actionNotificationInteractor(action, notification.id, userQuery) - val notificationIndex = edges.indexOfFirst { it.node.id == notification.id } + val notificationIndex = notifications.indexOfFirst { it.id == notification.id } if (notificationIndex != -1) { modifications(notification) notification @@ -526,17 +509,16 @@ class NotificationStore internal constructor( return runCatching { withContext(coroutineContext) { actionNotificationInteractor(action, userQuery = userQuery) - for (i in edges.indices) { - modifications(edges[i].node) + for (i in notifications.indices) { + modifications(notifications[i]) } } } } private fun configurePagination(storePage: StorePage) { - val pageInfo = storePage.pageInfo - nextPageCursor = pageInfo.endCursor - setHasNextPage(pageInfo.hasNextPage) + nextPage = storePage.currentPage + 1 + setHasNextPage(storePage.currentPage < storePage.totalPages) } private fun configureCount(storePage: StorePage) { diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/NotificationValidator.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/NotificationValidator.kt index 5a783f9..466b9f1 100644 --- a/sdk/src/main/java/com/magicbell/sdk/feature/store/NotificationValidator.kt +++ b/sdk/src/main/java/com/magicbell/sdk/feature/store/NotificationValidator.kt @@ -41,22 +41,12 @@ class NotificationValidator(private val storePredicate: StorePredicate) { } private fun validateCategory(notification: Notification): Boolean { - return if (storePredicate.categories.isEmpty()) { - true - } else if (notification.category != null) { - storePredicate.categories.contains(notification.category) - } else { - false - } + return storePredicate.category == null || + storePredicate.category == notification.category } private fun validateTopic(notification: Notification): Boolean { - return if (storePredicate.topics.isEmpty()) { - true - } else if (notification.topic != null) { - storePredicate.topics.contains(notification.topic) - } else { - false - } + return storePredicate.topic == null || + storePredicate.topic == notification.topic } } diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/StoreContext.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/StoreContext.kt index b3c51b7..d7cf138 100644 --- a/sdk/src/main/java/com/magicbell/sdk/feature/store/StoreContext.kt +++ b/sdk/src/main/java/com/magicbell/sdk/feature/store/StoreContext.kt @@ -1,18 +1,6 @@ package com.magicbell.sdk.feature.store -import com.magicbell.sdk.common.network.graphql.CursorPredicate -import com.magicbell.sdk.common.network.graphql.GraphQLRepresentable - internal class StoreContext( - val name: String, val storePredicate: StorePredicate, - val cursorPredicate: CursorPredicate, -) : GraphQLRepresentable { - override val graphQLValue: String - get() { - val storePredicateString = storePredicate.graphQLValue - val cursorPredicateString = cursorPredicate.graphQLValue - - return " $name: notifications ($storePredicateString, $cursorPredicateString) { ...notification }" - } -} \ No newline at end of file + val storePagePredicate: StorePagePredicate, +) \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/StoreDirector.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/StoreDirector.kt index 2da418a..98b6565 100644 --- a/sdk/src/main/java/com/magicbell/sdk/feature/store/StoreDirector.kt +++ b/sdk/src/main/java/com/magicbell/sdk/feature/store/StoreDirector.kt @@ -129,19 +129,19 @@ fun StoreDirector.forRead(): NotificationStore { } /** - * Return the store for notifications with the given categories + * Return the store for notifications with the given category * - * @param categories The list of categories + * @param category The category */ -fun StoreDirector.forCategories(categories: List): NotificationStore { - return build(StorePredicate(categories = categories)) +fun StoreDirector.forCategory(category: String): NotificationStore { + return build(StorePredicate(category = category)) } /** - * Return the store for notifications with the given topics + * Return the store for notifications with the given topic * - * @param topics The list of topics + * @param topic The topic */ -fun StoreDirector.forTopics(topics: List): NotificationStore { - return build(StorePredicate(topics = topics)) +fun StoreDirector.forTopics(topic: String): NotificationStore { + return build(StorePredicate(topic = topic)) } \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/StorePage.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/StorePage.kt index b3eecef..8b58932 100644 --- a/sdk/src/main/java/com/magicbell/sdk/feature/store/StorePage.kt +++ b/sdk/src/main/java/com/magicbell/sdk/feature/store/StorePage.kt @@ -1,15 +1,22 @@ package com.magicbell.sdk.feature.store -import com.magicbell.sdk.common.network.graphql.Edge -import com.magicbell.sdk.common.network.graphql.PageInfo import com.magicbell.sdk.feature.notification.Notification import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames @Serializable internal class StorePage( - val edges: List>, - val pageInfo: PageInfo, + val notifications: List, + @JsonNames("total") val totalCount: Int, + @JsonNames("unread_count") val unreadCount: Int, + @JsonNames("unseen_count") val unseenCount: Int, + @JsonNames("total_pages") + val totalPages: Int, + @JsonNames("per_page") + val perPage: Int, + @JsonNames("current_page") + val currentPage: Int ) diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/StorePagePredicate.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/StorePagePredicate.kt new file mode 100644 index 0000000..26abb1d --- /dev/null +++ b/sdk/src/main/java/com/magicbell/sdk/feature/store/StorePagePredicate.kt @@ -0,0 +1,6 @@ +package com.magicbell.sdk.feature.store + +internal data class StorePagePredicate( + val page: Int, + val size: Int, +) \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/StorePredicate.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/StorePredicate.kt index 979fb75..2077c25 100644 --- a/sdk/src/main/java/com/magicbell/sdk/feature/store/StorePredicate.kt +++ b/sdk/src/main/java/com/magicbell/sdk/feature/store/StorePredicate.kt @@ -1,6 +1,5 @@ package com.magicbell.sdk.feature.store -import com.magicbell.sdk.common.network.graphql.GraphQLRepresentable import com.magicbell.sdk.feature.notification.Notification /** @@ -8,53 +7,16 @@ import com.magicbell.sdk.feature.notification.Notification * @param read: The read status. Defaults to null, no filter. * @param seen: The seen status. Defaults to null, no filter. * @param archived: The archived status. Defaults to false, not archived notifications. - * @param categories: The list of categories. Defaults to empty array. - * @param topics: The list of topics. Defaults to empty array. + * @param category: The category. Defaults to null. + * @param topic: The topic. Defaults to null. */ data class StorePredicate( val read: Boolean? = null, val seen: Boolean? = null, val archived: Boolean = false, - val categories: List = listOf(), - val topics: List = listOf(), -) : GraphQLRepresentable { - override val graphQLValue: String - get() { - val storePredicateParams = mutableListOf() - - read?.also { - if (it) { - storePredicateParams.add("read: true") - } else { - storePredicateParams.add("read: false") - } - } - - seen?.also { - if (it) { - storePredicateParams.add("seen: true") - } else { - storePredicateParams.add("seen: false") - } - } - - if (archived) { - storePredicateParams.add("archived: true") - } else { - storePredicateParams.add("archived: false") - } - - if (categories.isNotEmpty()) { - storePredicateParams.add("categories:[${categories.joinToString(", ") { "\"$it\"" }}]") - } - - if (topics.isNotEmpty()) { - storePredicateParams.add("topics:[${categories.joinToString(", ") { "\"$it\"" }}]") - } - - return storePredicateParams.joinToString(", ") - } -} + val category: String? = null, + val topic: String? = null, +) internal fun StorePredicate.match(notification: Notification): Boolean { val validator = NotificationValidator(this) diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/StoreProvider.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/StoreProvider.kt index 48a0568..b0c6b93 100644 --- a/sdk/src/main/java/com/magicbell/sdk/feature/store/StoreProvider.kt +++ b/sdk/src/main/java/com/magicbell/sdk/feature/store/StoreProvider.kt @@ -1,17 +1,13 @@ package com.magicbell.sdk.feature.store -import android.content.Context import com.magicbell.sdk.common.network.HttpClient -import com.magicbell.sdk.common.network.graphql.GraphQLRequestEntity -import com.magicbell.sdk.common.network.graphql.GraphQLResponse import com.magicbell.sdk.common.query.UserQuery import com.magicbell.sdk.common.threading.MainThread import com.magicbell.sdk.feature.config.ConfigComponent import com.magicbell.sdk.feature.notification.NotificationComponent import com.magicbell.sdk.feature.realtime.StoreRealTimeComponent -import com.magicbell.sdk.feature.store.data.GraphQLRequestToGraphQLEntityMapper -import com.magicbell.sdk.feature.store.data.GraphQLResponseToStorePageMapper -import com.magicbell.sdk.feature.store.data.StoresGraphQLNetworkDataSource +import com.magicbell.sdk.feature.store.data.StoreNetworkDataSource +import com.magicbell.sdk.feature.store.data.StoreResponseToStorePageMapper import com.magicbell.sdk.feature.store.interactor.FetchStorePageDefaultInteractor import com.magicbell.sdk.feature.store.interactor.FetchStorePageInteractor import com.magicbell.sdk.feature.store.interactor.GetStorePagesInteractor @@ -31,25 +27,22 @@ internal class DefaultStoreModule( private val coroutineContext: CoroutineContext, private val coroutineScope: CoroutineScope, private val mainThread: MainThread, - private val context: Context, private val notificationComponent: NotificationComponent, private val storeRealTimeComponent: StoreRealTimeComponent, private val configComponent: ConfigComponent, ) : StoreComponent { - private val storeNotificationGraphQLRepository by lazy { + private val storeNotificationRepository by lazy { SingleGetDataSourceRepository( - StoresGraphQLNetworkDataSource( + StoreNetworkDataSource( httpClient, - context, - GraphQLRequestToGraphQLEntityMapper(GraphQLRequestEntity.serializer(), json), - GraphQLResponseToStorePageMapper(GraphQLResponse.serializer(StorePage.serializer()), json) + StoreResponseToStorePageMapper(StorePage.serializer(), json) ) ) } private fun getStorePagesInteractor(): GetStorePagesInteractor { - return GetStorePagesInteractor(coroutineContext, storeNotificationGraphQLRepository.toGetInteractor(coroutineContext)) + return GetStorePagesInteractor(coroutineContext, storeNotificationRepository.toGetInteractor(coroutineContext)) } private fun getFetchStorePageInteractor(): FetchStorePageInteractor { diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/data/GraphQLRequestToGraphQLEntityMapper.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/data/GraphQLRequestToGraphQLEntityMapper.kt deleted file mode 100644 index cee6917..0000000 --- a/sdk/src/main/java/com/magicbell/sdk/feature/store/data/GraphQLRequestToGraphQLEntityMapper.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.magicbell.sdk.feature.store.data - -import com.mobilejazz.harmony.data.mapper.Mapper -import com.magicbell.sdk.common.network.graphql.GraphQLRequest -import com.magicbell.sdk.common.network.graphql.GraphQLRequestEntity -import kotlinx.serialization.KSerializer -import kotlinx.serialization.json.Json - -internal class GraphQLRequestToGraphQLEntityMapper( - val serializer: KSerializer, - val json: Json, -) : Mapper { - override fun map(from: GraphQLRequest): String { - val graphQLRequestEntity = GraphQLRequestEntity(from.graphQLValue) - return json.encodeToString(serializer, graphQLRequestEntity) - } -} diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/data/GraphQLResponseToStorePageMapper.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/data/GraphQLResponseToStorePageMapper.kt deleted file mode 100644 index 6a10345..0000000 --- a/sdk/src/main/java/com/magicbell/sdk/feature/store/data/GraphQLResponseToStorePageMapper.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.magicbell.sdk.feature.store.data - -import com.mobilejazz.harmony.data.mapper.Mapper -import com.magicbell.sdk.common.network.graphql.GraphQLResponse -import com.magicbell.sdk.feature.store.StorePage -import kotlinx.serialization.KSerializer -import kotlinx.serialization.json.Json - -internal class GraphQLResponseToStorePageMapper( - private val serializer: KSerializer>, - private val json: Json, -) : Mapper> { - override fun map(from: String): Map { - val graphQLResponse = json.decodeFromString(serializer, from) - return graphQLResponse.response - } -} \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoreNetworkDataSource.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoreNetworkDataSource.kt new file mode 100644 index 0000000..607cc5e --- /dev/null +++ b/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoreNetworkDataSource.kt @@ -0,0 +1,37 @@ +package com.magicbell.sdk.feature.store.data + +import com.mobilejazz.harmony.data.datasource.GetDataSource +import com.mobilejazz.harmony.data.error.OperationNotAllowedException +import com.mobilejazz.harmony.data.query.Query +import com.magicbell.sdk.common.error.MappingException +import com.magicbell.sdk.common.network.HttpClient +import com.magicbell.sdk.feature.store.StorePage + +internal class StoreNetworkDataSource( + private val httpClient: HttpClient, + private val outMapper: StoreResponseToStorePageMapper, +) : GetDataSource { + override suspend fun get(query: Query): StorePage { + return when (query) { + is StoreQuery -> { + + val request = httpClient.prepareRequest( + "notifications", + query.userQuery.externalId, + query.userQuery.email, + query.userQuery.hmac, + HttpClient.HttpMethod.Get(query.context.asQueryParameters()) + ) + + httpClient.performRequest(request)?.let { + outMapper.map(it) + } ?: run { + throw MappingException(StoreResponseToStorePageMapper::class.java.name) + } + } + else -> throw OperationNotAllowedException() + } + } + + override suspend fun getAll(query: Query): List = throw NotImplementedError() +} \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoreQuery.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoreQuery.kt index d6629f6..4a1aab4 100644 --- a/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoreQuery.kt +++ b/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoreQuery.kt @@ -1,26 +1,19 @@ package com.magicbell.sdk.feature.store.data import com.mobilejazz.harmony.data.query.Query -import com.magicbell.sdk.common.network.graphql.CursorPredicate -import com.magicbell.sdk.common.network.graphql.GraphQLRepresentable import com.magicbell.sdk.common.query.UserQuery import com.magicbell.sdk.feature.store.StoreContext +import com.magicbell.sdk.feature.store.StorePagePredicate import com.magicbell.sdk.feature.store.StorePredicate internal class StoreQuery( - val contexts: List, + val context: StoreContext, val userQuery: UserQuery, -) : Query(), GraphQLRepresentable { +) : Query() { constructor( - name: String, storePredicate: StorePredicate, - cursorPredicate: CursorPredicate, + storePagePredicate: StorePagePredicate, userQuery: UserQuery, - ) : this(listOf(StoreContext(name, storePredicate, cursorPredicate)), userQuery) - - override val graphQLValue: String - get() { - return contexts.joinToString("\n") { it.graphQLValue } - } + ) : this(StoreContext(storePredicate, storePagePredicate), userQuery) } \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoreQueryParameter.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoreQueryParameter.kt new file mode 100644 index 0000000..ecc122c --- /dev/null +++ b/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoreQueryParameter.kt @@ -0,0 +1,32 @@ +package com.magicbell.sdk.feature.store.data + +import com.magicbell.sdk.common.network.QueryParameters +import com.magicbell.sdk.feature.store.StoreContext +import com.magicbell.sdk.feature.store.StorePagePredicate +import com.magicbell.sdk.feature.store.StorePredicate + +internal fun StoreContext.asQueryParameters(): QueryParameters { + return storePredicate.asQueryParameters() + storePagePredicate.asQueryParameters() +} + +private fun StorePredicate.asQueryParameters(): QueryParameters { + val result: MutableList> = mutableListOf() + + read?.let { result += "read" to "$it" } + seen?.let { result += "seen" to "$it" } + result += "archived" to "$archived" + + category?.let { result += "category" to it } + + topic?.let { result += "topic" to it } + + return result +} + +private fun StorePagePredicate.asQueryParameters(): QueryParameters { + return listOf( + "page" to "$page", + "per_page" to "$size", + ) +} + diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoreResponseToStorePageMapper.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoreResponseToStorePageMapper.kt new file mode 100644 index 0000000..2701525 --- /dev/null +++ b/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoreResponseToStorePageMapper.kt @@ -0,0 +1,16 @@ +package com.magicbell.sdk.feature.store.data + +import com.magicbell.sdk.feature.store.StorePage +import com.mobilejazz.harmony.data.mapper.Mapper +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json + +internal class StoreResponseToStorePageMapper( + private val storePageSerializer: KSerializer, + private val json: Json, +) : Mapper { + override fun map(from: String): StorePage { + val storePage = json.decodeFromString(storePageSerializer, from) + return storePage + } +} diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoresGraphQLNetworkDataSource.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoresGraphQLNetworkDataSource.kt deleted file mode 100644 index b408caf..0000000 --- a/sdk/src/main/java/com/magicbell/sdk/feature/store/data/StoresGraphQLNetworkDataSource.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.magicbell.sdk.feature.store.data - -import android.content.Context -import com.mobilejazz.harmony.data.datasource.GetDataSource -import com.mobilejazz.harmony.data.error.OperationNotAllowedException -import com.mobilejazz.harmony.data.query.Query -import com.magicbell.sdk.common.error.MappingException -import com.magicbell.sdk.common.network.HttpClient -import com.magicbell.sdk.common.network.graphql.GraphQLFragment -import com.magicbell.sdk.common.network.graphql.GraphQLRequest -import com.magicbell.sdk.common.network.graphql.GraphQLResponse -import com.magicbell.sdk.feature.store.StorePage - -internal class StoresGraphQLNetworkDataSource( - private val httpClient: HttpClient, - private val context: Context, - private val inMapper: GraphQLRequestToGraphQLEntityMapper, - private val outMapper: GraphQLResponseToStorePageMapper, -) : GetDataSource> { - override suspend fun get(query: Query): Map { - return when (query) { - is StoreQuery -> { - val graphQLRequest = GraphQLRequest( - GraphQLFragment("NotificationFragment", context), - query - ) - - val request = httpClient.prepareRequest( - "graphql", - query.userQuery.externalId, - query.userQuery.email, - query.userQuery.hmac, - HttpClient.HttpMethod.Post(inMapper.map(graphQLRequest)) - ) - - httpClient.performRequest(request)?.let { - outMapper.map(it) - } ?: run { - throw MappingException(GraphQLResponse::class.java.name) - } - } - else -> throw OperationNotAllowedException() - } - } - - override suspend fun getAll(query: Query): List> = throw NotImplementedError() -} \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/interactor/FetchStorePageInteractor.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/interactor/FetchStorePageInteractor.kt index 1561f99..b1aca7d 100644 --- a/sdk/src/main/java/com/magicbell/sdk/feature/store/interactor/FetchStorePageInteractor.kt +++ b/sdk/src/main/java/com/magicbell/sdk/feature/store/interactor/FetchStorePageInteractor.kt @@ -1,18 +1,16 @@ package com.magicbell.sdk.feature.store.interactor -import com.magicbell.sdk.common.network.graphql.CursorPredicate import com.magicbell.sdk.common.query.UserQuery import com.magicbell.sdk.feature.store.StorePage +import com.magicbell.sdk.feature.store.StorePagePredicate import com.magicbell.sdk.feature.store.StorePredicate -import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.coroutineContext internal interface FetchStorePageInteractor { suspend operator fun invoke( storePredicate: StorePredicate, - cursorPredicate: CursorPredicate, + storePagePredicate: StorePagePredicate, userQuery: UserQuery, ): StorePage } @@ -24,11 +22,11 @@ internal class FetchStorePageDefaultInteractor( override suspend operator fun invoke( storePredicate: StorePredicate, - cursorPredicate: CursorPredicate, + storePagePredicate: StorePagePredicate, userQuery: UserQuery, ): StorePage { return withContext(interactorCoroutineContext) { - getStorePagesInteractor(storePredicate, cursorPredicate, userQuery) + getStorePagesInteractor(storePredicate, storePagePredicate, userQuery) } } } \ No newline at end of file diff --git a/sdk/src/main/java/com/magicbell/sdk/feature/store/interactor/GetStorePagesInteractor.kt b/sdk/src/main/java/com/magicbell/sdk/feature/store/interactor/GetStorePagesInteractor.kt index 99f0031..b4bfa96 100644 --- a/sdk/src/main/java/com/magicbell/sdk/feature/store/interactor/GetStorePagesInteractor.kt +++ b/sdk/src/main/java/com/magicbell/sdk/feature/store/interactor/GetStorePagesInteractor.kt @@ -1,43 +1,28 @@ package com.magicbell.sdk.feature.store.interactor -import com.mobilejazz.harmony.domain.interactor.GetInteractor -import com.magicbell.sdk.common.error.MagicBellError -import com.magicbell.sdk.common.network.graphql.CursorPredicate import com.magicbell.sdk.common.query.UserQuery import com.magicbell.sdk.feature.store.StoreContext import com.magicbell.sdk.feature.store.StorePage +import com.magicbell.sdk.feature.store.StorePagePredicate import com.magicbell.sdk.feature.store.StorePredicate import com.magicbell.sdk.feature.store.data.StoreQuery +import com.mobilejazz.harmony.domain.interactor.GetInteractor import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext internal class GetStorePagesInteractor( private val coroutineContext: CoroutineContext, - private val getStoreNotificationInteractor: GetInteractor>, + private val getStoreNotificationInteractor: GetInteractor, ) { suspend operator fun invoke( storePredicate: StorePredicate, - cursorPredicate: CursorPredicate, + storePagePredicate: StorePagePredicate, userQuery: UserQuery, ): StorePage { + val context = StoreContext(storePredicate, storePagePredicate) return withContext(coroutineContext) { - val storePages = invoke(listOf(StoreContext("data", storePredicate, cursorPredicate)), userQuery) - if (storePages.containsKey("data")) { - storePages["data"]!! - } else { - throw MagicBellError("Server didn't response correct data") - } + getStoreNotificationInteractor(StoreQuery(context, userQuery)) } } - - suspend operator fun invoke( - contexts: List, - userQuery: UserQuery, - ): Map { - return withContext(coroutineContext) { - getStoreNotificationInteractor(StoreQuery(contexts, userQuery)) - } - } - } diff --git a/sdk/src/test/java/com/magicbell/sdk/common/network/DefaultHttpClientTests.kt b/sdk/src/test/java/com/magicbell/sdk/common/network/DefaultHttpClientTests.kt new file mode 100644 index 0000000..e19e2f8 --- /dev/null +++ b/sdk/src/test/java/com/magicbell/sdk/common/network/DefaultHttpClientTests.kt @@ -0,0 +1,43 @@ +package com.magicbell.sdk.common.network + +import com.magicbell.sdk.common.environment.Environment +import io.mockk.mockk +import kotlinx.serialization.json.Json +import okhttp3.OkHttpClient +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.net.URL + +internal class DefaultHttpClientTests { + + private val okHttpClient:OkHttpClient = mockk() + private val json: Json = mockk() + + @Test + fun test_prepareRequest_get_queryParameters_appendsSome() { + // GIVEN + val environment = Environment("api-key", URL("http://api.magicbell.test")) + val httpClient = DefaultHttpClient(environment, okHttpClient, json) + val get = HttpClient.HttpMethod.Get(listOf("key" to "value", "key2" to "value-2")) + + // WHEN + val request = httpClient.prepareRequest("the-pointy-end", null, null, null, get) + + // THEN + assertEquals(request.url.toString(), "http://api.magicbell.test/the-pointy-end?key=value&key2=value-2") + } + + @Test + fun test_prepareRequest_get_queryParameters_supportsMultipleValuesForSameKey() { + // GIVEN + val environment = Environment("api-key", URL("http://api.magicbell.test")) + val httpClient = DefaultHttpClient(environment, okHttpClient, json) + val get = HttpClient.HttpMethod.Get(listOf("key" to "value", "key" to "another-value")) + + // WHEN + val request = httpClient.prepareRequest("the-pointy-end", null, null, null, get) + + // THEN + assertEquals(request.url.toString(), "http://api.magicbell.test/the-pointy-end?key=value&key=another-value") + } +} \ No newline at end of file diff --git a/sdk/src/test/java/com/magicbell/sdk/common/network/NotificationEdgeMotherObject.kt b/sdk/src/test/java/com/magicbell/sdk/common/network/NotificationEdgeMotherObject.kt deleted file mode 100644 index d32812e..0000000 --- a/sdk/src/test/java/com/magicbell/sdk/common/network/NotificationEdgeMotherObject.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.magicbell.sdk.common.network - -import com.magicbell.sdk.common.network.graphql.Edge -import com.magicbell.sdk.feature.notification.Notification -import com.magicbell.sdk.feature.notification.motherobject.ForceProperty -import com.magicbell.sdk.feature.notification.motherobject.anyNotification -import com.magicbell.sdk.feature.store.StorePredicate - -sealed class AnyCursor(val value: String) { - object ANY : AnyCursor("any") - object FIRST_PAGE_CURSOR : AnyCursor("first-cursor") - object SECOND_PAGE_CURSOR : AnyCursor("second-cursor") -} - -internal fun anyNotificationEdgeArray( - predicate: StorePredicate, - size: Int, - forceNotificationProperty: ForceProperty -): List> { - return (0 until size).map { - anyNotificationEdge(predicate, it.toString(), forceNotificationProperty) - } -} - -internal fun anyNotificationEdge(predicate: StorePredicate, id: String?, forceProperty: ForceProperty): Edge { - return Edge(AnyCursor.ANY.value, anyNotification(predicate, id, forceProperty)) -} - -internal fun Edge.create(cursor: String, notification: Notification): Edge { - return Edge(cursor, notification) -} - -internal fun List>.totalCount(): Int = this.size - -internal fun List>.unreadCount(): Int = this.filter { !it.node.isRead }.size - -internal fun List>.unseenCount(): Int = this.filter { !it.node.isSeen }.size diff --git a/sdk/src/test/java/com/magicbell/sdk/common/network/NotificationMotherObject.kt b/sdk/src/test/java/com/magicbell/sdk/common/network/NotificationMotherObject.kt new file mode 100644 index 0000000..433227d --- /dev/null +++ b/sdk/src/test/java/com/magicbell/sdk/common/network/NotificationMotherObject.kt @@ -0,0 +1,22 @@ +package com.magicbell.sdk.common.network + +import com.magicbell.sdk.feature.notification.Notification +import com.magicbell.sdk.feature.notification.motherobject.ForceProperty +import com.magicbell.sdk.feature.notification.motherobject.anyNotification +import com.magicbell.sdk.feature.store.StorePredicate + +internal fun anyNotificationEdgeArray( + predicate: StorePredicate, + size: Int, + forceNotificationProperty: ForceProperty +): List { + return (0 until size).map { + anyNotification(predicate, it.toString(), forceNotificationProperty) + } +} + +internal fun List.totalCount(): Int = this.size + +internal fun List.unreadCount(): Int = this.filter { !it.isRead }.size + +internal fun List.unseenCount(): Int = this.filter { !it.isSeen }.size diff --git a/sdk/src/test/java/com/magicbell/sdk/common/network/PageInfoMotherObject.kt b/sdk/src/test/java/com/magicbell/sdk/common/network/PageInfoMotherObject.kt deleted file mode 100644 index ddc93f4..0000000 --- a/sdk/src/test/java/com/magicbell/sdk/common/network/PageInfoMotherObject.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.magicbell.sdk.common.network - -import com.mobilejazz.harmony.common.randomBoolean -import com.magicbell.sdk.common.network.graphql.PageInfo - -internal fun anyPageInfo(): PageInfo { - return PageInfo( - endCursor = AnyCursor.ANY.value, - hasNextPage = randomBoolean(), - hasPreviousPage = randomBoolean(), - startCursor = AnyCursor.ANY.value - ) -} - -internal class PageInfoMotherObject { - companion object { - fun createPageInfo( - endCursor: String? = null, - hasNextPage: Boolean = randomBoolean(), - hasPreviousPage: Boolean = randomBoolean(), - startCursor: String? = null - ): PageInfo { - return PageInfo(endCursor, hasNextPage, hasPreviousPage, startCursor) - } - } -} \ No newline at end of file diff --git a/sdk/src/test/java/com/magicbell/sdk/feature/notification/motherobject/NotificationMotherObject.kt b/sdk/src/test/java/com/magicbell/sdk/feature/notification/motherobject/NotificationMotherObject.kt index dfd4e88..55ad2b4 100644 --- a/sdk/src/test/java/com/magicbell/sdk/feature/notification/motherobject/NotificationMotherObject.kt +++ b/sdk/src/test/java/com/magicbell/sdk/feature/notification/motherobject/NotificationMotherObject.kt @@ -67,8 +67,8 @@ internal class NotificationMotherObject { } } - val category = predicate.categories.firstOrNull() - val topic = predicate.topics.firstOrNull() + val category = predicate.category + val topic = predicate.topic return createNotification(id ?: randomString(), read, seen, archived, category, topic) } diff --git a/sdk/src/test/java/com/magicbell/sdk/feature/store/NotificationStoreRealTimeTests.kt b/sdk/src/test/java/com/magicbell/sdk/feature/store/NotificationStoreRealTimeTests.kt index 8d7c73a..2079173 100644 --- a/sdk/src/test/java/com/magicbell/sdk/feature/store/NotificationStoreRealTimeTests.kt +++ b/sdk/src/test/java/com/magicbell/sdk/feature/store/NotificationStoreRealTimeTests.kt @@ -137,8 +137,8 @@ internal class NotificationStoreRealTimeTests { // THEN coVerify(exactly = 2, timeout = 1000) { fetchStorePageInteractor.invoke(any(), any(), any()) } store.size.shouldBeExactly(defaultEdgeArraySize) - storePage.edges.mapIndexed { index, edge -> - store[index].id.shouldBe(edge.node.id) + storePage.notifications.mapIndexed { index, notif -> + store[index].id.shouldBe(notif.id) } Unit } @@ -238,8 +238,8 @@ internal class NotificationStoreRealTimeTests { coVerify(exactly = 2, timeout = 1000) { fetchStorePageInteractor.invoke(any(), any(), any()) } store.size.shouldBeExactly(defaultEdgeArraySize) store.totalCount.shouldBeExactly(initialCounter.totalCount) - storePage.edges.mapIndexed { index, edge -> - store[index].id.shouldBe(edge.node.id) + storePage.notifications.mapIndexed { index, notif -> + store[index].id.shouldBe(notif.id) } Unit } diff --git a/sdk/src/test/java/com/magicbell/sdk/feature/store/NotificationStoreTests.kt b/sdk/src/test/java/com/magicbell/sdk/feature/store/NotificationStoreTests.kt index e4ad685..9099fba 100644 --- a/sdk/src/test/java/com/magicbell/sdk/feature/store/NotificationStoreTests.kt +++ b/sdk/src/test/java/com/magicbell/sdk/feature/store/NotificationStoreTests.kt @@ -1,8 +1,6 @@ package com.magicbell.sdk.feature.store import com.magicbell.sdk.common.error.MagicBellError -import com.magicbell.sdk.common.network.AnyCursor -import com.magicbell.sdk.common.network.PageInfoMotherObject import com.magicbell.sdk.common.network.anyNotificationEdgeArray import com.magicbell.sdk.common.query.UserQuery import com.magicbell.sdk.common.threading.MainThread @@ -149,7 +147,7 @@ internal class NotificationStoreTests { // THEN coVerify(exactly = 1, timeout = 1000) { fetchStorePageInteractor.invoke(any(), any(), any()) } store.size.shouldBeExactly(defaultEdgeArraySize) - storePage.edges.map { it.node.id }.shouldBe(store.notifications.map { it.id }) + storePage.notifications.map { it.id }.shouldBe(store.notifications.map { it.id }) } @Test @@ -226,10 +224,10 @@ internal class NotificationStoreTests { fun test_fetch_withPagination_shouldReturnTwoNotificationPages() = runBlocking { // GIVEN val predicate = StorePredicate() - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = true) + val storePage = StoreMotherObject.createHasNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None) ) + val store = createNotificationStore(predicate, Result.success(storePage)) // WHEN @@ -255,9 +253,8 @@ internal class NotificationStoreTests { fun test_fetch_withoutPagination_shouldReturnEmptyArray() = runBlocking { // GIVEN val predicate = StorePredicate() - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = false) + val storePage = StoreMotherObject.createNoNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None) ) val store = createNotificationStore(predicate, Result.success(storePage)) @@ -282,9 +279,8 @@ internal class NotificationStoreTests { fun test_refresh_twoTimes_shouldReturnSamePage() = runBlocking { // GIVEN val predicate = StorePredicate() - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = true) + val storePage = StoreMotherObject.createHasNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None) ) val store = createNotificationStore(predicate, Result.success(storePage)) @@ -294,8 +290,8 @@ internal class NotificationStoreTests { // THEN coVerify(exactly = 1, timeout = 1000) { fetchStorePageInteractor.invoke(any(), any(), any()) } store.size.shouldBeExactly(defaultEdgeArraySize) - storePage.edges.mapIndexed { index, edge -> - store[index].id.shouldBe(edge.node.id) + storePage.notifications.mapIndexed { index, notif -> + store[index].id.shouldBe(notif.id) } // WHEN @@ -304,8 +300,8 @@ internal class NotificationStoreTests { // THEN coVerify(exactly = 2, timeout = 1000) { fetchStorePageInteractor.invoke(any(), any(), any()) } store.size.shouldBeExactly(defaultEdgeArraySize) - storePage.edges.mapIndexed { index, edge -> - store[index].id.shouldBe(edge.node.id) + storePage.notifications.mapIndexed { index, notif -> + store[index].id.shouldBe(notif.id) } Unit } @@ -314,9 +310,8 @@ internal class NotificationStoreTests { fun test_fetch_withPageInfoHasNextPageTrue_shouldConfigurePaginationTrue() = runBlocking { // GIVEN val predicate = StorePredicate() - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = true) + val storePage = StoreMotherObject.createHasNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None) ) val store = createNotificationStore(predicate, Result.success(storePage)) store.hasNextPage.shouldBeTrue() @@ -332,9 +327,8 @@ internal class NotificationStoreTests { fun test_fetch_withPageInfoHasNextPageFalse_shouldConfigurePaginationFalse() = runBlocking { // GIVEN val predicate = StorePredicate() - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = false) + val storePage = StoreMotherObject.createNoNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None) ) val store = createNotificationStore(predicate, Result.success(storePage)) store.hasNextPage.shouldBeTrue() @@ -342,7 +336,6 @@ internal class NotificationStoreTests { // WHEN store.fetch().getOrThrow() - // THEN store.hasNextPage.shouldBeFalse() } @@ -351,9 +344,8 @@ internal class NotificationStoreTests { fun test_refresh_withPageInfoHasNextPageFalse_shouldConfigurePaginationFalse() = runBlocking { // GIVEN val predicate = StorePredicate() - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = false) + val storePage = StoreMotherObject.createNoNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None) ) val store = createNotificationStore(predicate, Result.success(storePage)) store.hasNextPage.shouldBeTrue() @@ -369,9 +361,8 @@ internal class NotificationStoreTests { fun test_deleteNotification_withDefaultStorePredicate_shouldCallActionNotificationInteractor() = runBlocking { // GIVEN val predicate = StorePredicate() - val storePage = StoreMotherObject.createStorePage( + val storePage = StoreMotherObject.createNoNextPage( anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.Read), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = false) ) val store = createNotificationStore(predicate, Result.success(storePage)) // WHEN @@ -389,9 +380,8 @@ internal class NotificationStoreTests { fun test_deleteNotification_withError_shouldReturnError() = runBlocking { // GIVEN val predicate = StorePredicate() - val storePage = StoreMotherObject.createStorePage( + val storePage = StoreMotherObject.createNoNextPage( anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.Read), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = false) ) val store = createNotificationStore(predicate, Result.success(storePage)) @@ -421,9 +411,8 @@ internal class NotificationStoreTests { fun test_deleteNotification_withDefaultStorePredicateAndReadNotification_shouldRemoveNotificationAndSameUnreadCount() = runBlocking { // GIVEN val predicate = StorePredicate() - val storePage = StoreMotherObject.createStorePage( + val storePage = StoreMotherObject.createNoNextPage( anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.Read), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = false) ) val store = createNotificationStore(predicate, Result.success(storePage)) @@ -449,9 +438,8 @@ internal class NotificationStoreTests { fun test_deleteNotification_withDefaultStorePredicateAndUnreadNotification_shouldRemoveNotificationAndDifferentUnreadCount() = runBlocking { // GIVEN val predicate = StorePredicate() - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.Unread), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = false) + val storePage = StoreMotherObject.createNoNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.Unread) ) val store = createNotificationStore(predicate, Result.success(storePage)) @@ -476,9 +464,8 @@ internal class NotificationStoreTests { fun test_deleteNotification_withDefaultStorePredicateAndSeenNotification_shouldRemoveNotificationAndSameUnseenCount() = runBlocking { // GIVEN val predicate = StorePredicate() - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.Seen), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = false) + val storePage = StoreMotherObject.createNoNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.Seen) ) val store = createNotificationStore(predicate, Result.success(storePage)) @@ -503,9 +490,8 @@ internal class NotificationStoreTests { fun test_deleteNotification_withDefaultStorePredicateAndUnseenNotification_shouldRemoveNotificationAndDifferentUnseenCount() = runBlocking { // GIVEN val predicate = StorePredicate() - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.Unseen), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = false) + val storePage = StoreMotherObject.createNoNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.Unseen) ) val store = createNotificationStore(predicate, Result.success(storePage)) @@ -530,9 +516,8 @@ internal class NotificationStoreTests { fun test_deleteNotification_withReadStorePredicate_shouldRemoveNotification() = runBlocking { // GIVEN val predicate = StorePredicate(read = true) - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = false) + val storePage = StoreMotherObject.createNoNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None) ) val store = createNotificationStore(predicate, Result.success(storePage)) @@ -553,9 +538,8 @@ internal class NotificationStoreTests { fun test_deleteNotification_withUnreadStorePredicate_shouldRemoveNotification() = runBlocking { // GIVEN val predicate = StorePredicate(read = false) - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = false) + val storePage = StoreMotherObject.createNoNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None) ) val store = createNotificationStore(predicate, Result.success(storePage)) @@ -578,9 +562,8 @@ internal class NotificationStoreTests { fun test_deleteNotification_withUnreadStorePredicateWithUnseenNotifications_shouldRemoveNotificationAndUpdateUnseeCount() = runBlocking { // GIVEN val predicate = StorePredicate(read = false) - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.Unseen), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = false) + val storePage = StoreMotherObject.createNoNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.Unseen) ) val store = createNotificationStore(predicate, Result.success(storePage)) @@ -603,9 +586,8 @@ internal class NotificationStoreTests { fun test_deleteNotification_withUnreadStorePredicateWithSeenNotifications_shouldRemoveNotificationAndSameUnseenCount() = runBlocking { // GIVEN val predicate = StorePredicate(read = false) - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.Seen), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, hasNextPage = false) + val storePage = StoreMotherObject.createNoNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.Seen) ) val store = createNotificationStore(predicate, Result.success(storePage)) @@ -1299,9 +1281,8 @@ internal class NotificationStoreTests { // GIVEN val contentObserver = ContentObserverMock() val predicate = StorePredicate() - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, true) + val storePage = StoreMotherObject.createHasNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None) ) val store = createNotificationStore(predicate, Result.success(storePage)) store.addContentObserver(contentObserver) @@ -1330,9 +1311,8 @@ internal class NotificationStoreTests { // GIVEN val contentObserver = ContentObserverMock() val predicate = StorePredicate() - val storePage = StoreMotherObject.createStorePage( - anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None), - PageInfoMotherObject.createPageInfo(AnyCursor.ANY.value, true) + val storePage = StoreMotherObject.createHasNextPage( + anyNotificationEdgeArray(predicate, defaultEdgeArraySize, ForceProperty.None) ) val store = createNotificationStore(predicate, Result.success(storePage)) store.addContentObserver(contentObserver) diff --git a/sdk/src/test/java/com/magicbell/sdk/feature/store/NotificationValidatorTests.kt b/sdk/src/test/java/com/magicbell/sdk/feature/store/NotificationValidatorTests.kt index fa1e0f9..2a9c4f5 100644 --- a/sdk/src/test/java/com/magicbell/sdk/feature/store/NotificationValidatorTests.kt +++ b/sdk/src/test/java/com/magicbell/sdk/feature/store/NotificationValidatorTests.kt @@ -145,7 +145,7 @@ class NotificationValidatorTests { @Test fun test_predicate_category() { - val predicate = StorePredicate(categories = listOf("the-category")) + val predicate = StorePredicate(category = "the-category") for (notification in allNotifications(category = "the-category")) { predicate.match(notification).shouldBeTrue() } @@ -159,7 +159,7 @@ class NotificationValidatorTests { @Test fun test_predicate_topic() { - val predicate = StorePredicate(topics = listOf("the-topic")) + val predicate = StorePredicate(topic = "the-topic") for (notification in allNotifications(topic = "the-topic")) { predicate.match(notification).shouldBeTrue() } diff --git a/sdk/src/test/java/com/magicbell/sdk/feature/store/data/StoreContextTests.kt b/sdk/src/test/java/com/magicbell/sdk/feature/store/data/StoreContextTests.kt new file mode 100644 index 0000000..8d8697e --- /dev/null +++ b/sdk/src/test/java/com/magicbell/sdk/feature/store/data/StoreContextTests.kt @@ -0,0 +1,158 @@ +package com.magicbell.sdk.feature.store.data + +import com.magicbell.sdk.feature.store.StoreContext +import com.magicbell.sdk.feature.store.StorePagePredicate +import com.magicbell.sdk.feature.store.StorePredicate +import io.kotest.matchers.equality.shouldBeEqualToComparingFields +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class StoreContextTests { + private val defaultPagePredicate = StorePagePredicate(1, 1) + + private val defaultQueryParameters = listOf( + "archived" to "false", + "page" to "${defaultPagePredicate.page}", + "per_page" to "${defaultPagePredicate.size}") + + + @Test + fun test_asQueryParameter_defaultPredicate() { + // GIVEN + val predicate = StorePredicate() + val storeContext = StoreContext(predicate, defaultPagePredicate) + + // WHEN + val result = storeContext.asQueryParameters() + + // THEN + val expected = defaultQueryParameters + // compare content, not order + assertEquals( result.sortedBy { it.first }, expected.sortedBy { it.first } ) + } + + @Test + fun test_asQueryParameter_withReadPredicate() { + // GIVEN + val predicate = StorePredicate(read = true) + val storeContext = StoreContext(predicate, defaultPagePredicate) + + // WHEN + val result = storeContext.asQueryParameters() + + // THEN + val expected = defaultQueryParameters + listOf("read" to "true") + // compare content, not order + assertEquals( result.sortedBy { it.first }, expected.sortedBy { it.first } ) + } + + @Test + fun test_asQueryParameter_withUnreadPredicate() { + // GIVEN + val predicate = StorePredicate(read = false) + val storeContext = StoreContext(predicate, defaultPagePredicate) + + // WHEN + val result = storeContext.asQueryParameters() + + // THEN + val expected = defaultQueryParameters + listOf("read" to "false") + // compare content, not order + assertEquals( result.sortedBy { it.first }, expected.sortedBy { it.first } ) + } + + @Test + fun test_asQueryParameter_withSeenPredicate() { + // GIVEN + val predicate = StorePredicate(seen = true) + val storeContext = StoreContext(predicate, defaultPagePredicate) + + // WHEN + val result = storeContext.asQueryParameters() + + // THEN + val expected = defaultQueryParameters + listOf("seen" to "true") + // compare content, not order + assertEquals( result.sortedBy { it.first }, expected.sortedBy { it.first } ) + } + + @Test + fun test_asQueryParameter_withUnseenPredicate() { + // GIVEN + val predicate = StorePredicate(seen = false) + val storeContext = StoreContext(predicate, defaultPagePredicate) + + // WHEN + val result = storeContext.asQueryParameters() + + // THEN + val expected = defaultQueryParameters + listOf("seen" to "false") + // compare content, not order + assertEquals( result.sortedBy { it.first }, expected.sortedBy { it.first } ) + } + + @Test + fun test_asQueryParameter_withPagePredicate() { + // GIVEN + val predicate = StorePredicate() + val pagePredicate = StorePagePredicate(3, 77) + val storeContext = StoreContext(predicate, pagePredicate) + + // WHEN + val result = storeContext.asQueryParameters() + + // THEN + val expected = listOf("archived" to "false", "page" to "3", "per_page" to "77") + // compare content, not order + assertEquals( result.sortedBy { it.first }, expected.sortedBy { it.first } ) + } + + @Test + fun test_asQueryParameter_withTopicFilter() { + // GIVEN + val predicate = StorePredicate(topic = "example") + val storeContext = StoreContext(predicate, defaultPagePredicate) + + // WHEN + val result = storeContext.asQueryParameters() + + // THEN + val expected = defaultQueryParameters + listOf("topic" to "example") + // compare content, not order + assertEquals( result.sortedBy { it.first }, expected.sortedBy { it.first } ) + } + + @Test + fun test_asQueryParameter_withCategoryFilter() { + // GIVEN + val predicate = StorePredicate(category = "example") + val storeContext = StoreContext(predicate, defaultPagePredicate) + + // WHEN + val result = storeContext.asQueryParameters() + + // THEN + val expected = defaultQueryParameters + listOf("category" to "example") + // compare content, not order + assertEquals( result.sortedBy { it.first }, expected.sortedBy { it.first } ) + } + + @Test + fun test_asQueryParameter_withComplexPredicateAndPage() { + // GIVEN + val predicate = StorePredicate(seen = false, read = true, archived = true, topic = "a-topic", category = "a-category") + val pagePredicate = StorePagePredicate(9, 37) + val storeContext = StoreContext(predicate, pagePredicate) + + // WHEN + val result = storeContext.asQueryParameters() + + // THEN + val expected = listOf( + "seen" to "false", "read" to "true", "archived" to "true", + "topic" to "a-topic", "category" to "a-category", + "page" to "9", "per_page" to "37") + // compare content, not order + assertEquals( result.sortedBy { it.first }, expected.sortedBy { it.first } ) + } +} \ No newline at end of file diff --git a/sdk/src/test/java/com/magicbell/sdk/feature/store/data/StoreNetworkDataSourceTests.kt b/sdk/src/test/java/com/magicbell/sdk/feature/store/data/StoreNetworkDataSourceTests.kt new file mode 100644 index 0000000..fbbfb8a --- /dev/null +++ b/sdk/src/test/java/com/magicbell/sdk/feature/store/data/StoreNetworkDataSourceTests.kt @@ -0,0 +1,57 @@ +package com.magicbell.sdk.feature.store.data + +import com.magicbell.sdk.common.environment.Environment +import com.magicbell.sdk.common.network.DefaultHttpClient +import com.magicbell.sdk.common.network.HttpClient +import com.magicbell.sdk.common.query.UserQuery +import com.magicbell.sdk.feature.store.StoreContext +import com.magicbell.sdk.feature.store.StorePage +import com.magicbell.sdk.feature.store.StorePagePredicate +import com.magicbell.sdk.feature.store.StorePredicate +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import okhttp3.OkHttpClient +import okhttp3.Request +import org.junit.jupiter.api.Test +import java.net.URL + +internal class StoreNetworkDataSourceTests { + + private val defaultPagePredicate = StorePagePredicate(1, 1) + private val userQuery = UserQuery.createEmail("john@doe.com") + + private val httpClient: HttpClient = run { + val m = mockk() + coEvery { m.performRequest(any()) } returns "mock response" + coEvery { m.prepareRequest(any(), any(), any(),any(),any(),any()) } returns mockk() + m + } + + private val defaultMapper: StoreResponseToStorePageMapper = run { + val m = mockk() + every { m.map(any()) } returns mockk() + m + } + + @Test + fun test_queriesCorrectPath() = runBlocking { + // GIVEN + val predicate = StorePredicate() + val storeContext = StoreContext(predicate, defaultPagePredicate) + val query = StoreQuery(storeContext, userQuery) + val dataSource = StoreNetworkDataSource(httpClient, defaultMapper) + + // WHEN + dataSource.get(query) + + // THEN + coVerify(exactly = 1, timeout = 1000) { httpClient.prepareRequest("notifications", any(), any(), any(), any(), any()) } + + Unit + } +} \ No newline at end of file diff --git a/sdk/src/test/java/com/magicbell/sdk/feature/store/motherobject/StoreMotherObject.kt b/sdk/src/test/java/com/magicbell/sdk/feature/store/motherobject/StoreMotherObject.kt index 34a9aa2..1d21227 100644 --- a/sdk/src/test/java/com/magicbell/sdk/feature/store/motherobject/StoreMotherObject.kt +++ b/sdk/src/test/java/com/magicbell/sdk/feature/store/motherobject/StoreMotherObject.kt @@ -2,40 +2,60 @@ package com.magicbell.sdk.feature.store.motherobject import com.mobilejazz.harmony.common.randomInt import com.magicbell.sdk.common.network.anyNotificationEdgeArray -import com.magicbell.sdk.common.network.anyPageInfo -import com.magicbell.sdk.common.network.graphql.Edge -import com.magicbell.sdk.common.network.graphql.PageInfo +import com.magicbell.sdk.common.network.unreadCount +import com.magicbell.sdk.common.network.unseenCount import com.magicbell.sdk.feature.notification.Notification import com.magicbell.sdk.feature.notification.motherobject.ForceProperty import com.magicbell.sdk.feature.store.StorePage import com.magicbell.sdk.feature.store.StorePredicate internal fun givenPageStore(predicate: StorePredicate, size: Int, forceProperty: ForceProperty = ForceProperty.None): StorePage { + val totalPages = (1..10).random() + val currentPage = (totalPages .. 10).random() return StoreMotherObject.createStorePage( anyNotificationEdgeArray(predicate, size, forceProperty), - anyPageInfo() + currentPage, + totalPages, ) } internal fun anyPageStore(): StorePage { + val totalPages = (1..10).random() + val currentPage = (totalPages .. 10).random() return StoreMotherObject.createStorePage( anyNotificationEdgeArray(predicate = StorePredicate(), randomInt(0, 20), ForceProperty.None), - anyPageInfo() + currentPage, + totalPages, ) } internal class StoreMotherObject { companion object { + fun createNoNextPage(notifications: List): StorePage { + return createStorePage(notifications, 1, 1) + } + + fun createHasNextPage(notifications: List): StorePage { + return createStorePage(notifications, 1, 2) + } + + fun createAnyNextPage(notifications: List): StorePage { + return createStorePage(notifications, 1, (1..2).random()) + } + fun createStorePage( - edges: List>, - pageInfo: PageInfo + notifications: List, + currentPage: Int, + totalPages: Int, ): StorePage { return StorePage( - edges, - pageInfo, - edges.size, - edges.filter { !it.node.isRead }.size, - edges.filter { !it.node.isSeen }.size + notifications, + notifications.size, + notifications.unreadCount(), + notifications.unseenCount(), + totalPages, + notifications.size, + currentPage, ) } }