Skip to content

Commit

Permalink
Implement initialScanStatus scannedBrokers API (#2897)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1206488453854252/1207609177023014/f

**Description**:
This PR adds a `ScannedBroker` type and related `scannedBrokers` value
to `DBPUIScanProgress`. This value is populated in `UIMapper.
initialScanState`.
  • Loading branch information
aataraxiaa authored Jun 28, 2024
1 parent 0bda737 commit acee3d4
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,22 @@ struct DBPUIOptOutMatch: DBPUISendableMessage {

/// Data representing the initial scan progress
struct DBPUIScanProgress: DBPUISendableMessage {

struct ScannedBroker: Codable, Equatable {

enum Status: String, Codable {
case inProgress = "in-progress"
case completed
}

let name: String
let url: String
let status: Status
}

let currentScans: Int
let totalScans: Int
let scannedBrokers: [ScannedBroker]
}

/// Data to represent the intial scan state
Expand Down Expand Up @@ -232,6 +246,6 @@ struct DBPUIDebugMetadata: DBPUISendableMessage {
extension DBPUIInitialScanState {
static var empty: DBPUIInitialScanState {
.init(resultsFound: [DBPUIDataBrokerProfileMatch](),
scanProgress: DBPUIScanProgress(currentScans: 0, totalScans: 0))
scanProgress: DBPUIScanProgress(currentScans: 0, totalScans: 0, scannedBrokers: []))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ struct MirrorSite: Codable, Sendable {
}
}

extension MirrorSite {

typealias ScannedBroker = DBPUIScanProgress.ScannedBroker

func scannedBroker(withStatus status: ScannedBroker.Status) -> ScannedBroker {
ScannedBroker(name: name, url: url, status: status)
}
}

public enum DataBrokerHierarchy: Int {
case parent = 1
case child = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ struct DBPUICommunicationLayer: Subfeature {
weak var delegate: DBPUICommunicationDelegate?

private enum Constants {
static let version = 3
static let version = 4
}

internal init(webURLSettings: DataBrokerProtectionWebUIURLSettingsRepresentable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,23 +52,27 @@ struct MapperToUI {
}

func initialScanState(_ brokerProfileQueryData: [BrokerProfileQueryData]) -> DBPUIInitialScanState {
// Total and current scans are misleading. The UI are counting this per broker and
// not by the total real cans that the app is doing.
let profileQueriesGroupedByBroker = Dictionary(grouping: brokerProfileQueryData, by: { $0.dataBroker.name })

// We don't want to consider deprecated queries when reporting manual scans to the UI
let filteredProfileQueriesGroupedByBroker = profileQueriesGroupedByBroker.mapValues { queries in
queries.filter { !$0.profileQuery.deprecated }
}
let withoutDeprecated = brokerProfileQueryData.filter { !$0.profileQuery.deprecated }

let groupedByBroker = Dictionary(grouping: withoutDeprecated, by: { $0.dataBroker.name }).values

let totalScans = filteredProfileQueriesGroupedByBroker.reduce(0) { accumulator, element in
return accumulator + element.value.totalScans
let totalScans = groupedByBroker.reduce(0) { accumulator, brokerQueryData in
return accumulator + brokerQueryData.totalScans
}
let currentScans = filteredProfileQueriesGroupedByBroker.reduce(0) { accumulator, element in
return accumulator + element.value.currentScans

let withSortedGroups = groupedByBroker.map { $0.sortedByLastRunDate() }

let sorted = withSortedGroups.sortedByLastRunDate()

let partiallyScannedBrokers = sorted.flatMap { brokerQueryGroup in
brokerQueryGroup.scannedBrokers
}

let scanProgress = DBPUIScanProgress(currentScans: currentScans, totalScans: totalScans)
let scanProgress = DBPUIScanProgress(currentScans: partiallyScannedBrokers.count,
totalScans: totalScans,
scannedBrokers: partiallyScannedBrokers)

let matches = mapMatchesToUI(brokerProfileQueryData)

return .init(resultsFound: matches, scanProgress: scanProgress)
Expand Down Expand Up @@ -334,23 +338,80 @@ fileprivate extension BrokerProfileQueryData {
}
}

/// Extension on `Optional` which provides comparison abilities when the wrapped type is `Date`
private extension Optional where Wrapped == Date {

static func < (lhs: Date?, rhs: Date?) -> Bool {
switch (lhs, rhs) {
case let (lhsDate?, rhsDate?):
return lhsDate < rhsDate
case (nil, _?):
return false
case (_?, nil):
return true
case (nil, nil):
return false
}
}

static func == (lhs: Date?, rhs: Date?) -> Bool {
switch (lhs, rhs) {
case let (lhs?, rhs?):
return lhs == rhs
case (nil, nil):
return true
default:
return false
}
}
}

private extension Array where Element == [BrokerProfileQueryData] {

/// Sorts the 2-dimensional array in ascending order based on the `lastRunDate` value of the first element of each internal array
///
/// - Returns: An array of `[BrokerProfileQueryData]` values sorted by the first `lastRunDate` of each element
func sortedByLastRunDate() -> Self {
self.sorted { lhs, rhs in
lhs.first?.scanJobData.lastRunDate < rhs.first?.scanJobData.lastRunDate
}
}
}

fileprivate extension Array where Element == BrokerProfileQueryData {

typealias ScannedBroker = DBPUIScanProgress.ScannedBroker

var totalScans: Int {
guard let broker = self.first?.dataBroker else { return 0 }
return 1 + broker.mirrorSites.filter { $0.shouldWeIncludeMirrorSite() }.count
}

var currentScans: Int {
guard let broker = self.first?.dataBroker else { return 0 }
/// Returns an array of brokers which have been either fully or partially scanned
///
/// A broker is considered fully scanned is all scan jobs for that broker have completed.
/// A broker is considered partially scanned if at least one scan job for that broker has completed
/// Mirror brokers will be included in the returned array when `MirrorSite.shouldWeIncludeMirrorSite` returns true
var scannedBrokers: [ScannedBroker] {
guard let broker = self.first?.dataBroker else { return [] }

var completedScans = 0
self.forEach {
completedScans += $0.scanJobData.lastRunDate == nil ? 0 : 1
}

let didAllQueriesFinished = allSatisfy { $0.scanJobData.lastRunDate != nil }
guard completedScans != 0 else { return [] }

if !didAllQueriesFinished {
return 0
} else {
return 1 + broker.mirrorSites.filter { $0.shouldWeIncludeMirrorSite() }.count
var status: ScannedBroker.Status = .inProgress
if completedScans == self.count {
status = .completed
}

let mirrorBrokers = broker.mirrorSites.compactMap {
$0.shouldWeIncludeMirrorSite() ? $0.scannedBroker(withStatus: status) : nil
}

return [ScannedBroker(name: broker.name, url: broker.url, status: status)] + mirrorBrokers
}

var lastOperation: BrokerJobData? {
Expand Down Expand Up @@ -391,6 +452,15 @@ fileprivate extension Array where Element == BrokerProfileQueryData {
}
}).first
}

/// Sorts the array in ascending order based on `lastRunDate`
///
/// - Returns: An array of `BrokerProfileQueryData` sorted by `lastRunDate`
func sortedByLastRunDate() -> Self {
self.sorted { lhs, rhs in
lhs.scanJobData.lastRunDate < rhs.scanJobData.lastRunDate
}
}
}

fileprivate extension BrokerJobData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ final class MapperToUITests: XCTestCase {
XCTAssertEqual(result.scanProgress.totalScans, 2)
}

func testWhenAScanRanOnAllProfileQueriesOnTheSameBroker_thenCurrentScansReflectsThatScansWereDoneOnThatBroker() {
func testWhenAScanRanOnAllProfileQueriesOnTheSameBroker_thenScannedBrokersAndCurrentScansReflectsThat() {
let brokerProfileQueryData: [BrokerProfileQueryData] = [
.mock(dataBrokerName: "Broker #1", lastRunDate: Date()),
.mock(dataBrokerName: "Broker #1", lastRunDate: Date()),
Expand All @@ -55,6 +55,23 @@ final class MapperToUITests: XCTestCase {
let result = sut.initialScanState(brokerProfileQueryData)

XCTAssertEqual(result.scanProgress.currentScans, 1)
XCTAssertEqual(result.scanProgress.scannedBrokers.count, result.scanProgress.currentScans)
XCTAssertEqual(result.scanProgress.scannedBrokers.first!.name, "Broker #1")
XCTAssertTrue(result.resultsFound.isEmpty)
}

func testWhenAScanRanOnOneProfileQueryOnTheSameBroker_thenScannedBrokersAndCurrentScansReflectsThat() {
let brokerProfileQueryData: [BrokerProfileQueryData] = [
.mock(dataBrokerName: "Broker #1", lastRunDate: Date()),
.mock(dataBrokerName: "Broker #1"),
.mock(dataBrokerName: "Broker #2")
]

let result = sut.initialScanState(brokerProfileQueryData)

XCTAssertEqual(result.scanProgress.currentScans, 1)
XCTAssertEqual(result.scanProgress.scannedBrokers.count, result.scanProgress.currentScans)
XCTAssertEqual(result.scanProgress.scannedBrokers.first!.name, "Broker #1")
XCTAssertTrue(result.resultsFound.isEmpty)
}

Expand All @@ -78,7 +95,7 @@ final class MapperToUITests: XCTestCase {
XCTAssertEqual(result.resultsFound.count, 2)
}

func testWhenAllScansRan_thenCurrentScansEqualsTotalScans() {
func testWhenAllScansRan_thenScannedBrokersAndCurrentScansEqualsTotalScans() {
let brokerProfileQueryData: [BrokerProfileQueryData] = [
.mock(dataBrokerName: "Broker #1", lastRunDate: Date()),
.mock(dataBrokerName: "Broker #1", lastRunDate: Date()),
Expand All @@ -88,6 +105,9 @@ final class MapperToUITests: XCTestCase {
let result = sut.initialScanState(brokerProfileQueryData)

XCTAssertEqual(result.scanProgress.totalScans, result.scanProgress.currentScans)
XCTAssertEqual(result.scanProgress.currentScans, 2)
XCTAssertEqual(result.scanProgress.scannedBrokers.count, result.scanProgress.currentScans)
XCTAssertEqual(result.scanProgress.scannedBrokers.map{ $0.name }.sorted(), ["Broker #1", "Broker #2"])
}

func testWhenScansHaveDeprecatedProfileQueries_thenThoseAreNotTakenIntoAccount() {
Expand All @@ -102,6 +122,8 @@ final class MapperToUITests: XCTestCase {

XCTAssertEqual(result.scanProgress.totalScans, 2)
XCTAssertEqual(result.scanProgress.currentScans, 2)
XCTAssertEqual(result.scanProgress.scannedBrokers.count, result.scanProgress.currentScans)
XCTAssertEqual(result.scanProgress.scannedBrokers.map{ $0.name }.sorted(), ["Broker #1", "Broker #2"])
XCTAssertEqual(result.resultsFound.count, 1)
}

Expand All @@ -118,6 +140,8 @@ final class MapperToUITests: XCTestCase {

XCTAssertEqual(result.scanProgress.totalScans, 2)
XCTAssertEqual(result.scanProgress.currentScans, 2)
XCTAssertEqual(result.scanProgress.scannedBrokers.count, result.scanProgress.currentScans)
XCTAssertEqual(result.scanProgress.scannedBrokers.map{ $0.name }.sorted(), ["Broker #1", "Broker #2"])
XCTAssertEqual(result.resultsFound.count, 1)
}

Expand Down Expand Up @@ -212,7 +236,7 @@ final class MapperToUITests: XCTestCase {
XCTAssertEqual(result.scanProgress.totalScans, 2)
}

func testWhenMirrorSiteIsNotInRemovedPeriod_thenItShouldBeAddedToCurrentScans() {
func testWhenMirrorSiteIsNotInRemovedPeriod_thenItShouldBeAddedToScannedBrokersAndCurrentScans() {
let brokerWithMirrorSiteNotRemovedAndWithScan = BrokerProfileQueryData.mock(
dataBrokerName: "Broker #1",
lastRunDate: Date(),
Expand All @@ -227,9 +251,11 @@ final class MapperToUITests: XCTestCase {
let result = sut.initialScanState(brokerProfileQueryData)

XCTAssertEqual(result.scanProgress.currentScans, 2)
XCTAssertEqual(result.scanProgress.scannedBrokers.count, result.scanProgress.currentScans)
XCTAssertEqual(result.scanProgress.scannedBrokers.map{ $0.name }.sorted(), ["Broker #1", "mirror"])
}

func testWhenMirrorSiteIsInRemovedPeriod_thenItShouldNotBeAddedToCurrentScans() {
func testWhenMirrorSiteIsInRemovedPeriod_thenItShouldNotBeAddedToScannedBrokersCurrentScans() {
let brokerWithMirrorSiteRemovedAndWithScan = BrokerProfileQueryData.mock(
dataBrokerName: "Broker #2",
lastRunDate: Date(),
Expand All @@ -244,6 +270,8 @@ final class MapperToUITests: XCTestCase {
let result = sut.initialScanState(brokerProfileQueryData)

XCTAssertEqual(result.scanProgress.currentScans, 1)
XCTAssertEqual(result.scanProgress.scannedBrokers.count, result.scanProgress.currentScans)
XCTAssertEqual(result.scanProgress.scannedBrokers.map{ $0.name }.sorted(), ["Broker #2"])
}

func testWhenMirrorSiteIsNotInRemovedPeriod_thenMatchIsAdded() {
Expand Down Expand Up @@ -330,6 +358,46 @@ final class MapperToUITests: XCTestCase {
XCTAssertEqual(result.scanSchedule.nextScan.dataBrokers.count, 3)
XCTAssertTrue(areDatesEqualsOnDayMonthAndYear(date1: Date().tomorrow, date2: Date(timeIntervalSince1970: result.scanSchedule.nextScan.date)))
}

func testBrokersWithMixedScanProgress_areOrderedByLastRunDate_andHaveCorrectStatus() {

// Given
let brokerProfileQueryData: [BrokerProfileQueryData] = [
.mock(dataBrokerName: "Broker #1", lastRunDate: Date()),
.mock(dataBrokerName: "Broker #1", lastRunDate: Date()),
.mock(dataBrokerName: "Broker #1", lastRunDate: .minusTwoHours),
.mock(dataBrokerName: "Broker #2"),
.mock(dataBrokerName: "Broker #2", lastRunDate: .minusOneHour),
.mock(dataBrokerName: "Broker #2", lastRunDate: .minusThreeHours),
.mock(dataBrokerName: "Broker #3", lastRunDate: .minusTwoHours),
.mock(dataBrokerName: "Broker #3"),
.mock(dataBrokerName: "Broker #3", lastRunDate: Date()),
.mock(dataBrokerName: "Broker #4"),
.mock(dataBrokerName: "Broker #5"),
.mock(dataBrokerName: "Broker #7", lastRunDate: .minusThreeHours),
.mock(dataBrokerName: "Broker #6", lastRunDate: .minusThreeHours)
]

let expected: [DBPUIScanProgress.ScannedBroker] = [
.mock("Broker #2", status: .inProgress),
.mock("Broker #7", status: .completed),
.mock("Broker #6", status: .completed),
.mock("Broker #1", status: .completed),
.mock("Broker #3", status: .inProgress)
]

let result = sut.initialScanState(brokerProfileQueryData)

XCTAssertEqual(result.scanProgress.currentScans, 5)
XCTAssertEqual(result.scanProgress.scannedBrokers, expected)
}

}

extension DBPUIScanProgress.ScannedBroker {
static func mock(_ name: String, status: Self.Status) -> DBPUIScanProgress.ScannedBroker {
.init(name: name, url: "test.com", status: status)
}
}

extension Date {
Expand All @@ -345,4 +413,21 @@ extension Date {

return calendar.date(byAdding: .day, value: 1, to: self)
}

static var minusOneHour: Date? {
nowMinusHour(1)
}

static var minusTwoHours: Date? {
nowMinusHour(2)
}

static var minusThreeHours: Date? {
nowMinusHour(3)
}

private static func nowMinusHour(_ hour: Int) -> Date? {
let calendar = Calendar.current
return calendar.date(byAdding: .hour, value: -hour, to: Date())
}
}

0 comments on commit acee3d4

Please sign in to comment.