Revert "For #24222: Persist user interactions with nimbus messages"

This reverts commit 4389da78
This commit is contained in:
Arturo Mejia 2022-04-06 18:35:49 -04:00
parent 0e44ff7c9a
commit 22c1b9bc65
11 changed files with 237 additions and 517 deletions

View File

@ -156,6 +156,9 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
GlobalScope.launch(Dispatchers.IO) {
setStartupMetrics(store, settings())
}
if (FeatureFlags.messagingFeature && settings().isExperimentationEnabled) {
components.appStore.dispatch(AppAction.MessagingAction.Restore)
}
}
@CallSuper
@ -752,11 +755,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
)
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>) {
CustomizeHome.jumpBackIn.set(settings.showRecentTabsFeature)
CustomizeHome.recentlySaved.set(settings.showRecentBookmarksFeature)

View File

@ -26,8 +26,7 @@ import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.experiments.createNimbus
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.gleanplumb.CustomAttributeProvider
import org.mozilla.fenix.gleanplumb.OnDiskMessageMetadataStorage
import org.mozilla.fenix.gleanplumb.KeyPairMessageMetadataStorage
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.perf.lazyMonitored
@ -132,13 +131,12 @@ class Analytics(
val messagingStorage by lazyMonitored {
NimbusMessagingStorage(
context = context,
metadataStorage = OnDiskMessageMetadataStorage(context),
metadataStorage = KeyPairMessageMetadataStorage(),
gleanPlumb = experiments,
reportMalformedMessage = {
metrics.track(Event.Messaging.MessageMalformed(it))
},
messagingFeature = FxNimbus.features.messaging,
attributeProvider = CustomAttributeProvider,
)
}
}

View File

@ -0,0 +1,29 @@
/* 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) {
}
}

View File

@ -40,13 +40,11 @@ data class Message(
* @param displayCount Indicates how many times a message is displayed.
* @param pressed Indicates if a message has been clicked.
* @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(
val id: String,
val displayCount: Int = 0,
val pressed: Boolean = false,
val dismissed: Boolean = false,
val lastTimeShown: Long = 0L
val dismissed: Boolean = false
)
}

View File

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

View File

@ -12,7 +12,6 @@ import org.mozilla.experiments.nimbus.GleanPlumbInterface
import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper
import org.mozilla.experiments.nimbus.internal.FeatureHolder
import org.mozilla.experiments.nimbus.internal.NimbusException
import org.mozilla.fenix.nimbus.ControlMessageBehavior
import org.mozilla.fenix.nimbus.Messaging
import org.mozilla.fenix.nimbus.StyleData
@ -24,25 +23,26 @@ class NimbusMessagingStorage(
private val metadataStorage: MessageMetadataStorage,
private val reportMalformedMessage: (String) -> Unit,
private val gleanPlumb: GleanPlumbInterface,
private val messagingFeature: FeatureHolder<Messaging>,
private val attributeProvider: CustomAttributeProvider? = null
private val messagingFeature: FeatureHolder<Messaging>
) {
private val logger = Logger("MessagingStorage")
private val nimbusFeature = messagingFeature.value()
private val customAttributes: JSONObject
get() = attributeProvider?.getCustomAttributes(context) ?: JSONObject()
get() = JSONObject()
/**
* Returns a list of available messages descending sorted by their priority.
*/
suspend fun getMessages(): List<Message> {
fun getMessages(): List<Message> {
val nimbusTriggers = nimbusFeature.triggers
val nimbusStyles = nimbusFeature.styles
val nimbusActions = nimbusFeature.actions
val nimbusMessages = nimbusFeature.messages
val defaultStyle = StyleData(context)
val storageMetadata = metadataStorage.getMetadata()
val storageMetadata = metadataStorage.getMetadata().associateBy {
it.id
}
return nimbusMessages.mapNotNull { (key, value) ->
val action = sanitizeAction(key, value.action, nimbusActions) ?: return@mapNotNull null
@ -56,7 +56,7 @@ class NimbusMessagingStorage(
?: return@mapNotNull null
)
}.filter {
it.maxDisplayCount >= it.metadata.displayCount &&
it.data.maxDisplayCount >= it.metadata.displayCount &&
!it.metadata.dismissed &&
!it.metadata.pressed
}.sortedByDescending {
@ -68,33 +68,21 @@ class NimbusMessagingStorage(
* Returns the next higher priority message which all their triggers are true.
*/
fun getNextMessage(availableMessages: List<Message>): Message? {
val jexlCache = HashMap<String, Boolean>()
val helper = gleanPlumb.createMessageHelper(customAttributes)
val message = availableMessages.firstOrNull {
isMessageEligible(it, helper, jexlCache)
var message = availableMessages.firstOrNull {
isMessageEligible(it, helper)
} ?: return null
// Check this isn't an experimental message. If not, we can go ahead and return it.
if (!isMessageUnderExperiment(message, nimbusFeature.messageUnderExperiment)) {
return message
}
// If the message is under experiment, then we need to record the exposure
messagingFeature.recordExposure()
if (isMessageUnderExperiment(message, nimbusFeature.messageUnderExperiment)) {
messagingFeature.recordExposure()
// If this is an experimental message, but not a placebo, then just return the message.
return if (!message.data.isControl) {
message
} else {
// This is a control, so we need to either return the next message (there may not be one)
// or not display anything.
when (getOnControlBehavior()) {
ControlMessageBehavior.SHOW_NEXT_MESSAGE -> availableMessages.firstOrNull {
// There should only be one control message, and we've just detected it.
!it.data.isControl && isMessageEligible(it, helper, jexlCache)
}
ControlMessageBehavior.SHOW_NONE -> null
if (message.data.isControl) {
message = availableMessages.firstOrNull {
!it.data.isControl && isMessageEligible(it, helper)
} ?: return null
}
}
return message
}
/**
@ -110,7 +98,7 @@ class NimbusMessagingStorage(
/**
* Updated the provided [metadata] in the storage.
*/
suspend fun updateMetadata(metadata: Message.Metadata) {
fun updateMetadata(metadata: Message.Metadata) {
metadataStorage.updateMetadata(metadata)
}
@ -150,7 +138,7 @@ class NimbusMessagingStorage(
@VisibleForTesting
internal fun isMessageUnderExperiment(message: Message, expression: String?): Boolean {
return message.data.isControl || when {
return when {
expression.isNullOrBlank() -> {
false
}
@ -166,27 +154,21 @@ class NimbusMessagingStorage(
@VisibleForTesting
internal fun isMessageEligible(
message: Message,
helper: GleanPlumbMessageHelper,
jexlCache: MutableMap<String, Boolean> = mutableMapOf()
helper: GleanPlumbMessageHelper
): Boolean {
return message.triggers.all { condition ->
jexlCache[condition]
?: try {
helper.evalJexl(condition).also { result ->
jexlCache[condition] = result
}
} catch (e: NimbusException.EvaluationException) {
reportMalformedMessage(message.id)
logger.info("Unable to evaluate $condition")
false
}
try {
helper.evalJexl(condition)
} catch (e: NimbusException.EvaluationException) {
reportMalformedMessage(message.id)
logger.info("Unable to evaluate $condition")
false
}
}
}
@VisibleForTesting
internal fun getOnControlBehavior(): ControlMessageBehavior = nimbusFeature.onControl
private suspend fun addMetadata(id: String): Message.Metadata {
private fun addMetadata(id: String): Message.Metadata {
// This will be improve on https://github.com/mozilla-mobile/fenix/issues/24222
return metadataStorage.addMetadata(
Message.Metadata(
id = id,

View File

@ -1,95 +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
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")
)
}

View File

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

View File

@ -4,12 +4,11 @@
package org.mozilla.fenix.gleanplumb
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@ -24,7 +23,6 @@ import org.mozilla.experiments.nimbus.internal.FeatureHolder
import org.mozilla.experiments.nimbus.internal.NimbusException
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.ControlMessageBehavior.SHOW_NEXT_MESSAGE
import org.mozilla.fenix.nimbus.MessageData
import org.mozilla.fenix.nimbus.Messaging
import org.mozilla.fenix.nimbus.StyleData
@ -43,6 +41,7 @@ class NimbusMessagingStorageTest {
private val reportMalformedMessage: (String) -> Unit = {
malformedWasReported = true
}
@Before
fun setup() {
gleanPlumb = mockk(relaxed = true)
@ -50,7 +49,7 @@ class NimbusMessagingStorageTest {
malformedWasReported = false
messagingFeature = createMessagingFeature()
coEvery { metadataStorage.getMetadata() } returns mapOf("message-1" to Message.Metadata(id = "message-1"))
every { metadataStorage.getMetadata() } returns listOf(Message.Metadata(id = "message-1"))
storage = NimbusMessagingStorage(
testContext,
@ -62,7 +61,7 @@ class NimbusMessagingStorageTest {
}
@Test
fun `WHEN calling getMessages THEN provide a list of available messages`() = runBlockingTest {
fun `WHEN calling getMessages THEN provide a list of available messages`() {
val message = storage.getMessages().first()
assertEquals("message-1", message.id)
@ -70,160 +69,150 @@ class NimbusMessagingStorageTest {
}
@Test
fun `WHEN calling getMessages THEN provide a list of sorted messages by priority`() =
runBlockingTest {
val messages = mapOf(
"low-message" to createMessageData(style = "low-priority"),
"high-message" to createMessageData(style = "high-priority"),
"medium-message" to createMessageData(style = "medium-priority"),
)
val styles = mapOf(
"high-priority" to createStyle(priority = 100),
"medium-priority" to createStyle(priority = 50),
"low-priority" to createStyle(priority = 1)
)
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
val messagingFeature = createMessagingFeature(
styles = styles,
messages = messages
)
fun `WHEN calling getMessages THEN provide a list of sorted messages by priority`() {
val messages = mapOf(
"low-message" to createMessageData(style = "low-priority"),
"high-message" to createMessageData(style = "high-priority"),
"medium-message" to createMessageData(style = "medium-priority"),
)
val styles = mapOf(
"high-priority" to createStyle(priority = 100),
"medium-priority" to createStyle(priority = 50),
"low-priority" to createStyle(priority = 1)
)
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
val messagingFeature = createMessagingFeature(
styles = styles,
messages = messages
)
coEvery { metadataStorage.getMetadata() } returns mapOf(
"message-1" to Message.Metadata(
id = "message-1"
)
)
every { metadataStorage.getMetadata() } returns listOf(Message.Metadata(id = "message-1"))
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
reportMalformedMessage,
gleanPlumb,
messagingFeature
)
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
reportMalformedMessage,
gleanPlumb,
messagingFeature
)
val results = storage.getMessages()
val results = storage.getMessages()
assertEquals("high-message", results[0].id)
assertEquals("medium-message", results[1].id)
assertEquals("low-message", results[2].id)
}
assertEquals("high-message", results[0].id)
assertEquals("medium-message", results[1].id)
assertEquals("low-message", results[2].id)
}
@Test
fun `GIVEN pressed message WHEN calling getMessages THEN filter out the pressed message`() =
runBlockingTest {
val metadataList = mapOf(
"pressed-message" to Message.Metadata(id = "pressed-message", pressed = true),
"normal-message" to Message.Metadata(id = "normal-message", pressed = false)
)
val messages = mapOf(
"pressed-message" to createMessageData(style = "high-priority"),
"normal-message" to createMessageData(style = "high-priority"),
)
val styles = mapOf(
"high-priority" to createStyle(priority = 100),
)
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
val messagingFeature = createMessagingFeature(
styles = styles,
messages = messages
)
fun `GIVEN pressed message WHEN calling getMessages THEN filter out the pressed message`() {
val metadataList = listOf(
Message.Metadata(id = "pressed-message", pressed = true),
Message.Metadata(id = "normal-message", pressed = false)
)
val messages = mapOf(
"pressed-message" to createMessageData(style = "high-priority"),
"normal-message" to createMessageData(style = "high-priority"),
)
val styles = mapOf(
"high-priority" to createStyle(priority = 100),
)
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
val messagingFeature = createMessagingFeature(
styles = styles,
messages = messages
)
coEvery { metadataStorage.getMetadata() } returns metadataList
every { metadataStorage.getMetadata() } returns metadataList
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
reportMalformedMessage,
gleanPlumb,
messagingFeature
)
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
reportMalformedMessage,
gleanPlumb,
messagingFeature
)
val results = storage.getMessages()
val results = storage.getMessages()
assertEquals(1, results.size)
assertEquals("normal-message", results[0].id)
}
assertEquals(1, results.size)
assertEquals("normal-message", results[0].id)
}
@Test
fun `GIVEN dismissed message WHEN calling getMessages THEN filter out the dismissed message`() =
runBlockingTest {
val metadataList = mapOf(
"dismissed-message" to Message.Metadata(id = "dismissed-message", dismissed = true),
"normal-message" to Message.Metadata(id = "normal-message", dismissed = false)
)
val messages = mapOf(
"dismissed-message" to createMessageData(style = "high-priority"),
"normal-message" to createMessageData(style = "high-priority"),
)
val styles = mapOf(
"high-priority" to createStyle(priority = 100),
)
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
val messagingFeature = createMessagingFeature(
styles = styles,
messages = messages
)
fun `GIVEN dismissed message WHEN calling getMessages THEN filter out the dismissed message`() {
val metadataList = listOf(
Message.Metadata(id = "dismissed-message", dismissed = true),
Message.Metadata(id = "normal-message", dismissed = false)
)
val messages = mapOf(
"dismissed-message" to createMessageData(style = "high-priority"),
"normal-message" to createMessageData(style = "high-priority"),
)
val styles = mapOf(
"high-priority" to createStyle(priority = 100),
)
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
val messagingFeature = createMessagingFeature(
styles = styles,
messages = messages
)
coEvery { metadataStorage.getMetadata() } returns metadataList
every { metadataStorage.getMetadata() } returns metadataList
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
reportMalformedMessage,
gleanPlumb,
messagingFeature
)
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
reportMalformedMessage,
gleanPlumb,
messagingFeature
)
val results = storage.getMessages()
val results = storage.getMessages()
assertEquals(1, results.size)
assertEquals("normal-message", results[0].id)
}
assertEquals(1, results.size)
assertEquals("normal-message", results[0].id)
}
@Test
fun `GIVEN a message that the maxDisplayCount WHEN calling getMessages THEN filter out the message`() =
runBlockingTest {
val metadataList = mapOf(
"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(
"shown-many-times-message" to createMessageData(
style = "high-priority"
),
"normal-message" to createMessageData(style = "high-priority"),
)
val styles = mapOf(
"high-priority" to createStyle(priority = 100, maxDisplayCount = 2),
)
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
val messagingFeature = createMessagingFeature(
styles = styles,
messages = messages
)
fun `GIVEN a message that the maxDisplayCount WHEN calling getMessages THEN filter out the message`() {
val metadataList = listOf(
Message.Metadata(id = "shown-many-times-message", displayCount = 10),
Message.Metadata(id = "normal-message", displayCount = 0)
)
val messages = mapOf(
"shown-many-times-message" to createMessageData(
style = "high-priority",
maxDisplayCount = 2
),
"normal-message" to createMessageData(style = "high-priority"),
)
val styles = mapOf(
"high-priority" to createStyle(priority = 100),
)
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
val messagingFeature = createMessagingFeature(
styles = styles,
messages = messages
)
coEvery { metadataStorage.getMetadata() } returns metadataList
every { metadataStorage.getMetadata() } returns metadataList
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
reportMalformedMessage,
gleanPlumb,
messagingFeature
)
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
reportMalformedMessage,
gleanPlumb,
messagingFeature
)
val results = storage.getMessages()
val results = storage.getMessages()
assertEquals(1, results.size)
assertEquals("normal-message", results[0].id)
}
assertEquals(1, results.size)
assertEquals("normal-message", results[0].id)
}
@Test
fun `GIVEN a malformed message WHEN calling getMessages THEN provide a list of messages ignoring the malformed one`() = runBlockingTest {
fun `GIVEN a malformed message WHEN calling getMessages THEN provide a list of messages ignoring the malformed one`() {
val messages = storage.getMessages()
val firstMessage = messages.first()
@ -248,11 +237,11 @@ class NimbusMessagingStorageTest {
}
@Test
fun `WHEN calling updateMetadata THEN delegate to metadataStorage`() = runBlockingTest {
fun `WHEN calling updateMetadata THEN delegate to metadataStorage`() {
storage.updateMetadata(mockk(relaxed = true))
storage.updateMetadata(mockk())
coEvery { metadataStorage.updateMetadata(any()) }
verify { metadataStorage.updateMetadata(any()) }
}
@Test
@ -291,10 +280,9 @@ class NimbusMessagingStorageTest {
@Test
fun `GIVEN a null or black expression WHEN calling isMessageUnderExperiment THEN return false`() {
val message = Message(
"id",
mockk(relaxed = true),
"id", mockk(),
action = "action",
mockk(relaxed = true),
mock(),
emptyList(),
Message.Metadata("id")
)
@ -307,10 +295,9 @@ class NimbusMessagingStorageTest {
@Test
fun `GIVEN messages id that ends with - WHEN calling isMessageUnderExperiment THEN return true`() {
val message = Message(
"end-",
mockk(relaxed = true),
"end-", mockk(),
action = "action",
mockk(relaxed = true),
mock(),
emptyList(),
Message.Metadata("end-")
)
@ -323,10 +310,9 @@ class NimbusMessagingStorageTest {
@Test
fun `GIVEN message under experiment WHEN calling isMessageUnderExperiment THEN return true`() {
val message = Message(
"same-id",
mockk(relaxed = true),
"same-id", mockk(),
action = "action",
mockk(relaxed = true),
mock(),
emptyList(),
Message.Metadata("same-id")
)
@ -340,10 +326,9 @@ class NimbusMessagingStorageTest {
fun `GIVEN an eligible message WHEN calling isMessageEligible THEN return true`() {
val helper: GleanPlumbMessageHelper = mockk(relaxed = true)
val message = Message(
"same-id",
mockk(relaxed = true),
"same-id", mockk(),
action = "action",
mockk(relaxed = true),
mock(),
listOf("trigger"),
Message.Metadata("same-id")
)
@ -359,10 +344,9 @@ class NimbusMessagingStorageTest {
fun `GIVEN a malformed trigger WHEN calling isMessageEligible THEN return false`() {
val helper: GleanPlumbMessageHelper = mockk(relaxed = true)
val message = Message(
"same-id",
mockk(relaxed = true),
"same-id", mockk(),
action = "action",
mockk(relaxed = true),
mock(),
listOf("trigger"),
Message.Metadata("same-id")
)
@ -378,10 +362,9 @@ class NimbusMessagingStorageTest {
fun `GIVEN none available messages are eligible WHEN calling getNextMessage THEN return null`() {
val spiedStorage = spyk(storage)
val message = Message(
"same-id",
mockk(relaxed = true),
"same-id", mockk(),
action = "action",
mockk(relaxed = true),
mock(),
listOf("trigger"),
Message.Metadata("same-id")
)
@ -397,10 +380,9 @@ class NimbusMessagingStorageTest {
fun `GIVEN an eligible message WHEN calling getNextMessage THEN return the message`() {
val spiedStorage = spyk(storage)
val message = Message(
"same-id",
mockk(relaxed = true),
"same-id", mockk(),
action = "action",
mockk(relaxed = true),
mock(),
listOf("trigger"),
Message.Metadata("same-id")
)
@ -445,7 +427,6 @@ class NimbusMessagingStorageTest {
val controlMessageData: MessageData = mockk(relaxed = true)
every { messageData.isControl } returns false
every { spiedStorage.getOnControlBehavior() } returns SHOW_NEXT_MESSAGE
every { controlMessageData.isControl } returns true
val message = Message(
@ -478,12 +459,14 @@ class NimbusMessagingStorageTest {
private fun createMessageData(
action: String = "action-1",
style: String = "style-1",
triggers: List<String> = listOf("trigger-1")
triggers: List<String> = listOf("trigger-1"),
maxDisplayCount: Int = 5
): MessageData {
val messageData1: MessageData = mockk(relaxed = true)
every { messageData1.action } returns action
every { messageData1.style } returns style
every { messageData1.trigger } returns triggers
every { messageData1.maxDisplayCount } returns maxDisplayCount
return messageData1
}
@ -509,10 +492,9 @@ class NimbusMessagingStorageTest {
return messagingFeature
}
private fun createStyle(priority: Int = 1, maxDisplayCount: Int = 5): StyleData {
private fun createStyle(priority: Int = 1): StyleData {
val style1: StyleData = mockk(relaxed = true)
every { style1.priority } returns priority
every { style1.maxDisplayCount } returns maxDisplayCount
return style1
}
}

View File

@ -1,144 +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
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])
}
}

View File

@ -5,14 +5,11 @@
package org.mozilla.fenix.gleanplumb.state
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.lib.state.MiddlewareContext
import mozilla.components.service.glean.testing.GleanTestRule
import mozilla.components.support.test.robolectric.testContext
@ -37,12 +34,10 @@ import org.mozilla.fenix.gleanplumb.MessagingState
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.MessageData
import org.mozilla.fenix.nimbus.StyleData
@RunWith(FenixRobolectricTestRunner::class)
class MessagingMiddlewareTest {
private val coroutineScope = TestCoroutineScope()
private lateinit var store: AppStore
private lateinit var middleware: MessagingMiddleware
private lateinit var messagingStorage: NimbusMessagingStorage
@ -53,14 +48,10 @@ class MessagingMiddlewareTest {
@Before
fun setUp() {
store = mockk(relaxed = true)
messagingStorage = mockk(relaxed = true)
middlewareContext = mockk(relaxed = true)
every { middlewareContext.store } returns store
middleware = MessagingMiddleware(
messagingStorage,
coroutineScope
messagingStorage
)
}
@ -68,11 +59,11 @@ class MessagingMiddlewareTest {
fun `WHEN Restore THEN get messages from the storage and UpdateMessages`() {
val messages: List<Message> = emptyList()
coEvery { messagingStorage.getMessages() } returns messages
every { messagingStorage.getMessages() } returns messages
middleware.invoke(middlewareContext, {}, Restore)
verify { store.dispatch(UpdateMessages(messages)) }
verify { middlewareContext.dispatch(UpdateMessages(messages)) }
}
@Test
@ -110,7 +101,7 @@ class MessagingMiddlewareTest {
middleware.invoke(middlewareContext, {}, MessageClicked(message))
coVerify { messagingStorage.updateMetadata(message.metadata.copy(pressed = true)) }
verify { messagingStorage.updateMetadata(message.metadata.copy(pressed = true)) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
}
@ -136,7 +127,7 @@ class MessagingMiddlewareTest {
MessageDismissed(message)
)
coVerify { messagingStorage.updateMetadata(message.metadata.copy(dismissed = true)) }
verify { messagingStorage.updateMetadata(message.metadata.copy(dismissed = true)) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
}
@ -152,19 +143,17 @@ class MessagingMiddlewareTest {
)
val appState: AppState = mockk(relaxed = true)
val messagingState: MessagingState = mockk(relaxed = true)
val spiedMiddleware = spyk(middleware)
every { spiedMiddleware.now() } returns 0L
every { messagingState.messages } returns emptyList()
every { appState.messaging } returns messagingState
every { middlewareContext.state } returns appState
spiedMiddleware.invoke(
middleware.invoke(
middlewareContext, {},
MessageDisplayed(message)
)
coVerify { messagingStorage.updateMetadata(message.metadata.copy(displayCount = 1)) }
verify { messagingStorage.updateMetadata(message.metadata.copy(displayCount = 1)) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
}
@ -186,7 +175,7 @@ class MessagingMiddlewareTest {
spiedMiddleware.onMessageDismissed(middlewareContext, message)
coVerify { messagingStorage.updateMetadata(message.metadata.copy(dismissed = true)) }
verify { messagingStorage.updateMetadata(message.metadata.copy(dismissed = true)) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
verify { spiedMiddleware.removeMessage(middlewareContext, message) }
}
@ -277,21 +266,19 @@ class MessagingMiddlewareTest {
@Test
fun `GIVEN a message with that not surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN update the available messages and the updateMetadata`() {
val style: StyleData = mockk(relaxed = true)
val oldMessageData: MessageData = mockk(relaxed = true)
val oldMessage = Message(
"oldMessage",
oldMessageData,
action = "action",
style,
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id", displayCount = 0)
)
val updatedMessage = oldMessage.copy(metadata = oldMessage.metadata.copy(displayCount = 1))
val spiedMiddleware = spyk(middleware)
every { spiedMiddleware.now() } returns 0
every { style.maxDisplayCount } returns 2
every { oldMessageData.maxDisplayCount } returns 2
every {
spiedMiddleware.updateMessage(
middlewareContext,
@ -304,26 +291,24 @@ class MessagingMiddlewareTest {
verify { spiedMiddleware.updateMessage(middlewareContext, oldMessage, updatedMessage) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
coVerify { messagingStorage.updateMetadata(updatedMessage.metadata) }
verify { messagingStorage.updateMetadata(updatedMessage.metadata) }
}
@Test
fun `GIVEN a message with that surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN remove the message and consume it`() {
val style: StyleData = mockk(relaxed = true)
val oldMessageData: MessageData = mockk(relaxed = true)
val oldMessage = Message(
"oldMessage",
oldMessageData,
action = "action",
style,
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id", displayCount = 0)
)
val updatedMessage = oldMessage.copy(metadata = oldMessage.metadata.copy(displayCount = 1))
val spiedMiddleware = spyk(middleware)
every { spiedMiddleware.now() } returns 0
every { style.maxDisplayCount } returns 1
every { oldMessageData.maxDisplayCount } returns 1
every {
spiedMiddleware.consumeMessageToShowIfNeeded(
middlewareContext,
@ -337,6 +322,6 @@ class MessagingMiddlewareTest {
verify { spiedMiddleware.consumeMessageToShowIfNeeded(middlewareContext, oldMessage) }
verify { spiedMiddleware.removeMessage(middlewareContext, oldMessage) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
coVerify { messagingStorage.updateMetadata(updatedMessage.metadata) }
verify { messagingStorage.updateMetadata(updatedMessage.metadata) }
}
}