From 1c82d0f12948e91e9fc8f434e86ddd5a77fdf9f4 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Thu, 12 Sep 2024 20:16:47 +0100 Subject: [PATCH 01/16] Add new fields to DBP->FE API, and associated constructors --- .../Model/DBPUICommunicationModel.swift | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index 16419ed9a5..7f7c350cb0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -76,6 +76,15 @@ struct DBPUIUserProfileAddress: Codable { let zipCode: String? } +extension DBPUIUserProfileAddress { + init(addressCityState: AddressCityState) { + self.init(street: addressCityState.fullAddress, + city: addressCityState.city, + state: addressCityState.state, + zipCode: nil) + } +} + /// Message Object representing a user profile containing one or more names and addresses /// also contains the user profile's birth year struct DBPUIUserProfile: Codable { @@ -112,6 +121,10 @@ struct DBPUIDataBroker: Codable, Hashable { self.date = date } + init(dataBroker: DataBroker) { + self.init(name: dataBroker.name, url: dataBroker.url) + } + func hash(into hasher: inout Hasher) { hasher.combine(name) } @@ -136,6 +149,43 @@ struct DBPUIDataBrokerProfileMatch: Codable { let alternativeNames: [String] let relatives: [String] let date: Double? // Used in some methods to set the removedDate or found date + let foundDate: Double + let optOutSubmittedDate: Double? + let estimatedRemovalDate: Double? + let removedDate: Double? +} + +extension DBPUIDataBrokerProfileMatch { + init(extractedProfile: ExtractedProfile, optOutJobData: OptOutJobData, dataBroker: DataBroker) { + let estimatedRemovalDate = Calendar.current.date(byAdding: .day, value: 14, to: optOutJobData.createdDate) + self.init(dataBroker: DBPUIDataBroker(dataBroker: dataBroker), + name: extractedProfile.fullName ?? "No name", + addresses: extractedProfile.addresses?.map {DBPUIUserProfileAddress(addressCityState: $0) } ?? [], + alternativeNames: extractedProfile.alternativeNames ?? [String](), + relatives: extractedProfile.relatives ?? [String](), + date: extractedProfile.removedDate?.timeIntervalSince1970, + foundDate: optOutJobData.createdDate.timeIntervalSince1970, + optOutSubmittedDate: optOutJobData.submittedSuccessfullyDate?.timeIntervalSince1970, + estimatedRemovalDate: estimatedRemovalDate?.timeIntervalSince1970, + removedDate: extractedProfile.removedDate?.timeIntervalSince1970) + } + + init(extractedProfile: ExtractedProfile, + optOutJobData: OptOutJobData, + dataBrokerName: String, + databrokerURL: String) { + let estimatedRemovalDate = Calendar.current.date(byAdding: .day, value: 14, to: optOutJobData.createdDate) + self.init(dataBroker: DBPUIDataBroker(name: dataBrokerName, url: databrokerURL), + name: extractedProfile.fullName ?? "No name", + addresses: extractedProfile.addresses?.map {DBPUIUserProfileAddress(addressCityState: $0) } ?? [], + alternativeNames: extractedProfile.alternativeNames ?? [String](), + relatives: extractedProfile.relatives ?? [String](), + date: extractedProfile.removedDate?.timeIntervalSince1970, + foundDate: optOutJobData.createdDate.timeIntervalSince1970, + optOutSubmittedDate: optOutJobData.submittedSuccessfullyDate?.timeIntervalSince1970, + estimatedRemovalDate: estimatedRemovalDate?.timeIntervalSince1970, + removedDate: extractedProfile.removedDate?.timeIntervalSince1970) + } } /// Protocol to represent a message that can be passed from the host to the UI @@ -156,6 +206,27 @@ struct DBPUIOptOutMatch: DBPUISendableMessage { let alternativeNames: [String] let addresses: [DBPUIUserProfileAddress] let date: Double + let foundDate: Double + let optOutSubmittedDate: Double? + let estimatedRemovalDate: Double? + let removedDate: Double? +} + +extension DBPUIOptOutMatch { + init?(profileMatch: DBPUIDataBrokerProfileMatch, matches: Int) { + guard let removedDate = profileMatch.removedDate else { return nil } + let dataBroker = profileMatch.dataBroker + self.init(dataBroker: dataBroker, + matches: matches, + name: profileMatch.name, + alternativeNames: profileMatch.alternativeNames, + addresses: profileMatch.addresses, + date: removedDate, + foundDate: profileMatch.foundDate, + optOutSubmittedDate: profileMatch.optOutSubmittedDate, + estimatedRemovalDate: nil, + removedDate: removedDate) + } } /// Data representing the initial scan progress From 6388873a83c922b1fd8abf843d81473bbba11ce0 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Thu, 12 Sep 2024 20:17:02 +0100 Subject: [PATCH 02/16] Make UIMapper use new constructors --- .../Model/BrokerProfileQueryData.swift | 4 ++ .../DataBrokerProtection/UI/UIMapper.swift | 60 ++++++------------- 2 files changed, 21 insertions(+), 43 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift index 45a23c5d28..e7d1264cf0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift @@ -33,6 +33,10 @@ public struct BrokerProfileQueryData: Sendable { optOutJobData.map { $0.extractedProfile } } + var extractedProfilesWithOptOutJobData: [(ExtractedProfile, OptOutJobData)] { + optOutJobData.map { ($0.extractedProfile, $0) } + } + var events: [HistoryEvent] { operationsData.flatMap { $0.historyEvents }.sorted { $0.date < $1.date } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index 5a7bbf5571..4b9531cd3c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -22,36 +22,6 @@ import os.log struct MapperToUI { - func mapToUI(_ dataBroker: DataBroker, extractedProfile: ExtractedProfile) -> DBPUIDataBrokerProfileMatch { - DBPUIDataBrokerProfileMatch( - dataBroker: mapToUI(dataBroker), - name: extractedProfile.fullName ?? "No name", - addresses: extractedProfile.addresses?.map(mapToUI) ?? [], - alternativeNames: extractedProfile.alternativeNames ?? [String](), - relatives: extractedProfile.relatives ?? [String](), - date: extractedProfile.removedDate?.timeIntervalSince1970 - ) - } - - func mapToUI(_ dataBrokerName: String, databrokerURL: String, extractedProfile: ExtractedProfile) -> DBPUIDataBrokerProfileMatch { - DBPUIDataBrokerProfileMatch( - dataBroker: DBPUIDataBroker(name: dataBrokerName, url: databrokerURL), - name: extractedProfile.fullName ?? "No name", - addresses: extractedProfile.addresses?.map(mapToUI) ?? [], - alternativeNames: extractedProfile.alternativeNames ?? [String](), - relatives: extractedProfile.relatives ?? [String](), - date: extractedProfile.removedDate?.timeIntervalSince1970 - ) - } - - func mapToUI(_ dataBroker: DataBroker) -> DBPUIDataBroker { - DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url) - } - - func mapToUI(_ address: AddressCityState) -> DBPUIUserProfileAddress { - DBPUIUserProfileAddress(street: address.fullAddress, city: address.city, state: address.state, zipCode: nil) - } - func initialScanState(_ brokerProfileQueryData: [BrokerProfileQueryData]) -> DBPUIInitialScanState { let withoutDeprecated = brokerProfileQueryData.filter { !$0.profileQuery.deprecated } @@ -82,13 +52,18 @@ struct MapperToUI { private func mapMatchesToUI(_ brokerProfileQueryData: [BrokerProfileQueryData]) -> [DBPUIDataBrokerProfileMatch] { return brokerProfileQueryData.flatMap { var profiles = [DBPUIDataBrokerProfileMatch]() - for extractedProfile in $0.extractedProfiles where !$0.profileQuery.deprecated { - profiles.append(mapToUI($0.dataBroker, extractedProfile: extractedProfile)) + for (extractedProfile, optOutJobData) in $0.extractedProfilesWithOptOutJobData where !$0.profileQuery.deprecated { + profiles.append(DBPUIDataBrokerProfileMatch(extractedProfile: extractedProfile, + optOutJobData: optOutJobData, + dataBroker: $0.dataBroker)) if !$0.dataBroker.mirrorSites.isEmpty { let mirrorSitesMatches = $0.dataBroker.mirrorSites.compactMap { mirrorSite in if mirrorSite.shouldWeIncludeMirrorSite() { - return mapToUI(mirrorSite.name, databrokerURL: mirrorSite.url, extractedProfile: extractedProfile) + return DBPUIDataBrokerProfileMatch(extractedProfile: extractedProfile, + optOutJobData: optOutJobData, + dataBrokerName: mirrorSite.name, + databrokerURL: mirrorSite.url) } return nil @@ -113,7 +88,9 @@ struct MapperToUI { let scanJob = $0.scanJobData for optOutJob in $0.optOutJobData { let extractedProfile = optOutJob.extractedProfile - let profileMatch = mapToUI(dataBroker, extractedProfile: extractedProfile) + let profileMatch = DBPUIDataBrokerProfileMatch(extractedProfile: extractedProfile, + optOutJobData: optOutJob, + dataBroker: dataBroker) if extractedProfile.removedDate == nil { inProgressOptOuts.append(profileMatch) @@ -123,7 +100,10 @@ struct MapperToUI { if let closestMatchesFoundEvent = scanJob.closestMatchesFoundEvent() { for mirrorSite in dataBroker.mirrorSites where mirrorSite.shouldWeIncludeMirrorSite(for: closestMatchesFoundEvent.date) { - let mirrorSiteMatch = mapToUI(mirrorSite.name, databrokerURL: mirrorSite.url, extractedProfile: extractedProfile) + let mirrorSiteMatch = DBPUIDataBrokerProfileMatch(extractedProfile: extractedProfile, + optOutJobData: optOutJob, + dataBrokerName: mirrorSite.name, + databrokerURL: mirrorSite.url) if let extractedProfileRemovedDate = extractedProfile.removedDate, mirrorSite.shouldWeIncludeMirrorSite(for: extractedProfileRemovedDate) { @@ -137,15 +117,9 @@ struct MapperToUI { } let completedOptOutsDictionary = Dictionary(grouping: removedProfiles, by: { $0.dataBroker }) - let completedOptOuts: [DBPUIOptOutMatch] = completedOptOutsDictionary.compactMap { (key: DBPUIDataBroker, value: [DBPUIDataBrokerProfileMatch]) in + let completedOptOuts: [DBPUIOptOutMatch] = completedOptOutsDictionary.compactMap { (_, value: [DBPUIDataBrokerProfileMatch]) in value.compactMap { match in - guard let removedDate = match.date else { return nil } - return DBPUIOptOutMatch(dataBroker: key, - matches: value.count, - name: match.name, - alternativeNames: match.alternativeNames, - addresses: match.addresses, - date: removedDate) + return DBPUIOptOutMatch(profileMatch: match, matches: value.count) } }.flatMap { $0 } From 24d3fc9c561d8de6412582a1aae0951b93e2587f Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Thu, 12 Sep 2024 20:22:29 +0100 Subject: [PATCH 03/16] Remove unused data field --- .../DataBrokerProtection/Model/DBPUICommunicationModel.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index 7f7c350cb0..a648775815 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -148,7 +148,6 @@ struct DBPUIDataBrokerProfileMatch: Codable { let addresses: [DBPUIUserProfileAddress] let alternativeNames: [String] let relatives: [String] - let date: Double? // Used in some methods to set the removedDate or found date let foundDate: Double let optOutSubmittedDate: Double? let estimatedRemovalDate: Double? @@ -163,7 +162,6 @@ extension DBPUIDataBrokerProfileMatch { addresses: extractedProfile.addresses?.map {DBPUIUserProfileAddress(addressCityState: $0) } ?? [], alternativeNames: extractedProfile.alternativeNames ?? [String](), relatives: extractedProfile.relatives ?? [String](), - date: extractedProfile.removedDate?.timeIntervalSince1970, foundDate: optOutJobData.createdDate.timeIntervalSince1970, optOutSubmittedDate: optOutJobData.submittedSuccessfullyDate?.timeIntervalSince1970, estimatedRemovalDate: estimatedRemovalDate?.timeIntervalSince1970, @@ -180,7 +178,6 @@ extension DBPUIDataBrokerProfileMatch { addresses: extractedProfile.addresses?.map {DBPUIUserProfileAddress(addressCityState: $0) } ?? [], alternativeNames: extractedProfile.alternativeNames ?? [String](), relatives: extractedProfile.relatives ?? [String](), - date: extractedProfile.removedDate?.timeIntervalSince1970, foundDate: optOutJobData.createdDate.timeIntervalSince1970, optOutSubmittedDate: optOutJobData.submittedSuccessfullyDate?.timeIntervalSince1970, estimatedRemovalDate: estimatedRemovalDate?.timeIntervalSince1970, From 2f450043941cfcc35a8342f704e9afa01d2ee405 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Mon, 16 Sep 2024 17:07:06 +0100 Subject: [PATCH 04/16] Refactor DBPUIDateBrokerProfileMatch inits to reduce duplication --- .../Model/DBPUICommunicationModel.swift | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index a648775815..c2a0599c49 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -121,10 +121,6 @@ struct DBPUIDataBroker: Codable, Hashable { self.date = date } - init(dataBroker: DataBroker) { - self.init(name: dataBroker.name, url: dataBroker.url) - } - func hash(into hasher: inout Hasher) { hasher.combine(name) } @@ -155,19 +151,6 @@ struct DBPUIDataBrokerProfileMatch: Codable { } extension DBPUIDataBrokerProfileMatch { - init(extractedProfile: ExtractedProfile, optOutJobData: OptOutJobData, dataBroker: DataBroker) { - let estimatedRemovalDate = Calendar.current.date(byAdding: .day, value: 14, to: optOutJobData.createdDate) - self.init(dataBroker: DBPUIDataBroker(dataBroker: dataBroker), - name: extractedProfile.fullName ?? "No name", - addresses: extractedProfile.addresses?.map {DBPUIUserProfileAddress(addressCityState: $0) } ?? [], - alternativeNames: extractedProfile.alternativeNames ?? [String](), - relatives: extractedProfile.relatives ?? [String](), - foundDate: optOutJobData.createdDate.timeIntervalSince1970, - optOutSubmittedDate: optOutJobData.submittedSuccessfullyDate?.timeIntervalSince1970, - estimatedRemovalDate: estimatedRemovalDate?.timeIntervalSince1970, - removedDate: extractedProfile.removedDate?.timeIntervalSince1970) - } - init(extractedProfile: ExtractedProfile, optOutJobData: OptOutJobData, dataBrokerName: String, @@ -183,6 +166,13 @@ extension DBPUIDataBrokerProfileMatch { estimatedRemovalDate: estimatedRemovalDate?.timeIntervalSince1970, removedDate: extractedProfile.removedDate?.timeIntervalSince1970) } + + init(extractedProfile: ExtractedProfile, optOutJobData: OptOutJobData, dataBroker: DataBroker) { + self.init(extractedProfile: extractedProfile, + optOutJobData: optOutJobData, + dataBrokerName: dataBroker.name, + databrokerURL: dataBroker.url) + } } /// Protocol to represent a message that can be passed from the host to the UI From 9e326ef12018e792373ead406b6d8b6b56ce647f Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Mon, 16 Sep 2024 18:15:45 +0100 Subject: [PATCH 05/16] Fix DBPUIDataBrokerProfileMatch creation when created date is default --- .../Model/DBPUICommunicationModel.swift | 31 +++++++++++++++++-- .../Model/HistoryEvent.swift | 9 ++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index c2a0599c49..f55f036353 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -155,14 +155,39 @@ extension DBPUIDataBrokerProfileMatch { optOutJobData: OptOutJobData, dataBrokerName: String, databrokerURL: String) { - let estimatedRemovalDate = Calendar.current.date(byAdding: .day, value: 14, to: optOutJobData.createdDate) + /* + createdDate used to not exist in the DB, so in the migration we defaulted it to Unix Epoch zero (i.e. 1970) + If that's the case, we should rely on the events instead + We don't do that all the time since it's unnecssarily expensive trawling through events, and + this is involved in some already heavy endpoints + + optOutSubmittedDate also used to not exist, but instead defaults to nil + However, it could be nil simply because the opt out hasn't been submitted yet. So since we don't want to + look through events unneccesarily, we instead only look for it if the createdDate is 1970 + */ + var foundDate = optOutJobData.createdDate + var optOutSubmittedDate = optOutJobData.submittedSuccessfullyDate + if foundDate == Date(timeIntervalSince1970: 0) { + let foundEvents = optOutJobData.historyEvents.filter { $0.isMatchesFoundEvent() } + let firstFoundEvent = foundEvents.min(by: { $0.date < $1.date }) + if let firstFoundEventDate = firstFoundEvent?.date { + foundDate = firstFoundEventDate + } else { + assertionFailure("No matching MatchFound event for an extract profile found") + } + + let optOutSubmittedEvents = optOutJobData.historyEvents.filter { $0.type == .optOutRequested } + let firstOptOutEvent = optOutSubmittedEvents.min(by: { $0.date < $1.date }) + optOutSubmittedDate = firstOptOutEvent?.date + } + let estimatedRemovalDate = Calendar.current.date(byAdding: .day, value: 14, to: foundDate) self.init(dataBroker: DBPUIDataBroker(name: dataBrokerName, url: databrokerURL), name: extractedProfile.fullName ?? "No name", addresses: extractedProfile.addresses?.map {DBPUIUserProfileAddress(addressCityState: $0) } ?? [], alternativeNames: extractedProfile.alternativeNames ?? [String](), relatives: extractedProfile.relatives ?? [String](), - foundDate: optOutJobData.createdDate.timeIntervalSince1970, - optOutSubmittedDate: optOutJobData.submittedSuccessfullyDate?.timeIntervalSince1970, + foundDate: foundDate.timeIntervalSince1970, + optOutSubmittedDate: optOutSubmittedDate?.timeIntervalSince1970, estimatedRemovalDate: estimatedRemovalDate?.timeIntervalSince1970, removedDate: extractedProfile.removedDate?.timeIntervalSince1970) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/HistoryEvent.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/HistoryEvent.swift index c783ea4422..37b364d58f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/HistoryEvent.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/HistoryEvent.swift @@ -69,4 +69,13 @@ public struct HistoryEvent: Identifiable, Sendable { return false } } + + func isMatchesFoundEvent() -> Bool { + switch type { + case .matchesFound: + return true + default: + return false + } + } } From fa5f6752dd00f8cf57578fe25176f788781a7770 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Mon, 16 Sep 2024 18:16:00 +0100 Subject: [PATCH 06/16] Add unit tests for DBPUIDataBrokerProfileMatch creation --- .../DBPUICommunicationModelTests.swift | 123 ++++++++++++++++++ .../DataBrokerProtectionTests/Mocks.swift | 7 + 2 files changed, 130 insertions(+) create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift new file mode 100644 index 0000000000..5c12395815 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift @@ -0,0 +1,123 @@ +// +// DBPUICommunicationModelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// 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. +// + +import XCTest +import Foundation +@testable import DataBrokerProtection + +final class DBPUICommunicationModelTests: XCTestCase { + + func testProfileMatchInit_whenCreatedDateIsNotDefault_thenResultingProfileMatchDatesAreBothBasedOnOptOutJobDataDates() { + + // Given + let extractedProfile = ExtractedProfile.mockWithRemovedDate + + let foundEventDate = Calendar.current.date(byAdding: .day, value: -20, to: Date.now)! + let submittedEventDate = Calendar.current.date(byAdding: .day, value: -18, to: Date.now)! + let historyEvents = [ + HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .matchesFound(count: 1), date: foundEventDate), + HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .optOutRequested, date: submittedEventDate) + ] + + let createdDate = Calendar.current.date(byAdding: .day, value: -14, to: Date.now)! + let submittedDate = Calendar.current.date(byAdding: .day, value: -7, to: Date.now)! + let optOut = OptOutJobData.mock(with: extractedProfile, + historyEvents: historyEvents, + createdDate: createdDate, + submittedSuccessfullyDate: submittedDate) + + // When + let profileMatch = DBPUIDataBrokerProfileMatch(extractedProfile: extractedProfile, + optOutJobData: optOut, + dataBrokerName: "doesn't matter for the test", + databrokerURL: "see above") + + // Then + XCTAssertEqual(profileMatch.foundDate, createdDate.timeIntervalSince1970) + XCTAssertEqual(profileMatch.optOutSubmittedDate, submittedDate.timeIntervalSince1970) + } + + func testProfileMatchInit_whenCreatedDateIsDefault_thenResultingProfileMatchDatesAreBothBasedOnEventDates() { + + // Given + let extractedProfile = ExtractedProfile.mockWithRemovedDate + + let foundEventDate = Calendar.current.date(byAdding: .day, value: -20, to: Date.now)! + let submittedEventDate = Calendar.current.date(byAdding: .day, value: -18, to: Date.now)! + let historyEvents = [ + HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .matchesFound(count: 1), date: foundEventDate), + HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .optOutRequested, date: submittedEventDate) + ] + + let createdDate = Date(timeIntervalSince1970: 0) + let submittedDate = Calendar.current.date(byAdding: .day, value: -7, to: Date.now)! + let optOut = OptOutJobData.mock(with: extractedProfile, + historyEvents: historyEvents, + createdDate: createdDate, + submittedSuccessfullyDate: submittedDate) + + // When + let profileMatch = DBPUIDataBrokerProfileMatch(extractedProfile: extractedProfile, + optOutJobData: optOut, + dataBrokerName: "doesn't matter for the test", + databrokerURL: "see above") + + // Then + XCTAssertEqual(profileMatch.foundDate, foundEventDate.timeIntervalSince1970) + XCTAssertEqual(profileMatch.optOutSubmittedDate, submittedEventDate.timeIntervalSince1970) + } + + func testProfileMatchInit_whenCreatedDateIsDefaultAndThereAreMultipleEventsOfTheSameType_thenResultingProfileMatchDatesAreBothBasedOnFirstEventDates() { + + // Given + let extractedProfile = ExtractedProfile.mockWithRemovedDate + + let foundEventDate1 = Calendar.current.date(byAdding: .day, value: -20, to: Date.now)! + let foundEventDate2 = Calendar.current.date(byAdding: .day, value: -21, to: Date.now)! + let foundEventDate3 = Calendar.current.date(byAdding: .day, value: -19, to: Date.now)! + let submittedEventDate1 = Calendar.current.date(byAdding: .day, value: -18, to: Date.now)! + let submittedEventDate2 = Calendar.current.date(byAdding: .day, value: -19, to: Date.now)! + let submittedEventDate3 = Calendar.current.date(byAdding: .day, value: -17, to: Date.now)! + let historyEvents = [ + HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .matchesFound(count: 1), date: foundEventDate1), + HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .matchesFound(count: 1), date: foundEventDate2), + HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .matchesFound(count: 1), date: foundEventDate3), + HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .optOutRequested, date: submittedEventDate1), + HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .optOutRequested, date: submittedEventDate2), + HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .optOutRequested, date: submittedEventDate3) + ] + + let createdDate = Date(timeIntervalSince1970: 0) + let submittedDate = Calendar.current.date(byAdding: .day, value: -7, to: Date.now)! + let optOut = OptOutJobData.mock(with: extractedProfile, + historyEvents: historyEvents, + createdDate: createdDate, + submittedSuccessfullyDate: submittedDate) + + // When + let profileMatch = DBPUIDataBrokerProfileMatch(extractedProfile: extractedProfile, + optOutJobData: optOut, + dataBrokerName: "doesn't matter for the test", + databrokerURL: "see above") + + // Then + XCTAssertEqual(profileMatch.foundDate, foundEventDate2.timeIntervalSince1970) + XCTAssertEqual(profileMatch.optOutSubmittedDate, submittedEventDate2.timeIntervalSince1970) + } + +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index fa4e8ce759..9fbf66a9c1 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -1145,6 +1145,13 @@ extension OptOutJobData { .init(brokerId: 1, profileQueryId: 1, createdDate: Date(), historyEvents: historyEvents, extractedProfile: extractedProfile) } + static func mock(with extractedProfile: ExtractedProfile, + historyEvents: [HistoryEvent] = [HistoryEvent](), + createdDate: Date, + submittedSuccessfullyDate: Date?) -> OptOutJobData { + .init(brokerId: 1, profileQueryId: 1, createdDate: createdDate, historyEvents: historyEvents, submittedSuccessfullyDate: submittedSuccessfullyDate, extractedProfile: extractedProfile) + } + static func mock(with type: HistoryEvent.EventType, submittedDate: Date?, sevenDaysConfirmationPixelFired: Bool, From 59988dd891e0d1e54a49a43d0a49e06075c787e8 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 18 Sep 2024 17:54:45 +0100 Subject: [PATCH 07/16] Change estimatedRemovalDate to be calculated from optOutSubmittedDate if it exists --- .../DataBrokerProtection/Model/DBPUICommunicationModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index f55f036353..2be171c488 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -180,7 +180,7 @@ extension DBPUIDataBrokerProfileMatch { let firstOptOutEvent = optOutSubmittedEvents.min(by: { $0.date < $1.date }) optOutSubmittedDate = firstOptOutEvent?.date } - let estimatedRemovalDate = Calendar.current.date(byAdding: .day, value: 14, to: foundDate) + let estimatedRemovalDate = Calendar.current.date(byAdding: .day, value: 14, to: optOutSubmittedDate ?? foundDate) self.init(dataBroker: DBPUIDataBroker(name: dataBrokerName, url: databrokerURL), name: extractedProfile.fullName ?? "No name", addresses: extractedProfile.addresses?.map {DBPUIUserProfileAddress(addressCityState: $0) } ?? [], From 0865a78bcb931dcd91f54c88c33054be93ecbf21 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 18 Sep 2024 17:55:46 +0100 Subject: [PATCH 08/16] Add match testing function to Extracted profile --- .../Model/ExtractedProfile.swift | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift index 5bf40900ae..3412e94def 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/ExtractedProfile.swift @@ -56,7 +56,7 @@ struct ExtractProfileSelectors: Codable, Sendable { } } -struct AddressCityState: Codable { +struct AddressCityState: Codable, Hashable { let city: String let state: String @@ -166,6 +166,39 @@ struct ExtractedProfile: Codable, Sendable { identifier: self.identifier ) } + + /* + Matching records are: + 1/ Completely identical records (same name, addresses, ages, etc) + 2/ Records that overlap completely (record A has all the data of record B, but might have + extra information as well (e.g. an extra address, a middle name where record B doesn't) + I.e. B is a subset of A, or vice versa + However, we ignore some of the properties + So, basically age == age, we ignore phone numbers and email, and then everything else one should be a subset of the other + */ + func doesMatchExtractedProfile(_ extractedProfile: ExtractedProfile) -> Bool { + if age != extractedProfile.age { + return false + } + + if name != extractedProfile.name { + return false + } + + if !(alternativeNames ?? []).isASubSetOrSuperSetOf(extractedProfile.alternativeNames ?? []) { + return false + } + + if !(addresses ?? []).isASubSetOrSuperSetOf(extractedProfile.addresses ?? []) { + return false + } + + if !(relatives ?? []).isASubSetOrSuperSetOf(extractedProfile.relatives ?? []) { + return false + } + + return true + } } extension ExtractedProfile: Equatable { @@ -173,3 +206,11 @@ extension ExtractedProfile: Equatable { lhs.name == rhs.name } } + +private extension Sequence where Element: Hashable { + func isASubSetOrSuperSetOf(_ sequence: Settable) -> Bool where Settable: Sequence, Element == Settable.Element { + let setA = Set(self) + let setB = Set(sequence) + return setA.isSubset(of: setB) || setB.isSubset(of: setA) + } +} From 9aa5b41000f780b561fa4063efa0a686ec408173 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 18 Sep 2024 17:55:57 +0100 Subject: [PATCH 09/16] Add unit tests for matching function --- .../ExtractedProfileTests.swift | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/ExtractedProfileTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/ExtractedProfileTests.swift index 2711b2de6c..0e7f1d14ab 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/ExtractedProfileTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/ExtractedProfileTests.swift @@ -61,4 +61,159 @@ final class ExtractedProfileTests: XCTestCase { XCTAssertEqual(sut.age, "52") } + + // MARK: - Test matching logic + + func testDoesMatchExtractedProfile_whenThereAnExactMatch_thenDoesMatchExtractedProfileIsTrue() { + + // Given + let extractedProfile = ExtractedProfile.mockWithName("Steve Jones", + alternativeNames: ["Steven Jones", + "Steven M Jones"], + age: "20", + addresses: [AddressCityState(city: "New York", state: "NY"), + AddressCityState(city: "Miami", state: "FL")], + relatives: ["Steven Jones Jr", + "Steven Jones Sr", + "Steven Jones Staff", + "Steven Jones Principle"]) + let matchingExtractedProfile = ExtractedProfile.mockWithName("Steve Jones", + alternativeNames: ["Steven Jones", + "Steven M Jones"], + age: "20", + addresses: [AddressCityState(city: "New York", state: "NY"), + AddressCityState(city: "Miami", state: "FL")], + relatives: ["Steven Jones Jr", + "Steven Jones Sr", + "Steven Jones Staff", + "Steven Jones Principle"]) + + // Then + XCTAssertTrue(extractedProfile.doesMatchExtractedProfile(matchingExtractedProfile)) + } + + func testDoesMatchExtractedProfile_whenThereIsANonMatch_thenDoesMatchExtractedProfileIsFalse() { + + // Given + let extractedProfile = ExtractedProfile.mockWithName("Steve Jones", + alternativeNames: ["Steven Jones", + "Steven M Jones"], + age: "20", + addresses: [AddressCityState(city: "New York", state: "NY"), + AddressCityState(city: "Miami", state: "FL")], + relatives: ["Steven Jones Jr", + "Steven Jones Sr", + "Steven Jones Staff", + "Steven Jones Principle"]) + let nonmatchingExtractedProfile = ExtractedProfile.mockWithName("James Smith", + alternativeNames: ["James Jameson and The Legion of Doom"], + age: "57", + addresses: [AddressCityState(city: "Blackpool", state: "NY"), + AddressCityState(city: "Underneath a Volcano", state: "FL")], + relatives: ["Beelzebub", + "Barney the Dinosaur"]) + + // Then + XCTAssertFalse(extractedProfile.doesMatchExtractedProfile(nonmatchingExtractedProfile)) + } + + func testDoesMatchExtractedProfile_whenThereAPartialMatch_thenDoesMatchExtractedProfileIsFalse() { + + // Given + let extractedProfile = ExtractedProfile.mockWithName("Steve Jones", + alternativeNames: ["Steven Jones", + "Steven M Jones"], + age: "20", + addresses: [AddressCityState(city: "New York", state: "NY"), + AddressCityState(city: "Miami", state: "FL")], + relatives: ["Steven Jones Jr", + "Steven Jones Sr", + "Steven Jones Staff", + "Steven Jones Principle"]) + let nonmatchingExtractedProfile = ExtractedProfile.mockWithName("Steve Jones", + alternativeNames: ["Steven Jones", + "Steven M Jones"], + age: "30", + addresses: [AddressCityState(city: "New York", state: "NY"), + AddressCityState(city: "Miami", state: "FL")], + relatives: ["Steven Jones Jr", + "Steven Jones Sr", + "Steven Jones Staff", + "Steven Jones Principle"]) + + // Then + XCTAssertFalse(extractedProfile.doesMatchExtractedProfile(nonmatchingExtractedProfile)) + } + + func testDoesMatchExtractedProfile_whenThereASubsetMatch_thenDoesMatchExtractedProfileIsTrue() { + + // Given + let extractedProfile = ExtractedProfile.mockWithName("Steve Jones", + alternativeNames: ["Steven Jones", + "Steven M Jones"], + age: "20", + addresses: [AddressCityState(city: "New York", state: "NY"), + AddressCityState(city: "Miami", state: "FL")], + relatives: ["Steven Jones Jr", + "Steven Jones Sr", + "Steven Jones Staff", + "Steven Jones Principle"]) + let matchingExtractedProfile = ExtractedProfile.mockWithName("Steve Jones", + alternativeNames: ["Steven Jones"], + age: "20", + addresses: [AddressCityState(city: "Miami", state: "FL")], + relatives: []) + + // Then + XCTAssertTrue(extractedProfile.doesMatchExtractedProfile(matchingExtractedProfile)) + } + + func testDoesMatchExtractedProfile_whenThereIsASupersetMatch_thenDoesMatchExtractedProfileIsTrue() { + + // Given + let extractedProfile = ExtractedProfile.mockWithName("Steve Jones", + alternativeNames: [], + age: "20", + addresses: [AddressCityState(city: "Miami", state: "FL")], + relatives: ["Steven Jones Staff", + "Steven Jones Principle"]) + let matchingExtractedProfile = ExtractedProfile.mockWithName("Steve Jones", + alternativeNames: ["Steven Jones", + "Steven M Jones"], + age: "20", + addresses: [AddressCityState(city: "New York", state: "NY"), + AddressCityState(city: "Miami", state: "FL")], + relatives: ["Steven Jones Jr", + "Steven Jones Sr", + "Steven Jones Staff", + "Steven Jones Principle"]) + + // Then + XCTAssertTrue(extractedProfile.doesMatchExtractedProfile(matchingExtractedProfile)) + } + + // When some fields are subsets, and some are supersets + func testDoesMatchExtractedProfile_whenThereAMixedSubsetSupersetMatch_thenDoesMatchExtractedProfileIsTrue() { + + // Given + let extractedProfile = ExtractedProfile.mockWithName("Steve Jones", + alternativeNames: [], + age: "20", + addresses: [], + relatives: ["Steven Jones Jr", + "Steven Jones Sr", + "Steven Jones Staff", + "Steven Jones Principle"]) + let matchingExtractedProfile = ExtractedProfile.mockWithName("Steve Jones", + alternativeNames: ["Steven Jones", + "Steven M Jones"], + age: "20", + addresses: [AddressCityState(city: "New York", state: "NY"), + AddressCityState(city: "Miami", state: "FL")], + relatives: ["Steven Jones Jr", + "Steven Jones Principle"]) + + // Then + XCTAssertTrue(extractedProfile.doesMatchExtractedProfile(matchingExtractedProfile)) + } } From 1af1ebe0540e921f71fab96f9a96daf254d02cc1 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 18 Sep 2024 18:01:52 +0100 Subject: [PATCH 10/16] Add parent matching to DBPUIDataBrokerProfileMatch --- .../Model/BrokerProfileQueryData.swift | 4 -- .../Model/DBPUICommunicationModel.swift | 33 +++++++++---- .../DataBrokerProtection/UI/UIMapper.swift | 48 +++++++++++++------ 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift index e7d1264cf0..45a23c5d28 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/BrokerProfileQueryData.swift @@ -33,10 +33,6 @@ public struct BrokerProfileQueryData: Sendable { optOutJobData.map { $0.extractedProfile } } - var extractedProfilesWithOptOutJobData: [(ExtractedProfile, OptOutJobData)] { - optOutJobData.map { ($0.extractedProfile, $0) } - } - var events: [HistoryEvent] { operationsData.flatMap { $0.historyEvents }.sorted { $0.date < $1.date } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index 2be171c488..13ccb940dc 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -148,13 +148,16 @@ struct DBPUIDataBrokerProfileMatch: Codable { let optOutSubmittedDate: Double? let estimatedRemovalDate: Double? let removedDate: Double? + let hasMatchingRecordOnParentBroker: Bool } extension DBPUIDataBrokerProfileMatch { - init(extractedProfile: ExtractedProfile, - optOutJobData: OptOutJobData, + init(optOutJobData: OptOutJobData, dataBrokerName: String, - databrokerURL: String) { + databrokerURL: String, + parentBrokerOptOutJobData: [OptOutJobData]?) { + let extractedProfile = optOutJobData.extractedProfile + /* createdDate used to not exist in the DB, so in the migration we defaulted it to Unix Epoch zero (i.e. 1970) If that's the case, we should rely on the events instead @@ -181,6 +184,19 @@ extension DBPUIDataBrokerProfileMatch { optOutSubmittedDate = firstOptOutEvent?.date } let estimatedRemovalDate = Calendar.current.date(byAdding: .day, value: 14, to: optOutSubmittedDate ?? foundDate) + + // Check for any matching records on the parent broker + var hasFoundParentMatch = false + if let parentBrokerOptOutJobData = parentBrokerOptOutJobData { + for parentOptOut in parentBrokerOptOutJobData { + let parentProfile = parentOptOut.extractedProfile + if extractedProfile.doesMatchExtractedProfile(parentProfile) { + hasFoundParentMatch = true + break + } + } + } + self.init(dataBroker: DBPUIDataBroker(name: dataBrokerName, url: databrokerURL), name: extractedProfile.fullName ?? "No name", addresses: extractedProfile.addresses?.map {DBPUIUserProfileAddress(addressCityState: $0) } ?? [], @@ -189,14 +205,15 @@ extension DBPUIDataBrokerProfileMatch { foundDate: foundDate.timeIntervalSince1970, optOutSubmittedDate: optOutSubmittedDate?.timeIntervalSince1970, estimatedRemovalDate: estimatedRemovalDate?.timeIntervalSince1970, - removedDate: extractedProfile.removedDate?.timeIntervalSince1970) + removedDate: extractedProfile.removedDate?.timeIntervalSince1970, + hasMatchingRecordOnParentBroker: hasFoundParentMatch) } - init(extractedProfile: ExtractedProfile, optOutJobData: OptOutJobData, dataBroker: DataBroker) { - self.init(extractedProfile: extractedProfile, - optOutJobData: optOutJobData, + init(optOutJobData: OptOutJobData, dataBroker: DataBroker, parentBrokerOptOutJobData: [OptOutJobData]?) { + self.init(optOutJobData: optOutJobData, dataBrokerName: dataBroker.name, - databrokerURL: dataBroker.url) + databrokerURL: dataBroker.url, + parentBrokerOptOutJobData: parentBrokerOptOutJobData) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index 4b9531cd3c..b035bf5c64 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -44,26 +44,36 @@ struct MapperToUI { totalScans: totalScans, scannedBrokers: partiallyScannedBrokers) - let matches = mapMatchesToUI(brokerProfileQueryData) + let matches = mapMatchesToUI(withoutDeprecated) return .init(resultsFound: matches, scanProgress: scanProgress) } private func mapMatchesToUI(_ brokerProfileQueryData: [BrokerProfileQueryData]) -> [DBPUIDataBrokerProfileMatch] { + + // Used to find opt outs on the parent + let brokerURLsToQueryData = Dictionary(grouping: brokerProfileQueryData, by: { $0.dataBroker.name }) + return brokerProfileQueryData.flatMap { var profiles = [DBPUIDataBrokerProfileMatch]() - for (extractedProfile, optOutJobData) in $0.extractedProfilesWithOptOutJobData where !$0.profileQuery.deprecated { - profiles.append(DBPUIDataBrokerProfileMatch(extractedProfile: extractedProfile, - optOutJobData: optOutJobData, - dataBroker: $0.dataBroker)) + for optOutJobData in $0.optOutJobData { + var parentBrokerOptOutJobData: [OptOutJobData]? + if let parent = $0.dataBroker.parent, + let parentsQueryData = brokerURLsToQueryData[parent] { + parentBrokerOptOutJobData = parentsQueryData.flatMap { $0.optOutJobData } + } + + profiles.append(DBPUIDataBrokerProfileMatch(optOutJobData: optOutJobData, + dataBroker: $0.dataBroker, + parentBrokerOptOutJobData: parentBrokerOptOutJobData)) if !$0.dataBroker.mirrorSites.isEmpty { let mirrorSitesMatches = $0.dataBroker.mirrorSites.compactMap { mirrorSite in if mirrorSite.shouldWeIncludeMirrorSite() { - return DBPUIDataBrokerProfileMatch(extractedProfile: extractedProfile, - optOutJobData: optOutJobData, + return DBPUIDataBrokerProfileMatch(optOutJobData: optOutJobData, dataBrokerName: mirrorSite.name, - databrokerURL: mirrorSite.url) + databrokerURL: mirrorSite.url, + parentBrokerOptOutJobData: parentBrokerOptOutJobData) } return nil @@ -83,14 +93,24 @@ struct MapperToUI { let scansThatRanAtLeastOnce = brokerProfileQueryData.flatMap { $0.sitesScanned } let sitesScanned = Dictionary(grouping: scansThatRanAtLeastOnce, by: { $0 }).count + // Used to find opt outs on the parent + let brokerURLsToQueryData = Dictionary(grouping: brokerProfileQueryData, by: { $0.dataBroker.name }) + brokerProfileQueryData.forEach { let dataBroker = $0.dataBroker let scanJob = $0.scanJobData for optOutJob in $0.optOutJobData { let extractedProfile = optOutJob.extractedProfile - let profileMatch = DBPUIDataBrokerProfileMatch(extractedProfile: extractedProfile, - optOutJobData: optOutJob, - dataBroker: dataBroker) + + var parentBrokerOptOutJobData: [OptOutJobData]? + if let parent = $0.dataBroker.parent, + let parentsQueryData = brokerURLsToQueryData[parent] { + parentBrokerOptOutJobData = parentsQueryData.flatMap { $0.optOutJobData } + } + + let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOutJob, + dataBroker: dataBroker, + parentBrokerOptOutJobData: parentBrokerOptOutJobData) if extractedProfile.removedDate == nil { inProgressOptOuts.append(profileMatch) @@ -100,10 +120,10 @@ struct MapperToUI { if let closestMatchesFoundEvent = scanJob.closestMatchesFoundEvent() { for mirrorSite in dataBroker.mirrorSites where mirrorSite.shouldWeIncludeMirrorSite(for: closestMatchesFoundEvent.date) { - let mirrorSiteMatch = DBPUIDataBrokerProfileMatch(extractedProfile: extractedProfile, - optOutJobData: optOutJob, + let mirrorSiteMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOutJob, dataBrokerName: mirrorSite.name, - databrokerURL: mirrorSite.url) + databrokerURL: mirrorSite.url, + parentBrokerOptOutJobData: parentBrokerOptOutJobData) if let extractedProfileRemovedDate = extractedProfile.removedDate, mirrorSite.shouldWeIncludeMirrorSite(for: extractedProfileRemovedDate) { From 29c8bb811c4af8e5a61207f4df5c42c5e9d89e56 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 18 Sep 2024 18:02:06 +0100 Subject: [PATCH 11/16] Add UIMapper tests for profile matching --- .../DBPUICommunicationModelTests.swift | 121 ++++++++++++++++-- ...kerProfileQueryOperationManagerTests.swift | 4 + 2 files changed, 116 insertions(+), 9 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift index 5c12395815..165441d30b 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift @@ -42,10 +42,10 @@ final class DBPUICommunicationModelTests: XCTestCase { submittedSuccessfullyDate: submittedDate) // When - let profileMatch = DBPUIDataBrokerProfileMatch(extractedProfile: extractedProfile, - optOutJobData: optOut, + let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut, dataBrokerName: "doesn't matter for the test", - databrokerURL: "see above") + databrokerURL: "see above", + parentBrokerOptOutJobData: nil) // Then XCTAssertEqual(profileMatch.foundDate, createdDate.timeIntervalSince1970) @@ -72,10 +72,10 @@ final class DBPUICommunicationModelTests: XCTestCase { submittedSuccessfullyDate: submittedDate) // When - let profileMatch = DBPUIDataBrokerProfileMatch(extractedProfile: extractedProfile, - optOutJobData: optOut, + let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut, dataBrokerName: "doesn't matter for the test", - databrokerURL: "see above") + databrokerURL: "see above", + parentBrokerOptOutJobData: nil) // Then XCTAssertEqual(profileMatch.foundDate, foundEventDate.timeIntervalSince1970) @@ -110,14 +110,117 @@ final class DBPUICommunicationModelTests: XCTestCase { submittedSuccessfullyDate: submittedDate) // When - let profileMatch = DBPUIDataBrokerProfileMatch(extractedProfile: extractedProfile, - optOutJobData: optOut, + let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut, dataBrokerName: "doesn't matter for the test", - databrokerURL: "see above") + databrokerURL: "see above", + parentBrokerOptOutJobData: nil) // Then XCTAssertEqual(profileMatch.foundDate, foundEventDate2.timeIntervalSince1970) XCTAssertEqual(profileMatch.optOutSubmittedDate, submittedEventDate2.timeIntervalSince1970) } + /* + test cases + one exact matching parent + one exact matching parent mixed in the array (probs can combnie with above + no match + partial match + */ + + func testProfileMatchInit_whenThereIsExactParentMatch_thenHasMatchingRecordOnParentBrokerIsTrue() { + + // Given + let extractedProfile = ExtractedProfile.mockWithName("Steve Jones", age: "20", addresses: [AddressCityState(city: "New York", state: "NY")]) + let parentProfile = ExtractedProfile.mockWithName("Steve Jones", age: "20", addresses: [AddressCityState(city: "New York", state: "NY")]) + + let optOut = OptOutJobData.mock(with: extractedProfile, + historyEvents: []) + let parentOptOut = OptOutJobData.mock(with: parentProfile, + historyEvents: []) + + // When + let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut, + dataBrokerName: "doesn't matter for the test", + databrokerURL: "see above", + parentBrokerOptOutJobData: [parentOptOut]) + + // Then + XCTAssertTrue(profileMatch.hasMatchingRecordOnParentBroker) + } + + func testProfileMatchInit_whenThereAreMultipleNonMatchingProfilesAndAnExactParentMatch_thenHasMatchingRecordOnParentBrokerIsTrue() { + + // Given + let extractedProfile = ExtractedProfile.mockWithName("Steve Jones", age: "20", addresses: [AddressCityState(city: "New York", state: "NY")]) + let parentProfileMatching = ExtractedProfile.mockWithName("Steve Jones", age: "20", addresses: [AddressCityState(city: "New York", state: "NY")]) + let parentProfileNonmatching1 = ExtractedProfile.mockWithName("Steve Jones", age: "30", addresses: [AddressCityState(city: "New York", state: "NY")]) + let parentProfileNonmatching2 = ExtractedProfile.mockWithName("Jamie Jones", age: "20", addresses: [AddressCityState(city: "New York", state: "NY")]) + + let optOut = OptOutJobData.mock(with: extractedProfile, + historyEvents: []) + let parentOptOutMatching = OptOutJobData.mock(with: parentProfileMatching, + historyEvents: []) + let parentOptOutNonmatching1 = OptOutJobData.mock(with: parentProfileNonmatching1, + historyEvents: []) + let parentOptOutNonmatching2 = OptOutJobData.mock(with: parentProfileNonmatching2, + historyEvents: []) + + // When + let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut, + dataBrokerName: "doesn't matter for the test", + databrokerURL: "see above", + parentBrokerOptOutJobData: [parentOptOutNonmatching1, + parentOptOutMatching, + parentOptOutNonmatching2]) + + // Then + XCTAssertTrue(profileMatch.hasMatchingRecordOnParentBroker) + } + + func testProfileMatchInit_whenThereIsNoParentMatch_thenHasMatchingRecordOnParentBrokerIsFalse() { + + // Given + let extractedProfile = ExtractedProfile.mockWithName("Steve Jones", age: "20", addresses: [AddressCityState(city: "New York", state: "NY")]) + let parentProfileNonmatching1 = ExtractedProfile.mockWithName("Steve Jones", age: "30", addresses: [AddressCityState(city: "New York", state: "NY")]) + let parentProfileNonmatching2 = ExtractedProfile.mockWithName("Jamie Jones", age: "20", addresses: [AddressCityState(city: "New York", state: "NY")]) + + let optOut = OptOutJobData.mock(with: extractedProfile, + historyEvents: []) + let parentOptOutNonmatching1 = OptOutJobData.mock(with: parentProfileNonmatching1, + historyEvents: []) + let parentOptOutNonmatching2 = OptOutJobData.mock(with: parentProfileNonmatching2, + historyEvents: []) + + // When + let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut, + dataBrokerName: "doesn't matter for the test", + databrokerURL: "see above", + parentBrokerOptOutJobData: [parentOptOutNonmatching1, + parentOptOutNonmatching2]) + + // Then + XCTAssertFalse(profileMatch.hasMatchingRecordOnParentBroker) + } + + func testProfileMatchInit_whenThereIsANonExactParentMatch_thenHasMatchingRecordOnParentBrokerIsTrue() { + + // Given + let extractedProfile = ExtractedProfile.mockWithName("Steve Jones", age: "20", addresses: [AddressCityState(city: "New York", state: "NY")]) + let parentProfile = ExtractedProfile.mockWithName("Steve Jones", age: "20", addresses: [AddressCityState(city: "New York", state: "NY"), AddressCityState(city: "Atlanta", state: "GA")]) + + let optOut = OptOutJobData.mock(with: extractedProfile, + historyEvents: []) + let parentOptOut = OptOutJobData.mock(with: parentProfile, + historyEvents: []) + + // When + let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut, + dataBrokerName: "doesn't matter for the test", + databrokerURL: "see above", + parentBrokerOptOutJobData: [parentOptOut]) + + // Then + XCTAssertTrue(profileMatch.hasMatchingRecordOnParentBroker) + } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift index af88800ccb..b41f802d47 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProfileQueryOperationManagerTests.swift @@ -1074,6 +1074,10 @@ extension ExtractedProfile { static func mockWithRemoveDate(_ date: Date) -> ExtractedProfile { ExtractedProfile(id: 1, name: "Some name", profileUrl: "someURL", removedDate: date, identifier: "someURL") } + + static func mockWithName(_ name: String, alternativeNames: [String]? = nil, age: String, addresses: [AddressCityState], relatives: [String]? = nil) -> ExtractedProfile { + ExtractedProfile(id: 1, name: name, alternativeNames: [], addressFull: nil, addresses: addresses, phoneNumbers: nil, relatives: nil, profileUrl: "someUrl", age: age, identifier: "someUrl") + } } extension AttemptInformation { From c2d4d9f2a96266b3733756c172d4e3653fdc8b90 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 18 Sep 2024 18:24:43 +0100 Subject: [PATCH 12/16] Add parent URL field to UIDataBroker --- .../DataBrokerProtectionDataManager.swift | 4 +- .../Model/DBPUICommunicationModel.swift | 12 ++++-- .../DataBrokerProtection/UI/UIMapper.swift | 41 ++++++++++++++----- .../DBPUICommunicationModelTests.swift | 21 ++++++---- 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift index 00f51edc6f..767dfcfd18 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift @@ -320,10 +320,10 @@ extension InMemoryDataCache: DBPUICommunicationDelegate { // 2. We map the brokers to the UI model .flatMap { dataBroker -> [DBPUIDataBroker] in var result: [DBPUIDataBroker] = [] - result.append(DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url)) + result.append(DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url, parentURL: dataBroker.parent)) for mirrorSite in dataBroker.mirrorSites { - result.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url)) + result.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, parentURL: dataBroker.parent)) } return result } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index 13ccb940dc..5792b9e637 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -114,11 +114,13 @@ struct DBPUIDataBroker: Codable, Hashable { let name: String let url: String let date: Double? + let parentURL: String? - init(name: String, url: String, date: Double? = nil) { + init(name: String, url: String, date: Double? = nil, parentURL: String?) { self.name = name self.url = url self.date = date + self.parentURL = parentURL } func hash(into hasher: inout Hasher) { @@ -154,7 +156,8 @@ struct DBPUIDataBrokerProfileMatch: Codable { extension DBPUIDataBrokerProfileMatch { init(optOutJobData: OptOutJobData, dataBrokerName: String, - databrokerURL: String, + dataBrokerURL: String, + dataBrokerParentURL: String?, parentBrokerOptOutJobData: [OptOutJobData]?) { let extractedProfile = optOutJobData.extractedProfile @@ -197,7 +200,7 @@ extension DBPUIDataBrokerProfileMatch { } } - self.init(dataBroker: DBPUIDataBroker(name: dataBrokerName, url: databrokerURL), + self.init(dataBroker: DBPUIDataBroker(name: dataBrokerName, url: dataBrokerURL, parentURL: dataBrokerParentURL), name: extractedProfile.fullName ?? "No name", addresses: extractedProfile.addresses?.map {DBPUIUserProfileAddress(addressCityState: $0) } ?? [], alternativeNames: extractedProfile.alternativeNames ?? [String](), @@ -212,7 +215,8 @@ extension DBPUIDataBrokerProfileMatch { init(optOutJobData: OptOutJobData, dataBroker: DataBroker, parentBrokerOptOutJobData: [OptOutJobData]?) { self.init(optOutJobData: optOutJobData, dataBrokerName: dataBroker.name, - databrokerURL: dataBroker.url, + dataBrokerURL: dataBroker.url, + dataBrokerParentURL: dataBroker.parent, parentBrokerOptOutJobData: parentBrokerOptOutJobData) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index b035bf5c64..4d079506bf 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -57,22 +57,25 @@ struct MapperToUI { return brokerProfileQueryData.flatMap { var profiles = [DBPUIDataBrokerProfileMatch]() for optOutJobData in $0.optOutJobData { + let dataBroker = $0.dataBroker + var parentBrokerOptOutJobData: [OptOutJobData]? - if let parent = $0.dataBroker.parent, + if let parent = dataBroker.parent, let parentsQueryData = brokerURLsToQueryData[parent] { parentBrokerOptOutJobData = parentsQueryData.flatMap { $0.optOutJobData } } profiles.append(DBPUIDataBrokerProfileMatch(optOutJobData: optOutJobData, - dataBroker: $0.dataBroker, + dataBroker: dataBroker, parentBrokerOptOutJobData: parentBrokerOptOutJobData)) - if !$0.dataBroker.mirrorSites.isEmpty { - let mirrorSitesMatches = $0.dataBroker.mirrorSites.compactMap { mirrorSite in + if !dataBroker.mirrorSites.isEmpty { + let mirrorSitesMatches = dataBroker.mirrorSites.compactMap { mirrorSite in if mirrorSite.shouldWeIncludeMirrorSite() { return DBPUIDataBrokerProfileMatch(optOutJobData: optOutJobData, dataBrokerName: mirrorSite.name, - databrokerURL: mirrorSite.url, + dataBrokerURL: mirrorSite.url, + dataBrokerParentURL: dataBroker.parent, parentBrokerOptOutJobData: parentBrokerOptOutJobData) } @@ -122,7 +125,8 @@ struct MapperToUI { for mirrorSite in dataBroker.mirrorSites where mirrorSite.shouldWeIncludeMirrorSite(for: closestMatchesFoundEvent.date) { let mirrorSiteMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOutJob, dataBrokerName: mirrorSite.name, - databrokerURL: mirrorSite.url, + dataBrokerURL: mirrorSite.url, + dataBrokerParentURL: dataBroker.parent, parentBrokerOptOutJobData: parentBrokerOptOutJobData) if let extractedProfileRemovedDate = extractedProfile.removedDate, @@ -168,10 +172,16 @@ struct MapperToUI { } .flatMap { var brokers = [DBPUIDataBroker]() - brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: $0.scanJobData.lastRunDate!.timeIntervalSince1970)) + brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, + url: $0.dataBroker.url, + date: $0.scanJobData.lastRunDate!.timeIntervalSince1970, + parentURL: $0.dataBroker.parent)) for mirrorSite in $0.dataBroker.mirrorSites where mirrorSite.addedAt < $0.scanJobData.lastRunDate! { - brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.lastRunDate!.timeIntervalSince1970)) + brokers.append(DBPUIDataBroker(name: mirrorSite.name, + url: mirrorSite.url, + date: $0.scanJobData.lastRunDate!.timeIntervalSince1970, + parentURL: $0.dataBroker.parent)) } return brokers @@ -198,15 +208,24 @@ struct MapperToUI { } .flatMap { var brokers = [DBPUIDataBroker]() - brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, url: $0.dataBroker.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970)) + brokers.append(DBPUIDataBroker(name: $0.dataBroker.name, + url: $0.dataBroker.url, + date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970, + parentURL: $0.dataBroker.parent)) for mirrorSite in $0.dataBroker.mirrorSites { if let removedDate = mirrorSite.removedAt { if removedDate > $0.scanJobData.preferredRunDate! { - brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970)) + brokers.append(DBPUIDataBroker(name: mirrorSite.name, + url: mirrorSite.url, + date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970, + parentURL: $0.dataBroker.parent)) } } else { - brokers.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970)) + brokers.append(DBPUIDataBroker(name: mirrorSite.name, + url: mirrorSite.url, + date: $0.scanJobData.preferredRunDate!.timeIntervalSince1970, + parentURL: $0.dataBroker.parent)) } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift index 165441d30b..68b71f4fdd 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DBPUICommunicationModelTests.swift @@ -44,7 +44,8 @@ final class DBPUICommunicationModelTests: XCTestCase { // When let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut, dataBrokerName: "doesn't matter for the test", - databrokerURL: "see above", + dataBrokerURL: "see above", + dataBrokerParentURL: "whatever", parentBrokerOptOutJobData: nil) // Then @@ -74,7 +75,8 @@ final class DBPUICommunicationModelTests: XCTestCase { // When let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut, dataBrokerName: "doesn't matter for the test", - databrokerURL: "see above", + dataBrokerURL: "see above", + dataBrokerParentURL: "whatever", parentBrokerOptOutJobData: nil) // Then @@ -112,7 +114,8 @@ final class DBPUICommunicationModelTests: XCTestCase { // When let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut, dataBrokerName: "doesn't matter for the test", - databrokerURL: "see above", + dataBrokerURL: "see above", + dataBrokerParentURL: "whatever", parentBrokerOptOutJobData: nil) // Then @@ -142,7 +145,8 @@ final class DBPUICommunicationModelTests: XCTestCase { // When let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut, dataBrokerName: "doesn't matter for the test", - databrokerURL: "see above", + dataBrokerURL: "see above", + dataBrokerParentURL: "whatever", parentBrokerOptOutJobData: [parentOptOut]) // Then @@ -169,7 +173,8 @@ final class DBPUICommunicationModelTests: XCTestCase { // When let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut, dataBrokerName: "doesn't matter for the test", - databrokerURL: "see above", + dataBrokerURL: "see above", + dataBrokerParentURL: "whatever", parentBrokerOptOutJobData: [parentOptOutNonmatching1, parentOptOutMatching, parentOptOutNonmatching2]) @@ -195,7 +200,8 @@ final class DBPUICommunicationModelTests: XCTestCase { // When let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut, dataBrokerName: "doesn't matter for the test", - databrokerURL: "see above", + dataBrokerURL: "see above", + dataBrokerParentURL: "whatever", parentBrokerOptOutJobData: [parentOptOutNonmatching1, parentOptOutNonmatching2]) @@ -217,7 +223,8 @@ final class DBPUICommunicationModelTests: XCTestCase { // When let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut, dataBrokerName: "doesn't matter for the test", - databrokerURL: "see above", + dataBrokerURL: "see above", + dataBrokerParentURL: "whatever", parentBrokerOptOutJobData: [parentOptOut]) // Then From 06b1020e4d46127ab83e20d5c28019703b39ec4c Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Fri, 20 Sep 2024 18:58:49 +0100 Subject: [PATCH 13/16] Fix incorrect data being given when looking for parent matches --- .../Sources/DataBrokerProtection/UI/UIMapper.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index 4d079506bf..d59fbd1f38 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -52,7 +52,7 @@ struct MapperToUI { private func mapMatchesToUI(_ brokerProfileQueryData: [BrokerProfileQueryData]) -> [DBPUIDataBrokerProfileMatch] { // Used to find opt outs on the parent - let brokerURLsToQueryData = Dictionary(grouping: brokerProfileQueryData, by: { $0.dataBroker.name }) + let brokerURLsToQueryData = Dictionary(grouping: brokerProfileQueryData, by: { $0.dataBroker.url }) return brokerProfileQueryData.flatMap { var profiles = [DBPUIDataBrokerProfileMatch]() @@ -97,7 +97,7 @@ struct MapperToUI { let sitesScanned = Dictionary(grouping: scansThatRanAtLeastOnce, by: { $0 }).count // Used to find opt outs on the parent - let brokerURLsToQueryData = Dictionary(grouping: brokerProfileQueryData, by: { $0.dataBroker.name }) + let brokerURLsToQueryData = Dictionary(grouping: brokerProfileQueryData, by: { $0.dataBroker.url }) brokerProfileQueryData.forEach { let dataBroker = $0.dataBroker From 88fb01bdaf02cf9f3dd06e7cee26ee2a963a44c4 Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Tue, 24 Sep 2024 11:46:12 +0100 Subject: [PATCH 14/16] Simplify parent match logic --- .../Model/DBPUICommunicationModel.swift | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index 5792b9e637..e9416cc242 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -189,16 +189,9 @@ extension DBPUIDataBrokerProfileMatch { let estimatedRemovalDate = Calendar.current.date(byAdding: .day, value: 14, to: optOutSubmittedDate ?? foundDate) // Check for any matching records on the parent broker - var hasFoundParentMatch = false - if let parentBrokerOptOutJobData = parentBrokerOptOutJobData { - for parentOptOut in parentBrokerOptOutJobData { - let parentProfile = parentOptOut.extractedProfile - if extractedProfile.doesMatchExtractedProfile(parentProfile) { - hasFoundParentMatch = true - break - } - } - } + let hasFoundParentMatch = parentBrokerOptOutJobData?.contains { parentOptOut in + extractedProfile.doesMatchExtractedProfile(parentOptOut.extractedProfile) + } ?? false self.init(dataBroker: DBPUIDataBroker(name: dataBrokerName, url: dataBrokerURL, parentURL: dataBrokerParentURL), name: extractedProfile.fullName ?? "No name", From 608bd9f8f6a1c59697c94f5465fa0aadfa63312c Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 25 Sep 2024 14:35:01 +0100 Subject: [PATCH 15/16] Fix estimated removal date not being propagated to completed opt outs --- .../DataBrokerProtection/Model/DBPUICommunicationModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift index e9416cc242..13379680a9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUICommunicationModel.swift @@ -250,7 +250,7 @@ extension DBPUIOptOutMatch { date: removedDate, foundDate: profileMatch.foundDate, optOutSubmittedDate: profileMatch.optOutSubmittedDate, - estimatedRemovalDate: nil, + estimatedRemovalDate: profileMatch.estimatedRemovalDate, removedDate: removedDate) } } From b389b6b49842f6ae3869d9d4a93c9ed23ec4395d Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Wed, 25 Sep 2024 18:34:06 +0100 Subject: [PATCH 16/16] bump native - FE api version number --- .../DataBrokerProtection/UI/DBPUICommunicationLayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift index 7a45fc4aa1..7928ba2b4d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift @@ -74,7 +74,7 @@ struct DBPUICommunicationLayer: Subfeature { weak var delegate: DBPUICommunicationDelegate? private enum Constants { - static let version = 4 + static let version = 6 } internal init(webURLSettings: DataBrokerProtectionWebUIURLSettingsRepresentable) {