Skip to content

Commit 2e761f9

Browse files
authored
PIR: Handle removal of profiles from dashboard (#7172)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1211859301335913?focus=true ### Description Handles removal of profiles from the dashboard and deprecation of the extracted profiles and opt out jobs. ### Steps to test this PR See https://app.asana.com/1/137249556945/task/1211991562624989?focus=true ### UI changes No UI changes
1 parent 95a639d commit 2e761f9

File tree

8 files changed

+130
-14
lines changed

8 files changed

+130
-14
lines changed

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/dashboard/messaging/handlers/PirRemoveOptOutFromDashboardMessageHandler.kt

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@
1616

1717
package com.duckduckgo.pir.impl.dashboard.messaging.handlers
1818

19+
import com.duckduckgo.app.di.AppCoroutineScope
20+
import com.duckduckgo.common.utils.DispatcherProvider
1921
import com.duckduckgo.di.scopes.ActivityScope
2022
import com.duckduckgo.js.messaging.api.JsMessage
2123
import com.duckduckgo.js.messaging.api.JsMessageCallback
2224
import com.duckduckgo.js.messaging.api.JsMessaging
2325
import com.duckduckgo.pir.impl.dashboard.messaging.PirDashboardWebMessages
2426
import com.duckduckgo.pir.impl.dashboard.messaging.model.PirWebMessageRequest
2527
import com.duckduckgo.pir.impl.dashboard.messaging.model.PirWebMessageResponse
28+
import com.duckduckgo.pir.impl.scheduling.JobRecordUpdater
2629
import com.squareup.anvil.annotations.ContributesMultibinding
30+
import kotlinx.coroutines.CoroutineScope
31+
import kotlinx.coroutines.launch
2732
import logcat.logcat
2833
import javax.inject.Inject
2934

@@ -34,7 +39,11 @@ import javax.inject.Inject
3439
scope = ActivityScope::class,
3540
boundType = PirWebJsMessageHandler::class,
3641
)
37-
class PirRemoveOptOutFromDashboardMessageHandler @Inject constructor() : PirWebJsMessageHandler() {
42+
class PirRemoveOptOutFromDashboardMessageHandler @Inject constructor(
43+
private val jobRecordUpdater: JobRecordUpdater,
44+
private val dispatcherProvider: DispatcherProvider,
45+
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
46+
) : PirWebJsMessageHandler() {
3847

3948
override val message = PirDashboardWebMessages.REMOVE_OPT_OUT_FROM_DASHBOARD
4049

@@ -58,12 +67,14 @@ class PirRemoveOptOutFromDashboardMessageHandler @Inject constructor() : PirWebJ
5867
return
5968
}
6069

61-
logcat { "PIR-WEB: PirRemoveOptOutFromDashboardMessageHandler: removing recordId=$recordId" }
70+
appCoroutineScope.launch(dispatcherProvider.io()) {
71+
jobRecordUpdater.markRecordsAsRemovedByUser(extractedProfileId = recordId)
6272

63-
// TODO: Implement actual removal logic
64-
jsMessaging.sendResponse(
65-
jsMessage = jsMessage,
66-
response = PirWebMessageResponse.DefaultResponse.SUCCESS,
67-
)
73+
logcat { "PIR-WEB: PirRemoveOptOutFromDashboardMessageHandler: successfully removed recordId=$recordId" }
74+
jsMessaging.sendResponse(
75+
jsMessage = jsMessage,
76+
response = PirWebMessageResponse.DefaultResponse.SUCCESS,
77+
)
78+
}
6879
}
6980
}

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/models/scheduling/JobRecord.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ sealed class JobRecord(
7676

7777
/** The job is waiting for email confirmation to complete before we can move it to [REQUESTED]. */
7878
PENDING_EMAIL_CONFIRMATION,
79+
80+
/** The profile was removed from the dashboard by the user. */
81+
REMOVED_BY_USER,
7982
}
8083
}
8184

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/scheduling/JobRecordUpdater.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,15 @@ interface JobRecordUpdater {
234234
* that have an extracted profile associated to it to continue running scan jobs on them.
235235
*/
236236
suspend fun removeScanJobRecordsWithNoMatchesForProfiles(profileQueryIds: List<Long>)
237+
238+
/**
239+
* Marks the [ExtractedProfile], associated [OptOutJobRecord], and [EmailConfirmationJobRecord] as removed by user.
240+
*
241+
* This method should be called when the user removes an extracted profile from the dashboard.
242+
*
243+
* @param extractedProfileId The id of the [ExtractedProfile] to be marked as removed by user
244+
*/
245+
suspend fun markRecordsAsRemovedByUser(extractedProfileId: Long)
237246
}
238247

