Skip to content

Commit 0178097

Browse files
committed
Handle removing extracted profile from dashboard
1 parent 095ce56 commit 0178097

File tree

8 files changed

+150
-14
lines changed

8 files changed

+150
-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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,19 @@ 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+
* It will:
243+
* - Mark the ExtractedProfile as deprecated
244+
* - Update all OptOutJobRecords for that ExtractedProfile (set status to REMOVED_BY_USER, deprecated = true)
245+
* - Update all EmailConfirmationJobRecords for that ExtractedProfile (deprecated = true)
246+
*
247+
* @param extractedProfileId The id of the [ExtractedProfile] to be marked as removed by user
248+
*/
249+
suspend fun markRecordsAsRemovedByUser(extractedProfileId: Long)
237250
}
238251

239252
@ContributesBinding(AppScope::class)
@@ -477,6 +490,35 @@ class RealJobRecordUpdater @Inject constructor(
477490
}
478491
}
479492

493+
override suspend fun markRecordsAsRemovedByUser(extractedProfileId: Long) {
494+
withContext(dispatcherProvider.io()) {
495+
repository.markExtractedProfileAsDeprecated(extractedProfileId)
496+
497+
// update the OptOutJobRecord for this ExtractedProfile
498+
schedulingRepository.getValidOptOutJobRecord(extractedProfileId)?.run {
499+
schedulingRepository.saveOptOutJobRecord(
500+
copy(
501+
status = OptOutJobStatus.REMOVED_BY_USER,
502+
deprecated = true,
503+
).also {
504+
logcat { "PIR-JOB-RECORD: Updated OptOutJobRecord for $extractedProfileId to REMOVED_BY_USER: $it" }
505+
},
506+
)
507+
}
508+
509+
// update the EmailConfirmationJobRecord for this ExtractedProfile (if it exists)
510+
schedulingRepository.getEmailConfirmationJob(extractedProfileId)?.run {
511+
schedulingRepository.saveEmailConfirmationJobRecord(
512+
copy(
513+
deprecated = true,
514+
).also {
515+
logcat { "PIR-JOB-RECORD: Marked EmailConfirmationJobRecord for $extractedProfileId as deprecated: $it" }
516+
},
517+
)
518+
}
519+
}
520+
}
521+
480522
private data class ExtractedProfileComparisonKey(
481523
val profileQueryId: Long,
482524
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: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -795,13 +795,52 @@ 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+
whenever(mockSchedulingRepository.getEmailConfirmationJob(testExtractedProfileId))
809+
.thenReturn(null)
810+
whenever(mockSchedulingRepository.getEmailConfirmationJob(testExtractedProfileId))
811+
.thenReturn(testEmailConfirmationJobRecord)
812+
813+
toTest.markRecordsAsRemovedByUser(testExtractedProfileId)
814+
815+
verify(mockRepository).markExtractedProfileAsDeprecated(testExtractedProfileId)
816+
verify(mockSchedulingRepository).saveOptOutJobRecord(
817+
testOptOutJobRecord.copy(
818+
status = OptOutJobStatus.REMOVED_BY_USER,
819+
deprecated = true,
820+
),
821+
)
822+
verify(mockSchedulingRepository).saveEmailConfirmationJobRecord(
823+
testEmailConfirmationJobRecord.copy(
824+
deprecated = true,
825+
),
826+
)
827+
}
828+
829+
@Test
830+
fun whenMarkRecordsAsRemovedByUserAndOptOutJobRecordAndEmailConfirmationDoesNotExistThenOnlyMarksExtractedProfileAsDeprecated() =
831+
runTest {
832+
whenever(mockSchedulingRepository.getValidOptOutJobRecord(testExtractedProfileId))
833+
.thenReturn(null)
834+
whenever(mockSchedulingRepository.getEmailConfirmationJob(testExtractedProfileId))
835+
.thenReturn(null)
836+
837+
toTest.markRecordsAsRemovedByUser(testExtractedProfileId)
838+
839+
verify(mockRepository).markExtractedProfileAsDeprecated(testExtractedProfileId)
840+
verify(mockSchedulingRepository, never()).saveOptOutJobRecord(any())
841+
verify(mockSchedulingRepository, never()).saveEmailConfirmationJobRecord(any())
842+
}
843+
805844
companion object {
806845
private const val TEST_CURRENT_TIME = 5000L
807846
}

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)