diff --git a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/dashboard/state/PirDashboardMaintenanceScanDataProvider.kt b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/dashboard/state/PirDashboardMaintenanceScanDataProvider.kt index ee90873e6eda..652d0b2c3f7a 100644 --- a/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/dashboard/state/PirDashboardMaintenanceScanDataProvider.kt +++ b/pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/dashboard/state/PirDashboardMaintenanceScanDataProvider.kt @@ -123,6 +123,7 @@ class RealPirDashboardMaintenanceScanDataProvider @Inject constructor( it.lastScanDateInMillis in startDate..endDate }, getDateMillis = { it.lastScanDateInMillis }, + keepEarliest = false, ) return DashboardScanDetails( @@ -146,6 +147,7 @@ class RealPirDashboardMaintenanceScanDataProvider @Inject constructor( val schedulingConfig = schedulingConfigMap[it.brokerName] ?: return@getBrokerMatches 0L it.getNextRunMillis(schedulingConfig, nextRunFromOptOutDataMap[it.brokerName], startDate, endDate) }, + keepEarliest = true, ) return DashboardScanDetails( @@ -230,6 +232,7 @@ class RealPirDashboardMaintenanceScanDataProvider @Inject constructor( private suspend fun getBrokerMatches( scanFilter: (ScanJobRecord) -> Boolean, getDateMillis: (ScanJobRecord) -> Long, + keepEarliest: Boolean, ): List { val activeBrokerMap = pirRepository.getAllActiveBrokerObjects().associateBy { it.name } // Only consider active brokers and ignore removed ones @@ -255,7 +258,13 @@ class RealPirDashboardMaintenanceScanDataProvider @Inject constructor( } val mirrorValidScanJobs = validScansJobs.getMirrorSites() - return (validScansJobs + mirrorValidScanJobs).sortedBy { it.dateInMillis } + val combined = validScansJobs + mirrorValidScanJobs + val sorted = if (keepEarliest) { + combined.sortedBy { it.dateInMillis } + } else { + combined.sortedByDescending { it.dateInMillis } + } + return sorted.distinctBy { it.broker.name } } private suspend fun List.getMirrorSites(): List { diff --git a/pir/pir-impl/src/test/java/com/duckduckgo/pir/impl/dashboard/state/RealPirDashboardMaintenanceScanDataProviderTest.kt b/pir/pir-impl/src/test/java/com/duckduckgo/pir/impl/dashboard/state/RealPirDashboardMaintenanceScanDataProviderTest.kt index 11db611ee3e6..281c2070fdf0 100644 --- a/pir/pir-impl/src/test/java/com/duckduckgo/pir/impl/dashboard/state/RealPirDashboardMaintenanceScanDataProviderTest.kt +++ b/pir/pir-impl/src/test/java/com/duckduckgo/pir/impl/dashboard/state/RealPirDashboardMaintenanceScanDataProviderTest.kt @@ -255,9 +255,9 @@ class RealPirDashboardMaintenanceScanDataProviderTest { // Then assertEquals(2, result.brokerMatches.size) // Only scans within 8 days assertEquals( - currentTime - TimeUnit.DAYS.toMillis(5), + currentTime - TimeUnit.DAYS.toMillis(2), result.dateInMillis, - ) // Earliest scan within range + ) // Most recent scan within range val broker1Match = result.brokerMatches.find { it.broker.name == "broker1" }!! assertEquals(currentTime - TimeUnit.DAYS.toMillis(2), broker1Match.dateInMillis) @@ -266,6 +266,41 @@ class RealPirDashboardMaintenanceScanDataProviderTest { assertEquals(currentTime - TimeUnit.DAYS.toMillis(5), broker2Match.dateInMillis) } + @Test + fun whenScanJobsExistForSameBrokerButDifferentProfileQueriesThenGetLastScanDetailsReturnsOneBrokerMatch() = + runTest { + // Given - same broker with two different profile queries (userProfileId) + val activeBrokers = listOf(createBroker("broker1")) + val scanJobs = listOf( + createScanJobRecord( + "broker1", + userProfileId = 1L, + ScanJobStatus.MATCHES_FOUND, + currentTime - TimeUnit.DAYS.toMillis(2), + ), + createScanJobRecord( + "broker1", + userProfileId = 2L, + ScanJobStatus.MATCHES_FOUND, + currentTime - TimeUnit.DAYS.toMillis(3), + ), + ) + + whenever(mockPirRepository.getAllActiveBrokerObjects()).thenReturn(activeBrokers) + whenever(mockPirRepository.getAllBrokerOptOutUrls()).thenReturn(emptyMap()) + whenever(mockPirSchedulingRepository.getAllValidScanJobRecords()).thenReturn(scanJobs) + whenever(mockPirRepository.getAllMirrorSites()).thenReturn(emptyList()) + + // When + val result = testee.getLastScanDetails() + + // Then - Returns only ONE broker match per broker, regardless of how many profile queries exist + // and keeps the most recent (latest) scan date for getLastScanDetails + assertEquals(1, result.brokerMatches.size) + assertEquals("broker1", result.brokerMatches[0].broker.name) + assertEquals(currentTime - TimeUnit.DAYS.toMillis(2), result.brokerMatches[0].dateInMillis) + } + @Test fun whenNoScheduledScanJobsExistThenGetNextScanDetailsReturnsEmptyDetails() = runTest { // Given @@ -660,6 +695,65 @@ class RealPirDashboardMaintenanceScanDataProviderTest { assertEquals(expectedNextScan, result.dateInMillis) } + @Test + fun whenOptOutsExistForSameBrokerButDifferentProfileQueriesThenGetNextScanDetailsReturnsOneBrokerMatch() = + runTest { + // Given - same broker with two different profile queries (userProfileId) + val activeBrokers = listOf(createBroker("broker1")) + val schedulingConfigs = listOf( + createBrokerSchedulingConfig( + "broker1", + maintenanceScanInMillis = TimeUnit.DAYS.toMillis(10), + confirmOptOutScanInMillis = TimeUnit.DAYS.toMillis(5), + ), + ) + val scanJobs = listOf( + createScanJobRecord( + "broker1", + userProfileId = 1L, + ScanJobStatus.MATCHES_FOUND, + currentTime - TimeUnit.DAYS.toMillis(3), + ), + createScanJobRecord( + "broker1", + userProfileId = 2L, + ScanJobStatus.MATCHES_FOUND, + currentTime - TimeUnit.DAYS.toMillis(4), + ), + ) + val optOutJobs = listOf( + createOptOutJobRecord( + brokerName = "broker1", + extractedProfileId = 1L, + userProfileId = 1L, + status = OptOutJobStatus.REQUESTED, + optOutRequestedDateInMillis = currentTime - TimeUnit.DAYS.toMillis(2), + ), + createOptOutJobRecord( + brokerName = "broker1", + extractedProfileId = 2L, + userProfileId = 2L, + status = OptOutJobStatus.REQUESTED, + optOutRequestedDateInMillis = currentTime - TimeUnit.DAYS.toMillis(1), + ), + ) + + whenever(mockPirSchedulingRepository.getAllValidOptOutJobRecords()).thenReturn(optOutJobs) + whenever(mockPirRepository.getAllActiveBrokerObjects()).thenReturn(activeBrokers) + whenever(mockPirRepository.getAllBrokerOptOutUrls()).thenReturn(emptyMap()) + whenever(mockPirRepository.getAllBrokerSchedulingConfigs()).thenReturn(schedulingConfigs) + whenever(mockPirSchedulingRepository.getAllValidScanJobRecords()).thenReturn(scanJobs) + whenever(mockPirRepository.getAllMirrorSites()).thenReturn(emptyList()) + + // When + val result = testee.getNextScanDetails() + + // Then - Returns only ONE broker match per broker, regardless of how many profile queries exist + // This is because DashboardBrokerMatch only contains broker info, not profile query info + assertEquals(1, result.brokerMatches.size) + assertEquals("broker1", result.brokerMatches[0].broker.name) + } + @Test fun whenNoneInRangeThenGetNextScanDetailsReturnsEmpty() = runTest { // Given