239248
@ContributesBinding(AppScope::class)
@@ -477,6 +486,29 @@ class RealJobRecordUpdater @Inject constructor(
477486
}
478487
}
479488

489+
override suspend fun markRecordsAsRemovedByUser(extractedProfileId: Long) {
490+
withContext(dispatcherProvider.io()) {
491+
repository.markExtractedProfileAsDeprecated(extractedProfileId)
492+
493+
// update the OptOutJobRecord for this ExtractedProfile (if it exists)
494+
schedulingRepository.getValidOptOutJobRecord(extractedProfileId)?.run {
495+
schedulingRepository.saveOptOutJobRecord(
496+
copy(
497+
status = OptOutJobStatus.REMOVED_BY_USER,
498+
deprecated = true,
499+
).also {
500+
logcat { "PIR-JOB-RECORD: Updated OptOutJobRecord for $extractedProfileId to REMOVED_BY_USER: $it" }
501+
},
502+
)
503+
}
504+
505+
// delete the EmailConfirmationJobRecord for this ExtractedProfile (if it exists)
506+
schedulingRepository.deleteEmailConfirmationJobRecord(extractedProfileId).also {
507+
logcat { "PIR-JOB-RECORD: Delete EmailConfirmationJobRecord for $extractedProfileId" }
508+
}
509+
}
510+
}
511+
480512
private data class ExtractedProfileComparisonKey(
481513
val profileQueryId: Long,
482514
val brokerName: String,

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/store/PirRepository.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ interface PirRepository {
143143

144144
suspend fun getAllExtractedProfiles(): List<ExtractedProfile>
145145

146+
suspend fun markExtractedProfileAsDeprecated(extractedProfileId: Long)
147+
146148
suspend fun getUserProfileQuery(id: Long): ProfileQuery?
147149

148150
/**
@@ -552,6 +554,12 @@ class RealPirRepository(
552554
}.orEmpty()
553555
}
554556

557+
override suspend fun markExtractedProfileAsDeprecated(extractedProfileId: Long) {
558+
withContext(dispatcherProvider.io()) {
559+
extractedProfileDao()?.updateExtractedProfileDeprecated(extractedProfileId, deprecated = true)
560+
}
561+
}
562+
555563
override suspend fun getUserProfileQuery(id: Long): ProfileQuery? =
556564
withContext(dispatcherProvider.io()) {
557565
userProfileDao()?.getUserProfile(id)?.toProfileQuery()

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/store/db/ExtractedProfileDao.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,10 @@ interface ExtractedProfileDao {
5353

5454
@Query("DELETE from pir_extracted_profiles")
5555
fun deleteAllExtractedProfiles()
56+
57+
@Query("UPDATE pir_extracted_profiles SET deprecated = :deprecated WHERE id = :extractedProfileId")
58+
fun updateExtractedProfileDeprecated(
59+
extractedProfileId: Long,
60+
deprecated: Boolean,
61+
)
5662
}

pir/pir-impl/src/test/java/com/duckduckgo/pir/impl/dashboard/messaging/handlers/PirRemoveOptOutFromDashboardMessageHandlerTest.kt

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,41 @@
1717
package com.duckduckgo.pir.impl.dashboard.messaging.handlers
1818

1919
import androidx.test.ext.junit.runners.AndroidJUnit4
20+
import com.duckduckgo.common.test.CoroutineTestRule
2021
import com.duckduckgo.js.messaging.api.JsMessageCallback
2122
import com.duckduckgo.js.messaging.api.JsMessaging
2223
import com.duckduckgo.pir.impl.dashboard.messaging.PirDashboardWebMessages
2324
import com.duckduckgo.pir.impl.dashboard.messaging.handlers.PirMessageHandlerUtils.createJsMessage
2425
import com.duckduckgo.pir.impl.dashboard.messaging.handlers.PirMessageHandlerUtils.verifyResponse
26+
import com.duckduckgo.pir.impl.scheduling.JobRecordUpdater
27+
import kotlinx.coroutines.test.runTest
2528
import org.junit.Assert.assertEquals
2629
import org.junit.Before
30+
import org.junit.Rule
2731
import org.junit.Test
2832
import org.junit.runner.RunWith
2933
import org.mockito.kotlin.mock
34+
import org.mockito.kotlin.verify
3035

3136
@RunWith(AndroidJUnit4::class)
3237
class PirRemoveOptOutFromDashboardMessageHandlerTest {
3338

39+
@get:Rule
40+
val coroutineRule = CoroutineTestRule()
41+
3442
private lateinit var testee: PirRemoveOptOutFromDashboardMessageHandler
3543

44+
private val mockJobRecordUpdater: JobRecordUpdater = mock()
3645
private val mockJsMessaging: JsMessaging = mock()
3746
private val mockJsMessageCallback: JsMessageCallback = mock()
3847

3948
@Before
4049
fun setUp() {
41-
testee = PirRemoveOptOutFromDashboardMessageHandler()
50+
testee = PirRemoveOptOutFromDashboardMessageHandler(
51+
jobRecordUpdater = mockJobRecordUpdater,
52+
dispatcherProvider = coroutineRule.testDispatcherProvider,
53+
appCoroutineScope = coroutineRule.testScope,
54+
)
4255
}
4356

4457
@Test
@@ -47,22 +60,24 @@ class PirRemoveOptOutFromDashboardMessageHandlerTest {
4760
}
4861

4962
@Test
50-
fun whenProcessWithValidRecordIdThenSendsSuccessResponse() {
63+
fun whenProcessWithValidRecordIdThenCallsJobRecordUpdaterAndSendsSuccessResponse() = runTest {
5164
// Given
65+
val testRecordId = 123L
5266
val jsMessage = createJsMessage(
53-
paramsJson = """{"recordId": 2}""",
67+
paramsJson = """{"recordId": $testRecordId}""",
5468
method = PirDashboardWebMessages.REMOVE_OPT_OUT_FROM_DASHBOARD,
5569
)
5670

5771
// When
5872
testee.process(jsMessage, mockJsMessaging, mockJsMessageCallback)
5973

6074
// Then
75+
verify(mockJobRecordUpdater).markRecordsAsRemovedByUser(testRecordId)
6176
verifyResponse(jsMessage, true, mockJsMessaging)
6277
}
6378

6479
@Test
65-
fun whenProcessWithMissingRecordIdThenSendsErrorResponse() {
80+
fun whenProcessWithMissingRecordIdThenSendsErrorResponseWithoutCallingJobRecordUpdater() = runTest {
6681
// Given
6782
val jsMessage = createJsMessage(
6883
paramsJson = """{}""",
@@ -74,5 +89,6 @@ class PirRemoveOptOutFromDashboardMessageHandlerTest {
7489

7590
// Then
7691
verifyResponse(jsMessage, false, mockJsMessaging)
92+
verify(mockJobRecordUpdater, org.mockito.kotlin.never()).markRecordsAsRemovedByUser(org.mockito.kotlin.any())
7793
}
7894
}

pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/scheduling/RealJobRecordUpdaterTest.kt

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -795,13 +795,42 @@ class RealJobRecordUpdaterTest {
795795
@Test
796796
fun whenRemoveJobRecordsForProfileWithExclusionsThenDeletesJobRecordsExceptExcluded() =
797797
runTest {
798-
val brokersToExclude = listOf("broker1", "broker2")
799-
800798
toTest.removeScanJobRecordsWithNoMatchesForProfiles(listOf(testProfileQueryId))
801799

802800
verify(mockSchedulingRepository).deleteScanJobRecordsWithoutMatchesForProfiles(listOf(testProfileQueryId))
803801
}
804802

803+
@Test
804+
fun whenMarkRecordsAsRemovedByUserThenMarksExtractedProfileAndOptOutJobRecordAndEmailConfirmationJobRecordAsDeprecated() =
805+
runTest {
806+
whenever(mockSchedulingRepository.getValidOptOutJobRecord(testExtractedProfileId))
807+
.thenReturn(testOptOutJobRecord)
808+
809+
toTest.markRecordsAsRemovedByUser(testExtractedProfileId)
810+
811+
verify(mockRepository).markExtractedProfileAsDeprecated(testExtractedProfileId)
812+
verify(mockSchedulingRepository).saveOptOutJobRecord(
813+
testOptOutJobRecord.copy(
814+
status = OptOutJobStatus.REMOVED_BY_USER,
815+
deprecated = true,
816+
),
817+
)
818+
verify(mockSchedulingRepository).deleteEmailConfirmationJobRecord(testExtractedProfileId)
819+
}
820+
821+
@Test
822+
fun whenMarkRecordsAsRemovedByUserAndOptOutJobRecordAndEmailConfirmationDoesNotExistThenOnlyMarksExtractedProfileAsDeprecated() =
823+
runTest {
824+
whenever(mockSchedulingRepository.getValidOptOutJobRecord(testExtractedProfileId))
825+
.thenReturn(null)
826+
827+
toTest.markRecordsAsRemovedByUser(testExtractedProfileId)
828+
829+
verify(mockRepository).markExtractedProfileAsDeprecated(testExtractedProfileId)
830+
verify(mockSchedulingRepository, never()).saveOptOutJobRecord(any())
831+
verify(mockSchedulingRepository).deleteEmailConfirmationJobRecord(testExtractedProfileId)
832+
}
833+
805834
companion object {
806835
private const val TEST_CURRENT_TIME = 5000L
807836
}

pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/store/RealPirRepositoryTest.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package com.duckduckgo.pir.impl.store
1818

1919
import com.duckduckgo.common.test.CoroutineTestRule
2020
import com.duckduckgo.common.utils.CurrentTimeProvider
21-
import com.duckduckgo.pir.impl.models.Address
2221
import com.duckduckgo.pir.impl.models.AddressCityState
2322
import com.duckduckgo.pir.impl.models.ExtractedProfile
2423
import com.duckduckgo.pir.impl.models.scheduling.JobRecord.EmailConfirmationJobRecord.EmailData
@@ -853,4 +852,16 @@ class RealPirRepositoryTest {
853852
verify(mockUserProfileDao).getUserProfile(profileQueryId)
854853
verify(mockExtractedProfileDao).insertNewExtractedProfiles(any())
855854
}
855+
856+
@Test
857+
fun whenMarkExtractedProfileAsDeprecatedThenCallsDaoUpdateMethod() = runTest {
858+
// Given
859+
val extractedProfileId = 123L
860+
861+
// When
862+
testee.markExtractedProfileAsDeprecated(extractedProfileId)
863+
864+
// Then
865+
verify(mockExtractedProfileDao).updateExtractedProfileDeprecated(extractedProfileId, true)
866+
}
856867
}

0 commit comments

Comments
 (0)