Skip to content

Commit

Permalink
RMF: shown message matching attribute implementation (#5303)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/1207619243206445/1208819791054807/f

### Description
Adds a new matching attribute to filter messages based on previously shown messages

### Steps to test this PR

_Feature 1_
- [x] Update RemoteService with `https://www.jsonblob.com/api/1309177955058114560`
- [x] Fresh install
- [x] Skip onboarding
- [x] Once in New Tab
- [x] Ensure you see Message1
- [x] Interact so it's dismissed
- [x] Fire button
- [x] Ensure you see Message3 (message2 should be excluded because Message1 was shown)
- [x] Interact so it's dismissed
- [x] Fire button
- [x] No message should be shown (message4 requires message2 to be shown, but that didn't happen)

### UI changes
| Before  | After |
| ------ | ----- |
!(Upload before screenshot)|(Upload after screenshot)|
  • Loading branch information
cmonfortep authored Nov 22, 2024
1 parent 0a1f156 commit 34863e5
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.duckduckgo.remote.messaging.api
import kotlinx.coroutines.flow.Flow

interface RemoteMessagingRepository {
fun getMessageById(id: String): RemoteMessage?
fun activeMessage(message: RemoteMessage?)
fun message(): RemoteMessage?
fun messageFlow(): Flow<RemoteMessage?>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ class AppRemoteMessagingRepository(
private val messageMapper: MessageMapper,
) : RemoteMessagingRepository {

override fun getMessageById(id: String): RemoteMessage? {
return remoteMessagesDao.messagesById(id)?.let {
messageMapper.fromMessage(it.message)
}
}

override fun activeMessage(message: RemoteMessage?) {
if (message == null) {
remoteMessagesDao.updateActiveMessageStateAndDeleteNeverShownMessages()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.remote.messaging.impl.matchers

import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.remote.messaging.api.AttributeMatcherPlugin
import com.duckduckgo.remote.messaging.api.JsonMatchingAttribute
import com.duckduckgo.remote.messaging.api.JsonToMatchingAttributeMapper
import com.duckduckgo.remote.messaging.api.MatchingAttribute
import com.duckduckgo.remote.messaging.api.RemoteMessagingRepository
import com.squareup.anvil.annotations.ContributesMultibinding
import dagger.SingleInstanceIn
import javax.inject.Inject

@ContributesMultibinding(
scope = AppScope::class,
boundType = JsonToMatchingAttributeMapper::class,
)
@ContributesMultibinding(
scope = AppScope::class,
boundType = AttributeMatcherPlugin::class,
)
@SingleInstanceIn(AppScope::class)
class ShownMessageMatcher @Inject constructor(
private val remoteMessagingRepository: RemoteMessagingRepository,
) : JsonToMatchingAttributeMapper, AttributeMatcherPlugin {
override fun map(
key: String,
jsonMatchingAttribute: JsonMatchingAttribute,
): MatchingAttribute? {
return when (key) {
"messageShown" -> {
val value = jsonMatchingAttribute.value
if (value is List<*>) {
val messageIds = value.filterIsInstance<String>()
if (messageIds.isNotEmpty()) {
return ShownMessageMatchingAttribute(messageIds)
}
}

return null
}

else -> null
}
}

override suspend fun evaluate(matchingAttribute: MatchingAttribute): Boolean? {
if (matchingAttribute is ShownMessageMatchingAttribute) {
assert(matchingAttribute.messageIds.isNotEmpty())

val currentMessage = remoteMessagingRepository.message()
matchingAttribute.messageIds.forEach {
if (currentMessage?.id != it) {
if (remoteMessagingRepository.didShow(it)) return true
}
}
return false
}
return null
}
}

data class ShownMessageMatchingAttribute(
val messageIds: List<String>,
) : MatchingAttribute
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package com.duckduckgo.remote.messaging.impl.matchers

import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.remote.messaging.api.JsonMatchingAttribute
import com.duckduckgo.remote.messaging.api.RemoteMessage
import com.duckduckgo.remote.messaging.api.RemoteMessagingRepository
import com.duckduckgo.remote.messaging.impl.AppRemoteMessagingRepositoryTest.Companion.aRemoteMessage
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

class ShownMessageMatcherTest {
@get:Rule
val coroutineTestRule: CoroutineTestRule = CoroutineTestRule()

private val remoteMessagingRepository: RemoteMessagingRepository = mock()

@Test
fun whenMapKeyIsMessageShownThenReturnMatchingAttribute() {
val matcher = ShownMessageMatcher(remoteMessagingRepository)
val jsonMatchingAttribute = JsonMatchingAttribute(value = listOf("1", "2", "3"))
val result = matcher.map("messageShown", jsonMatchingAttribute)
assertTrue(result is ShownMessageMatchingAttribute)
assertEquals(listOf("1", "2", "3"), (result as ShownMessageMatchingAttribute).messageIds)
}

@Test
fun whenJsonMatchingAttributeValueIsWrongTypeThenReturnNull() = runTest {
val matcher = ShownMessageMatcher(remoteMessagingRepository)
val jsonMatchingAttribute = JsonMatchingAttribute(value = listOf(1, true, 23L))
val result = matcher.map("messageShown", jsonMatchingAttribute)
assertNull(result)
}

@Test
fun whenJsonMatchingAttributeValueContainsWrongTypesThenReturnOnlyStringOnes() = runTest {
val matcher = ShownMessageMatcher(remoteMessagingRepository)
val jsonMatchingAttribute = JsonMatchingAttribute(value = listOf("1", true, 23L))
val result = matcher.map("messageShown", jsonMatchingAttribute)
assertEquals(listOf("1"), (result as ShownMessageMatchingAttribute).messageIds)
}

@Test
fun whenJsonMatchingAttributeValueIsNullThenReturnNull() = runTest {
val matcher = ShownMessageMatcher(remoteMessagingRepository)
val jsonMatchingAttribute = JsonMatchingAttribute(value = null)
val result = matcher.map("messageShown", jsonMatchingAttribute)
assertNull(result)
}

@Test
fun whenJsonMatchingAttributeValueIsEmptyThenReturnNull() = runTest {
val matcher = ShownMessageMatcher(remoteMessagingRepository)
val jsonMatchingAttribute = JsonMatchingAttribute(value = emptyList<String>())
val result = matcher.map("messageShown", jsonMatchingAttribute)
assertNull(result)
}

@Test
fun whenJsonMatchingAttributeValueIsNotListThenReturnNull() = runTest {
val matcher = ShownMessageMatcher(remoteMessagingRepository)
val jsonMatchingAttribute = JsonMatchingAttribute(value = 1)
val result = matcher.map("messageShown", jsonMatchingAttribute)
assertNull(result)
}

@Test
fun whenShownMessageIdMatchesThenReturnTrue() = runTest {
givenMessageIdShown(listOf("1", "2", "3"))
val matcher = ShownMessageMatcher(remoteMessagingRepository)
val matchingAttribute = ShownMessageMatchingAttribute(listOf("1", "2", "3"))
val result = matcher.evaluate(matchingAttribute)!!
assertTrue(result)
}

@Test
fun whenOneShownMessageMatchesThenReturnTrue() = runTest {
givenMessageIdShown(listOf("1", "2", "3"))
val matcher = ShownMessageMatcher(remoteMessagingRepository)
val matchingAttribute = ShownMessageMatchingAttribute(listOf("0", "1", "4"))
val result = matcher.evaluate(matchingAttribute)!!
assertTrue(result)
}

@Test
fun whenNoShownMessageMatchesThenReturnFalse() = runTest {
givenMessageIdShown(listOf("1", "2", "3"))
val matcher = ShownMessageMatcher(remoteMessagingRepository)
val matchingAttribute = ShownMessageMatchingAttribute(listOf("0", "4", "5"))
val result = matcher.evaluate(matchingAttribute)!!
assertFalse(result)
}

@Test
fun whenOnlyCurrentMessageIdMatchesThenReturnFalse() = runTest {
givenCurrentActiveMessage(aRemoteMessage("1"))
val matcher = ShownMessageMatcher(remoteMessagingRepository)
val matchingAttribute = ShownMessageMatchingAttribute(listOf("1"))
val result = matcher.evaluate(matchingAttribute)!!
assertFalse(result)
}

@Test
fun whenCurrentMessageAndOtherIdsMatchThenReturnTrue() = runTest {
givenMessageIdShown(listOf("2", "3"))
givenCurrentActiveMessage(aRemoteMessage("1"))
val matcher = ShownMessageMatcher(remoteMessagingRepository)
val matchingAttribute = ShownMessageMatchingAttribute(listOf("1", "2", "3"))
val result = matcher.evaluate(matchingAttribute)!!
assertTrue(result)
}

@Test(expected = AssertionError::class)
fun whenEmptyListThenThrowAssertionError() = runTest {
givenMessageIdShown(listOf("1", "2", "3"))
val matcher = ShownMessageMatcher(remoteMessagingRepository)
val matchingAttribute = ShownMessageMatchingAttribute(emptyList())
matcher.evaluate(matchingAttribute)
}

private fun givenCurrentActiveMessage(message: RemoteMessage) {
whenever(remoteMessagingRepository.message()).thenReturn(message)
}

private fun givenMessageIdShown(listOf: List<String>) {
listOf.forEach {
whenever(remoteMessagingRepository.didShow(it)).thenReturn(true)
}
}
}

0 comments on commit 34863e5

Please sign in to comment.