[fenix] For https://github.com/mozilla-mobile/fenix/issues/24222: Persist user interactions with nimbus messages

pull/600/head
Arturo Mejia 3 years ago committed by mergify[bot]
parent 7f73bfcd90
commit 36eeae0c0f

@ -160,9 +160,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
setStartupMetrics(store, settings()) setStartupMetrics(store, settings())
} }
if (FeatureFlags.messagingFeature && settings().isExperimentationEnabled) {
components.appStore.dispatch(AppAction.MessagingAction.Restore)
}
} }
@CallSuper @CallSuper
@ -755,6 +752,11 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
} }
) )
components.analytics.experiments.register(object : NimbusInterface.Observer { components.analytics.experiments.register(object : NimbusInterface.Observer {
override fun onExperimentsFetched() {
if (FeatureFlags.messagingFeature && settings().isExperimentationEnabled) {
components.appStore.dispatch(AppAction.MessagingAction.Restore)
}
}
override fun onUpdatesApplied(updated: List<EnrolledExperiment>) { override fun onUpdatesApplied(updated: List<EnrolledExperiment>) {
CustomizeHome.jumpBackIn.set(settings.showRecentTabsFeature) CustomizeHome.jumpBackIn.set(settings.showRecentTabsFeature)
CustomizeHome.recentlySaved.set(settings.showRecentBookmarksFeature) CustomizeHome.recentlySaved.set(settings.showRecentBookmarksFeature)

@ -26,7 +26,7 @@ import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.experiments.createNimbus import org.mozilla.fenix.experiments.createNimbus
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.gleanplumb.KeyPairMessageMetadataStorage import org.mozilla.fenix.gleanplumb.OnDiskMessageMetadataStorage
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored
@ -123,7 +123,7 @@ class Analytics(
val messagingStorage by lazyMonitored { val messagingStorage by lazyMonitored {
NimbusMessagingStorage( NimbusMessagingStorage(
context = context, context = context,
metadataStorage = KeyPairMessageMetadataStorage(), metadataStorage = OnDiskMessageMetadataStorage(context),
gleanPlumb = experiments, gleanPlumb = experiments,
reportMalformedMessage = { reportMalformedMessage = {
metrics.track(Event.Messaging.MessageMalformed(it)) metrics.track(Event.Messaging.MessageMalformed(it))

@ -1,29 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.gleanplumb
/* Dummy implementation until we provide full implementation.
* This will covered on https://github.com/mozilla-mobile/fenix/issues/24222
* */
class KeyPairMessageMetadataStorage : MessageMetadataStorage {
override fun getMetadata(): List<Message.Metadata> {
return listOf(
Message.Metadata(
id = "eu-tracking-protection-for-ireland",
displayCount = 0,
pressed = false,
dismissed = false
)
)
}
override fun addMetadata(metadata: Message.Metadata): Message.Metadata {
return metadata
}
@SuppressWarnings("EmptyFunctionBlock")
override fun updateMetadata(metadata: Message.Metadata) {
}
}

@ -34,11 +34,13 @@ data class Message(
* @param displayCount Indicates how many times a message is displayed. * @param displayCount Indicates how many times a message is displayed.
* @param pressed Indicates if a message has been clicked. * @param pressed Indicates if a message has been clicked.
* @param dismissed Indicates if a message has been closed. * @param dismissed Indicates if a message has been closed.
* @param lastTimeShown A timestamp indicating when was the last time, the message was shown.
*/ */
data class Metadata( data class Metadata(
val id: String, val id: String,
val displayCount: Int = 0, val displayCount: Int = 0,
val pressed: Boolean = false, val pressed: Boolean = false,
val dismissed: Boolean = false val dismissed: Boolean = false,
val lastTimeShown: Long = 0L
) )
} }

@ -8,16 +8,16 @@ interface MessageMetadataStorage {
/** /**
* Provide all the message metadata saved in the storage. * Provide all the message metadata saved in the storage.
*/ */
fun getMetadata(): List<Message.Metadata> suspend fun getMetadata(): Map<String, Message.Metadata>
/** /**
* Given a [metadata] add the message metadata on the storage. * Given a [metadata] add the message metadata on the storage.
* @return the added message on the [MessageMetadataStorage] * @return the added message on the [MessageMetadataStorage]
*/ */
fun addMetadata(metadata: Message.Metadata): Message.Metadata suspend fun addMetadata(metadata: Message.Metadata): Message.Metadata
/** /**
* Given a [metadata] update the message metadata on the storage. * Given a [metadata] update the message metadata on the storage.
*/ */
fun updateMetadata(metadata: Message.Metadata) suspend fun updateMetadata(metadata: Message.Metadata)
} }

@ -33,16 +33,14 @@ class NimbusMessagingStorage(
/** /**
* Returns a list of available messages descending sorted by their priority. * Returns a list of available messages descending sorted by their priority.
*/ */
fun getMessages(): List<Message> { suspend fun getMessages(): List<Message> {
val nimbusTriggers = nimbusFeature.triggers val nimbusTriggers = nimbusFeature.triggers
val nimbusStyles = nimbusFeature.styles val nimbusStyles = nimbusFeature.styles
val nimbusActions = nimbusFeature.actions val nimbusActions = nimbusFeature.actions
val nimbusMessages = nimbusFeature.messages val nimbusMessages = nimbusFeature.messages
val defaultStyle = StyleData(context) val defaultStyle = StyleData(context)
val storageMetadata = metadataStorage.getMetadata().associateBy { val storageMetadata = metadataStorage.getMetadata()
it.id
}
return nimbusMessages.mapNotNull { (key, value) -> return nimbusMessages.mapNotNull { (key, value) ->
val action = sanitizeAction(key, value.action, nimbusActions) ?: return@mapNotNull null val action = sanitizeAction(key, value.action, nimbusActions) ?: return@mapNotNull null
@ -98,7 +96,7 @@ class NimbusMessagingStorage(
/** /**
* Updated the provided [metadata] in the storage. * Updated the provided [metadata] in the storage.
*/ */
fun updateMetadata(metadata: Message.Metadata) { suspend fun updateMetadata(metadata: Message.Metadata) {
metadataStorage.updateMetadata(metadata) metadataStorage.updateMetadata(metadata)
} }
@ -167,8 +165,7 @@ class NimbusMessagingStorage(
} }
} }
private fun addMetadata(id: String): Message.Metadata { private suspend fun addMetadata(id: String): Message.Metadata {
// This will be improve on https://github.com/mozilla-mobile/fenix/issues/24222
return metadataStorage.addMetadata( return metadataStorage.addMetadata(
Message.Metadata( Message.Metadata(
id = id, id = id,

@ -0,0 +1,95 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.gleanplumb
import android.content.Context
import android.util.AtomicFile
import androidx.annotation.VisibleForTesting
import mozilla.components.support.ktx.util.readAndDeserialize
import mozilla.components.support.ktx.util.writeString
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
internal const val FILE_NAME = "nimbus_messages_metadata.json"
/**
* A storage that persists [Message.Metadata] into disk.
*/
class OnDiskMessageMetadataStorage(
private val context: Context
) : MessageMetadataStorage {
private val diskCacheLock = Any()
@VisibleForTesting
internal var metadataMap: MutableMap<String, Message.Metadata> = hashMapOf()
override suspend fun getMetadata(): Map<String, Message.Metadata> {
if (metadataMap.isEmpty()) {
metadataMap = readFromDisk().toMutableMap()
}
return metadataMap
}
override suspend fun addMetadata(metadata: Message.Metadata): Message.Metadata {
metadataMap[metadata.id] = metadata
writeToDisk()
return metadata
}
override suspend fun updateMetadata(metadata: Message.Metadata) {
addMetadata(metadata)
}
@VisibleForTesting
internal fun readFromDisk(): Map<String, Message.Metadata> {
synchronized(diskCacheLock) {
return getFile().readAndDeserialize {
JSONArray(it).toMetadataMap()
} ?: emptyMap()
}
}
@VisibleForTesting
internal fun writeToDisk() {
synchronized(diskCacheLock) {
val json = metadataMap.values.toList().fold("") { acc, next ->
if (acc.isEmpty()) {
next.toJson()
} else {
"$acc,${next.toJson()}"
}
}
getFile().writeString { "[$json]" }
}
}
private fun getFile(): AtomicFile {
return AtomicFile(File(context.filesDir, FILE_NAME))
}
}
internal fun JSONArray.toMetadataMap(): Map<String, Message.Metadata> {
return (0 until length()).map { index ->
getJSONObject(index).toMetadata()
}.associateBy {
it.id
}
}
@Suppress("MaxLineLength") // To avoid adding any extra space to the string.
internal fun Message.Metadata.toJson(): String {
return """{"id":"$id","displayCount":$displayCount,"pressed":$pressed,"dismissed":$dismissed,"lastTimeShown":$lastTimeShown}"""
}
internal fun JSONObject.toMetadata(): Message.Metadata {
return Message.Metadata(
id = optString("id"),
displayCount = optInt("displayCount"),
pressed = optBoolean("pressed"),
dismissed = optBoolean("dismissed"),
lastTimeShown = optLong("lastTimeShown")
)
}

@ -5,6 +5,9 @@
package org.mozilla.fenix.gleanplumb.state package org.mozilla.fenix.gleanplumb.state
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext import mozilla.components.lib.state.MiddlewareContext
import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppAction
@ -23,7 +26,8 @@ import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
typealias AppStoreMiddlewareContext = MiddlewareContext<AppState, AppAction> typealias AppStoreMiddlewareContext = MiddlewareContext<AppState, AppAction>
class MessagingMiddleware( class MessagingMiddleware(
private val messagingStorage: NimbusMessagingStorage private val messagingStorage: NimbusMessagingStorage,
private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO),
) : Middleware<AppState, AppAction> { ) : Middleware<AppState, AppAction> {
override fun invoke( override fun invoke(
@ -33,9 +37,10 @@ class MessagingMiddleware(
) { ) {
when (action) { when (action) {
is Restore -> { is Restore -> {
coroutineScope.launch {
val messages = messagingStorage.getMessages() val messages = messagingStorage.getMessages()
context.store.dispatch(UpdateMessages(messages))
context.dispatch(UpdateMessages(messages)) }
} }
is Evaluate -> { is Evaluate -> {
@ -62,7 +67,8 @@ class MessagingMiddleware(
context: AppStoreMiddlewareContext context: AppStoreMiddlewareContext
) { ) {
val newMetadata = oldMessage.metadata.copy( val newMetadata = oldMessage.metadata.copy(
displayCount = oldMessage.metadata.displayCount + 1 displayCount = oldMessage.metadata.displayCount + 1,
lastTimeShown = now()
) )
val newMessage = oldMessage.copy( val newMessage = oldMessage.copy(
metadata = newMetadata metadata = newMetadata
@ -74,8 +80,10 @@ class MessagingMiddleware(
removeMessage(context, oldMessage) removeMessage(context, oldMessage)
} }
context.dispatch(UpdateMessages(newMessages)) context.dispatch(UpdateMessages(newMessages))
coroutineScope.launch {
messagingStorage.updateMetadata(newMetadata) messagingStorage.updateMetadata(newMetadata)
} }
}
@VisibleForTesting @VisibleForTesting
internal fun onMessageDismissed( internal fun onMessageDismissed(
@ -83,11 +91,12 @@ class MessagingMiddleware(
message: Message message: Message
) { ) {
val newMessages = removeMessage(context, message) val newMessages = removeMessage(context, message)
val updatedMetadata = message.metadata.copy(dismissed = true)
messagingStorage.updateMetadata(updatedMetadata)
context.dispatch(UpdateMessages(newMessages)) context.dispatch(UpdateMessages(newMessages))
consumeMessageToShowIfNeeded(context, message) consumeMessageToShowIfNeeded(context, message)
coroutineScope.launch {
val updatedMetadata = message.metadata.copy(dismissed = true)
messagingStorage.updateMetadata(updatedMetadata)
}
} }
@VisibleForTesting @VisibleForTesting
@ -96,9 +105,10 @@ class MessagingMiddleware(
context: AppStoreMiddlewareContext context: AppStoreMiddlewareContext
) { ) {
// Update Nimbus storage. // Update Nimbus storage.
coroutineScope.launch {
val updatedMetadata = message.metadata.copy(pressed = true) val updatedMetadata = message.metadata.copy(pressed = true)
messagingStorage.updateMetadata(updatedMetadata) messagingStorage.updateMetadata(updatedMetadata)
}
// Update app state. // Update app state.
val newMessages = removeMessage(context, message) val newMessages = removeMessage(context, message)
context.dispatch(UpdateMessages(newMessages)) context.dispatch(UpdateMessages(newMessages))
@ -136,4 +146,7 @@ class MessagingMiddleware(
} }
return removeMessage(context, oldMessage) + updatedMessage return removeMessage(context, oldMessage) + updatedMessage
} }
@VisibleForTesting
internal fun now(): Long = System.currentTimeMillis()
} }

@ -4,10 +4,13 @@
package org.mozilla.fenix.gleanplumb package org.mozilla.fenix.gleanplumb
import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.spyk import io.mockk.spyk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.support.test.mock import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@ -37,11 +40,11 @@ class NimbusMessagingStorageTest {
private lateinit var gleanPlumb: GleanPlumbInterface private lateinit var gleanPlumb: GleanPlumbInterface
private lateinit var messagingFeature: FeatureHolder<Messaging> private lateinit var messagingFeature: FeatureHolder<Messaging>
private lateinit var messaging: Messaging private lateinit var messaging: Messaging
private val coroutineScope = TestCoroutineScope()
private var malformedWasReported = false private var malformedWasReported = false
private val reportMalformedMessage: (String) -> Unit = { private val reportMalformedMessage: (String) -> Unit = {
malformedWasReported = true malformedWasReported = true
} }
@Before @Before
fun setup() { fun setup() {
gleanPlumb = mockk(relaxed = true) gleanPlumb = mockk(relaxed = true)
@ -49,7 +52,7 @@ class NimbusMessagingStorageTest {
malformedWasReported = false malformedWasReported = false
messagingFeature = createMessagingFeature() messagingFeature = createMessagingFeature()
every { metadataStorage.getMetadata() } returns listOf(Message.Metadata(id = "message-1")) coEvery { metadataStorage.getMetadata() } returns mapOf("message-1" to Message.Metadata(id = "message-1"))
storage = NimbusMessagingStorage( storage = NimbusMessagingStorage(
testContext, testContext,
@ -61,7 +64,7 @@ class NimbusMessagingStorageTest {
} }
@Test @Test
fun `WHEN calling getMessages THEN provide a list of available messages`() { fun `WHEN calling getMessages THEN provide a list of available messages`() = runBlockingTest {
val message = storage.getMessages().first() val message = storage.getMessages().first()
assertEquals("message-1", message.id) assertEquals("message-1", message.id)
@ -69,7 +72,8 @@ class NimbusMessagingStorageTest {
} }
@Test @Test
fun `WHEN calling getMessages THEN provide a list of sorted messages by priority`() { fun `WHEN calling getMessages THEN provide a list of sorted messages by priority`() =
runBlockingTest {
val messages = mapOf( val messages = mapOf(
"low-message" to createMessageData(style = "low-priority"), "low-message" to createMessageData(style = "low-priority"),
"high-message" to createMessageData(style = "high-priority"), "high-message" to createMessageData(style = "high-priority"),
@ -86,7 +90,11 @@ class NimbusMessagingStorageTest {
messages = messages messages = messages
) )
every { metadataStorage.getMetadata() } returns listOf(Message.Metadata(id = "message-1")) coEvery { metadataStorage.getMetadata() } returns mapOf(
"message-1" to Message.Metadata(
id = "message-1"
)
)
val storage = NimbusMessagingStorage( val storage = NimbusMessagingStorage(
testContext, testContext,
@ -104,10 +112,11 @@ class NimbusMessagingStorageTest {
} }
@Test @Test
fun `GIVEN pressed message WHEN calling getMessages THEN filter out the pressed message`() { fun `GIVEN pressed message WHEN calling getMessages THEN filter out the pressed message`() =
val metadataList = listOf( runBlockingTest {
Message.Metadata(id = "pressed-message", pressed = true), val metadataList = mapOf(
Message.Metadata(id = "normal-message", pressed = false) "pressed-message" to Message.Metadata(id = "pressed-message", pressed = true),
"normal-message" to Message.Metadata(id = "normal-message", pressed = false)
) )
val messages = mapOf( val messages = mapOf(
"pressed-message" to createMessageData(style = "high-priority"), "pressed-message" to createMessageData(style = "high-priority"),
@ -122,7 +131,7 @@ class NimbusMessagingStorageTest {
messages = messages messages = messages
) )
every { metadataStorage.getMetadata() } returns metadataList coEvery { metadataStorage.getMetadata() } returns metadataList
val storage = NimbusMessagingStorage( val storage = NimbusMessagingStorage(
testContext, testContext,
@ -139,10 +148,11 @@ class NimbusMessagingStorageTest {
} }
@Test @Test
fun `GIVEN dismissed message WHEN calling getMessages THEN filter out the dismissed message`() { fun `GIVEN dismissed message WHEN calling getMessages THEN filter out the dismissed message`() =
val metadataList = listOf( runBlockingTest {
Message.Metadata(id = "dismissed-message", dismissed = true), val metadataList = mapOf(
Message.Metadata(id = "normal-message", dismissed = false) "dismissed-message" to Message.Metadata(id = "dismissed-message", dismissed = true),
"normal-message" to Message.Metadata(id = "normal-message", dismissed = false)
) )
val messages = mapOf( val messages = mapOf(
"dismissed-message" to createMessageData(style = "high-priority"), "dismissed-message" to createMessageData(style = "high-priority"),
@ -157,7 +167,7 @@ class NimbusMessagingStorageTest {
messages = messages messages = messages
) )
every { metadataStorage.getMetadata() } returns metadataList coEvery { metadataStorage.getMetadata() } returns metadataList
val storage = NimbusMessagingStorage( val storage = NimbusMessagingStorage(
testContext, testContext,
@ -174,10 +184,14 @@ class NimbusMessagingStorageTest {
} }
@Test @Test
fun `GIVEN a message that the maxDisplayCount WHEN calling getMessages THEN filter out the message`() { fun `GIVEN a message that the maxDisplayCount WHEN calling getMessages THEN filter out the message`() =
val metadataList = listOf( runBlockingTest {
Message.Metadata(id = "shown-many-times-message", displayCount = 10), val metadataList = mapOf(
Message.Metadata(id = "normal-message", displayCount = 0) "shown-many-times-message" to Message.Metadata(
id = "shown-many-times-message",
displayCount = 10
),
"normal-message" to Message.Metadata(id = "normal-message", displayCount = 0)
) )
val messages = mapOf( val messages = mapOf(
"shown-many-times-message" to createMessageData( "shown-many-times-message" to createMessageData(
@ -195,7 +209,7 @@ class NimbusMessagingStorageTest {
messages = messages messages = messages
) )
every { metadataStorage.getMetadata() } returns metadataList coEvery { metadataStorage.getMetadata() } returns metadataList
val storage = NimbusMessagingStorage( val storage = NimbusMessagingStorage(
testContext, testContext,
@ -212,7 +226,7 @@ class NimbusMessagingStorageTest {
} }
@Test @Test
fun `GIVEN a malformed message WHEN calling getMessages THEN provide a list of messages ignoring the malformed one`() { fun `GIVEN a malformed message WHEN calling getMessages THEN provide a list of messages ignoring the malformed one`() = runBlockingTest {
val messages = storage.getMessages() val messages = storage.getMessages()
val firstMessage = messages.first() val firstMessage = messages.first()
@ -237,11 +251,11 @@ class NimbusMessagingStorageTest {
} }
@Test @Test
fun `WHEN calling updateMetadata THEN delegate to metadataStorage`() { fun `WHEN calling updateMetadata THEN delegate to metadataStorage`() = runBlockingTest {
storage.updateMetadata(mockk()) storage.updateMetadata(mockk())
verify { metadataStorage.updateMetadata(any()) } coEvery { metadataStorage.updateMetadata(any()) }
} }
@Test @Test

@ -0,0 +1,144 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.gleanplumb
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.just
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.support.test.robolectric.testContext
import org.json.JSONArray
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.experiments.nimbus.GleanPlumbInterface
import org.mozilla.experiments.nimbus.internal.FeatureHolder
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.Messaging
@RunWith(FenixRobolectricTestRunner::class)
class OnDiskMessageMetadataStorageTest {
private lateinit var storage: OnDiskMessageMetadataStorage
private lateinit var metadataStorage: MessageMetadataStorage
private lateinit var gleanPlumb: GleanPlumbInterface
private lateinit var messagingFeature: FeatureHolder<Messaging>
private lateinit var messaging: Messaging
@Before
fun setup() {
storage = OnDiskMessageMetadataStorage(
testContext
)
}
@Test
fun `GIVEN metadata is not loaded from disk WHEN calling getMetadata THEN load it`() =
runBlockingTest {
val spiedStorage = spyk(storage)
coEvery { spiedStorage.readFromDisk() } returns emptyMap()
spiedStorage.getMetadata()
verify { spiedStorage.readFromDisk() }
}
@Test
fun `GIVEN metadata is loaded from disk WHEN calling getMetadata THEN do not load it from disk`() =
runBlockingTest {
val spiedStorage = spyk(storage)
spiedStorage.metadataMap = hashMapOf("" to Message.Metadata("id"))
spiedStorage.getMetadata()
verify(exactly = 0) { spiedStorage.readFromDisk() }
}
@Test
fun `WHEN calling addMetadata THEN add in memory and disk`() = runBlockingTest {
val spiedStorage = spyk(storage)
assertTrue(spiedStorage.metadataMap.isEmpty())
coEvery { spiedStorage.writeToDisk() } just Runs
spiedStorage.addMetadata(Message.Metadata("id"))
assertFalse(spiedStorage.metadataMap.isEmpty())
coVerify { spiedStorage.writeToDisk() }
}
@Test
fun `WHEN calling updateMetadata THEN delegate to addMetadata`() = runBlockingTest {
val spiedStorage = spyk(storage)
val metadata = Message.Metadata("id")
coEvery { spiedStorage.writeToDisk() } just Runs
spiedStorage.updateMetadata(metadata)
coVerify { spiedStorage.addMetadata(metadata) }
}
@Test
fun `WHEN calling toJson THEN return an string json representation`() {
val metadata = Message.Metadata(
id = "id",
displayCount = 1,
pressed = false,
dismissed = false,
lastTimeShown = 0L,
)
val expected =
"""{"id":"id","displayCount":1,"pressed":false,"dismissed":false,"lastTimeShown":0}"""
assertEquals(expected, metadata.toJson())
}
@Test
fun `WHEN calling toMetadata THEN return Metadata representation`() {
val json =
"""{"id":"id","displayCount":1,"pressed":false,"dismissed":false,"lastTimeShown":0}"""
val jsonObject = JSONObject(json)
val metadata = Message.Metadata(
id = "id",
displayCount = 1,
pressed = false,
dismissed = false,
lastTimeShown = 0L,
)
assertEquals(metadata, jsonObject.toMetadata())
}
@Test
fun `WHEN calling toMetadataMap THEN return map representation`() {
val json =
"""[{"id":"id","displayCount":1,"pressed":false,"dismissed":false,"lastTimeShown":0}]"""
val jsonArray = JSONArray(json)
val metadata = Message.Metadata(
id = "id",
displayCount = 1,
pressed = false,
dismissed = false,
lastTimeShown = 0L,
)
assertEquals(metadata, jsonArray.toMetadataMap()[metadata.id])
}
}

@ -5,11 +5,14 @@
package org.mozilla.fenix.gleanplumb.state package org.mozilla.fenix.gleanplumb.state
import io.mockk.Runs import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.spyk import io.mockk.spyk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.lib.state.MiddlewareContext import mozilla.components.lib.state.MiddlewareContext
import mozilla.components.service.glean.testing.GleanTestRule import mozilla.components.service.glean.testing.GleanTestRule
import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.robolectric.testContext
@ -38,6 +41,7 @@ import org.mozilla.fenix.nimbus.MessageData
@RunWith(FenixRobolectricTestRunner::class) @RunWith(FenixRobolectricTestRunner::class)
class MessagingMiddlewareTest { class MessagingMiddlewareTest {
private val coroutineScope = TestCoroutineScope()
private lateinit var store: AppStore private lateinit var store: AppStore
private lateinit var middleware: MessagingMiddleware private lateinit var middleware: MessagingMiddleware
private lateinit var messagingStorage: NimbusMessagingStorage private lateinit var messagingStorage: NimbusMessagingStorage
@ -48,10 +52,14 @@ class MessagingMiddlewareTest {
@Before @Before
fun setUp() { fun setUp() {
store = mockk(relaxed = true)
messagingStorage = mockk(relaxed = true) messagingStorage = mockk(relaxed = true)
middlewareContext = mockk(relaxed = true) middlewareContext = mockk(relaxed = true)
every { middlewareContext.store } returns store
middleware = MessagingMiddleware( middleware = MessagingMiddleware(
messagingStorage messagingStorage,
coroutineScope
) )
} }
@ -59,11 +67,11 @@ class MessagingMiddlewareTest {
fun `WHEN Restore THEN get messages from the storage and UpdateMessages`() { fun `WHEN Restore THEN get messages from the storage and UpdateMessages`() {
val messages: List<Message> = emptyList() val messages: List<Message> = emptyList()
every { messagingStorage.getMessages() } returns messages coEvery { messagingStorage.getMessages() } returns messages
middleware.invoke(middlewareContext, {}, Restore) middleware.invoke(middlewareContext, {}, Restore)
verify { middlewareContext.dispatch(UpdateMessages(messages)) } verify { store.dispatch(UpdateMessages(messages)) }
} }
@Test @Test
@ -101,7 +109,7 @@ class MessagingMiddlewareTest {
middleware.invoke(middlewareContext, {}, MessageClicked(message)) middleware.invoke(middlewareContext, {}, MessageClicked(message))
verify { messagingStorage.updateMetadata(message.metadata.copy(pressed = true)) } coVerify { messagingStorage.updateMetadata(message.metadata.copy(pressed = true)) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) } verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
} }
@ -127,7 +135,7 @@ class MessagingMiddlewareTest {
MessageDismissed(message) MessageDismissed(message)
) )
verify { messagingStorage.updateMetadata(message.metadata.copy(dismissed = true)) } coVerify { messagingStorage.updateMetadata(message.metadata.copy(dismissed = true)) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) } verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
} }
@ -143,17 +151,19 @@ class MessagingMiddlewareTest {
) )
val appState: AppState = mockk(relaxed = true) val appState: AppState = mockk(relaxed = true)
val messagingState: MessagingState = mockk(relaxed = true) val messagingState: MessagingState = mockk(relaxed = true)
val spiedMiddleware = spyk(middleware)
every { spiedMiddleware.now() } returns 0L
every { messagingState.messages } returns emptyList() every { messagingState.messages } returns emptyList()
every { appState.messaging } returns messagingState every { appState.messaging } returns messagingState
every { middlewareContext.state } returns appState every { middlewareContext.state } returns appState
middleware.invoke( spiedMiddleware.invoke(
middlewareContext, {}, middlewareContext, {},
MessageDisplayed(message) MessageDisplayed(message)
) )
verify { messagingStorage.updateMetadata(message.metadata.copy(displayCount = 1)) } coVerify { messagingStorage.updateMetadata(message.metadata.copy(displayCount = 1)) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) } verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
} }
@ -175,7 +185,7 @@ class MessagingMiddlewareTest {
spiedMiddleware.onMessageDismissed(middlewareContext, message) spiedMiddleware.onMessageDismissed(middlewareContext, message)
verify { messagingStorage.updateMetadata(message.metadata.copy(dismissed = true)) } coVerify { messagingStorage.updateMetadata(message.metadata.copy(dismissed = true)) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) } verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
verify { spiedMiddleware.removeMessage(middlewareContext, message) } verify { spiedMiddleware.removeMessage(middlewareContext, message) }
} }
@ -278,6 +288,7 @@ class MessagingMiddlewareTest {
val updatedMessage = oldMessage.copy(metadata = oldMessage.metadata.copy(displayCount = 1)) val updatedMessage = oldMessage.copy(metadata = oldMessage.metadata.copy(displayCount = 1))
val spiedMiddleware = spyk(middleware) val spiedMiddleware = spyk(middleware)
every { spiedMiddleware.now() } returns 0
every { oldMessageData.maxDisplayCount } returns 2 every { oldMessageData.maxDisplayCount } returns 2
every { every {
spiedMiddleware.updateMessage( spiedMiddleware.updateMessage(
@ -291,7 +302,7 @@ class MessagingMiddlewareTest {
verify { spiedMiddleware.updateMessage(middlewareContext, oldMessage, updatedMessage) } verify { spiedMiddleware.updateMessage(middlewareContext, oldMessage, updatedMessage) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) } verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
verify { messagingStorage.updateMetadata(updatedMessage.metadata) } coVerify { messagingStorage.updateMetadata(updatedMessage.metadata) }
} }
@Test @Test
@ -308,6 +319,7 @@ class MessagingMiddlewareTest {
val updatedMessage = oldMessage.copy(metadata = oldMessage.metadata.copy(displayCount = 1)) val updatedMessage = oldMessage.copy(metadata = oldMessage.metadata.copy(displayCount = 1))
val spiedMiddleware = spyk(middleware) val spiedMiddleware = spyk(middleware)
every { spiedMiddleware.now() } returns 0
every { oldMessageData.maxDisplayCount } returns 1 every { oldMessageData.maxDisplayCount } returns 1
every { every {
spiedMiddleware.consumeMessageToShowIfNeeded( spiedMiddleware.consumeMessageToShowIfNeeded(
@ -322,6 +334,6 @@ class MessagingMiddlewareTest {
verify { spiedMiddleware.consumeMessageToShowIfNeeded(middlewareContext, oldMessage) } verify { spiedMiddleware.consumeMessageToShowIfNeeded(middlewareContext, oldMessage) }
verify { spiedMiddleware.removeMessage(middlewareContext, oldMessage) } verify { spiedMiddleware.removeMessage(middlewareContext, oldMessage) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) } verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
verify { messagingStorage.updateMetadata(updatedMessage.metadata) } coVerify { messagingStorage.updateMetadata(updatedMessage.metadata) }
} }
} }

Loading…
Cancel
Save