Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add timelineEvents to DBPUIDataBrokerProfileMatch #3535

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,93 @@ struct DBPUIDataBrokerList: DBPUISendableMessage {
let dataBrokers: [DBPUIDataBroker]
}

/// Message Object representing a requested change to the user profile's brith year
/// Message Object representing a requested change to the user profile's birth year
struct DBPUIBirthYear: Codable {
let year: Int
}

/// Message Object representing a supported timeline event
/// https://app.asana.com/0/481882893211075/1208663928051302/f
struct DBPUITimelineEvent: Codable {
enum EventType: String, Codable {
case recordFound = "record-found"
case recordReappeared = "record-reappeared"
case optOutSubmitted = "opt-out-submitted"
case estimatedRemoval = "estimated-removal"
case removed = "removed"
}

let type: EventType
let date: Double

var eventDate: Date {
quanganhdo marked this conversation as resolved.
Show resolved Hide resolved
Date(timeIntervalSince1970: date)
}

init?(type: EventType, date: Date?) {
guard let date else { return nil }
self.type = type
self.date = date.timeIntervalSince1970
}
}

extension DBPUITimelineEvent {
init?(foundDate: Date?) {
self.init(type: .recordFound, date: foundDate)
}

init?(reappearedDate: Date?) {
self.init(type: .recordReappeared, date: reappearedDate)
}

init?(optOutSubmittedDate: Date?) {
self.init(type: .optOutSubmitted, date: optOutSubmittedDate)
}

init?(estimatedRemovalDate: Date?) {
self.init(type: .estimatedRemoval, date: estimatedRemovalDate)
}

init?(removedDate: Date?) {
self.init(type: .removed, date: removedDate)
}

static func from(historyEvents: [HistoryEvent], removedDate: Date?) -> [DBPUITimelineEvent] {
var timelineEvents = historyEvents.compactMap { event in
switch event.type {
case .matchesFound:
return DBPUITimelineEvent(foundDate: event.date)
case .reAppearence:
return DBPUITimelineEvent(reappearedDate: event.date)
case .optOutRequested:
return DBPUITimelineEvent(optOutSubmittedDate: event.date)
default:
return nil
}
}

let mostRecentFoundEvent = timelineEvents.filter({ $0.type == .recordFound || $0.type == .recordReappeared }).max(by: <)
let mostRecentOptOutSubmittedEvent = timelineEvents.filter({ $0.type == .optOutSubmitted }).max(by: <)

if let optOutSubmittedDate = (mostRecentOptOutSubmittedEvent ?? mostRecentFoundEvent)?.eventDate,
let estimatedRemovalEvent = DBPUITimelineEvent(estimatedRemovalDate: Calendar.current.date(byAdding: .day, value: 14, to: optOutSubmittedDate)) {
timelineEvents.append(estimatedRemovalEvent)
}

if let removedEvent = DBPUITimelineEvent(removedDate: removedDate) {
timelineEvents.append(removedEvent)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I right in thinking that if the entry has been removed, then the timeline will have an estimated removal date event, followed by an actual removed event? Is that what we want?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good point. Logically we shouldn't have the estimated removal date when the profile has been removed 😅

}

return timelineEvents
}
}

extension DBPUITimelineEvent: Comparable {
static func < (lhs: DBPUITimelineEvent, rhs: DBPUITimelineEvent) -> Bool {
lhs.date < rhs.date
}
}

/// Message object containing information related to a profile match on a data broker
/// The message contains the data broker on which the profile was found and the names
/// and addresses that were matched
Expand All @@ -160,11 +242,24 @@ struct DBPUIDataBrokerProfileMatch: Codable {
let addresses: [DBPUIUserProfileAddress]
let alternativeNames: [String]
let relatives: [String]
let foundDate: Double
let optOutSubmittedDate: Double?
let estimatedRemovalDate: Double?
let removedDate: Double?
let timelineEvents: [DBPUITimelineEvent]
let hasMatchingRecordOnParentBroker: Bool

init(dataBroker: DBPUIDataBroker,
name: String,
addresses: [DBPUIUserProfileAddress],
alternativeNames: [String],
relatives: [String],
timelineEvents: [DBPUITimelineEvent],
hasMatchingRecordOnParentBroker: Bool) {
self.dataBroker = dataBroker
self.name = name
self.addresses = addresses
self.alternativeNames = alternativeNames
self.relatives = relatives
self.timelineEvents = timelineEvents.sorted(by: <)
self.hasMatchingRecordOnParentBroker = hasMatchingRecordOnParentBroker
}
}

extension DBPUIDataBrokerProfileMatch {
Expand All @@ -175,33 +270,7 @@ extension DBPUIDataBrokerProfileMatch {
parentBrokerOptOutJobData: [OptOutJobData]?,
optOutUrl: String) {
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
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: optOutSubmittedDate ?? foundDate)
let timelineEvents = DBPUITimelineEvent.from(historyEvents: optOutJobData.historyEvents, removedDate: extractedProfile.removedDate)

// Check for any matching records on the parent broker
let hasFoundParentMatch = parentBrokerOptOutJobData?.contains { parentOptOut in
Expand All @@ -213,10 +282,7 @@ extension DBPUIDataBrokerProfileMatch {
addresses: extractedProfile.addresses?.map {DBPUIUserProfileAddress(addressCityState: $0) } ?? [],
alternativeNames: extractedProfile.alternativeNames ?? [String](),
relatives: extractedProfile.relatives ?? [String](),
foundDate: foundDate.timeIntervalSince1970,
optOutSubmittedDate: optOutSubmittedDate?.timeIntervalSince1970,
estimatedRemovalDate: estimatedRemovalDate?.timeIntervalSince1970,
removedDate: extractedProfile.removedDate?.timeIntervalSince1970,
timelineEvents: timelineEvents,
hasMatchingRecordOnParentBroker: hasFoundParentMatch)
}

Expand Down Expand Up @@ -301,26 +367,20 @@ struct DBPUIOptOutMatch: DBPUISendableMessage {
let alternativeNames: [String]
let addresses: [DBPUIUserProfileAddress]
let date: Double
let foundDate: Double
let optOutSubmittedDate: Double?
let estimatedRemovalDate: Double?
let removedDate: Double?
let timelineEvents: [DBPUITimelineEvent]
}

extension DBPUIOptOutMatch {
init?(profileMatch: DBPUIDataBrokerProfileMatch, matches: Int) {
guard let removedDate = profileMatch.removedDate else { return nil }
guard let removedDate = profileMatch.timelineEvents.first(where: { $0.type == .removed })?.date 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: profileMatch.estimatedRemovalDate,
removedDate: removedDate)
timelineEvents: profileMatch.timelineEvents)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ struct DBPUICommunicationLayer: Subfeature {
weak var delegate: DBPUICommunicationDelegate?

private enum Constants {
static let version = 8
static let version = 9
}

internal init(webURLSettings: DataBrokerProtectionWebUIURLSettingsRepresentable,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,110 @@ import Foundation

final class DBPUICommunicationModelTests: XCTestCase {

func testProfileMatchInit_whenCreatedDateIsNotDefault_thenResultingProfileMatchDatesAreBothBasedOnOptOutJobDataDates() {
func testProfileMatch_whenInitWithEmptyHistoryEventsAndNoRemovedDate_thenTimelineEventsAreAlsoEmpty() {
// Given
let historyEvents = [HistoryEvent]()

let mockDataBroker = DBPUIDataBroker(name: "some broker", url: "broker.com", parentURL: nil, optOutUrl: "some url")

// When
let timelineEvents = DBPUITimelineEvent.from(historyEvents: historyEvents, removedDate: nil)
let profileMatch = DBPUIDataBrokerProfileMatch(dataBroker: mockDataBroker,
name: "some profile",
addresses: [],
alternativeNames: [],
relatives: [],
timelineEvents: timelineEvents,
hasMatchingRecordOnParentBroker: true)

// Then
XCTAssertEqual(profileMatch.timelineEvents, [])
}

func testProfileMatch_whenInit_thenTimelineEventsAreFilteredAndSortedChronologically() {
// Given
let extractedProfile = ExtractedProfile.mockWithRemovedDate
let historyEvents = [
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .scanStarted, date: Date(timeIntervalSince1970: 0)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .noMatchFound, date: Date(timeIntervalSince1970: 50)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .matchesFound(count: 2), date: Date(timeIntervalSince1970: 100)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 2, type: .reAppearence, date: Date(timeIntervalSince1970: 2500)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 2, type: .optOutStarted, date: Date(timeIntervalSince1970: 2000)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 2, type: .optOutRequested, date: Date(timeIntervalSince1970: 2000)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 1, type: .optOutStarted, date: Date(timeIntervalSince1970: 1000)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 1, type: .optOutRequested, date: Date(timeIntervalSince1970: 1000)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 1, type: .reAppearence, date: Date(timeIntervalSince1970: 1500)),
]

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 expectedTimelineEvents = [
DBPUITimelineEvent(foundDate: Date(timeIntervalSince1970: 100)),
DBPUITimelineEvent(optOutSubmittedDate: Date(timeIntervalSince1970: 1000)),
DBPUITimelineEvent(reappearedDate: Date(timeIntervalSince1970: 1500)),
DBPUITimelineEvent(optOutSubmittedDate: Date(timeIntervalSince1970: 2000)),
DBPUITimelineEvent(reappearedDate: Date(timeIntervalSince1970: 2500)),
DBPUITimelineEvent(removedDate: Date(timeIntervalSince1970: 5000)),
DBPUITimelineEvent(estimatedRemovalDate: Calendar.current.date(byAdding: .day, value: 14, to: Date(timeIntervalSince1970: 2000))),
]

let mockDataBroker = DBPUIDataBroker(name: "some broker", url: "broker.com", parentURL: nil, optOutUrl: "some url")

// When
let timelineEvents = DBPUITimelineEvent.from(historyEvents: historyEvents, removedDate: Date(timeIntervalSince1970: 5000))
let profileMatch = DBPUIDataBrokerProfileMatch(dataBroker: mockDataBroker,
name: "some profile",
addresses: [],
alternativeNames: [],
relatives: [],
timelineEvents: timelineEvents,
hasMatchingRecordOnParentBroker: true)

// Then
XCTAssertEqual(profileMatch.timelineEvents, expectedTimelineEvents)
}

func testOptOutMatchInitializer() {
// Given
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)
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .scanStarted, date: Date(timeIntervalSince1970: 0)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .noMatchFound, date: Date(timeIntervalSince1970: 50)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 0, type: .matchesFound(count: 2), date: Date(timeIntervalSince1970: 100)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 2, type: .reAppearence, date: Date(timeIntervalSince1970: 2500)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 2, type: .optOutStarted, date: Date(timeIntervalSince1970: 2000)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 2, type: .optOutRequested, date: Date(timeIntervalSince1970: 2000)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 1, type: .optOutStarted, date: Date(timeIntervalSince1970: 1000)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 1, type: .optOutRequested, date: Date(timeIntervalSince1970: 1000)),
HistoryEvent(extractedProfileId: 0, brokerId: 0, profileQueryId: 1, type: .reAppearence, date: Date(timeIntervalSince1970: 1500)),
]

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)
let expectedTimelineEvents = [
DBPUITimelineEvent(foundDate: Date(timeIntervalSince1970: 100)),
DBPUITimelineEvent(optOutSubmittedDate: Date(timeIntervalSince1970: 1000)),
DBPUITimelineEvent(reappearedDate: Date(timeIntervalSince1970: 1500)),
DBPUITimelineEvent(optOutSubmittedDate: Date(timeIntervalSince1970: 2000)),
DBPUITimelineEvent(reappearedDate: Date(timeIntervalSince1970: 2500)),
DBPUITimelineEvent(removedDate: Date(timeIntervalSince1970: 5000)),
DBPUITimelineEvent(estimatedRemovalDate: Calendar.current.date(byAdding: .day, value: 14, to: Date(timeIntervalSince1970: 2000))),
]

let mockDataBroker = DBPUIDataBroker(name: "some broker", url: "broker.com", parentURL: nil, optOutUrl: "some url")

// When
let profileMatch = DBPUIDataBrokerProfileMatch(optOutJobData: optOut,
dataBrokerName: "doesn't matter for the test",
dataBrokerURL: "see above",
dataBrokerParentURL: "whatever",
parentBrokerOptOutJobData: nil,
optOutUrl: "broker.com")
let timelineEvents = DBPUITimelineEvent.from(historyEvents: historyEvents, removedDate: Date(timeIntervalSince1970: 5000))
let profileMatch = DBPUIDataBrokerProfileMatch(dataBroker: mockDataBroker,
name: "some profile",
addresses: [],
alternativeNames: [],
relatives: [],
timelineEvents: timelineEvents,
hasMatchingRecordOnParentBroker: true)

let optOutMatch = DBPUIOptOutMatch(profileMatch: profileMatch, matches: 1)

// Then
XCTAssertEqual(profileMatch.foundDate, createdDate.timeIntervalSince1970)
XCTAssertEqual(profileMatch.optOutSubmittedDate, submittedDate.timeIntervalSince1970)
XCTAssertEqual(optOutMatch?.timelineEvents, expectedTimelineEvents)
XCTAssertEqual(optOutMatch?.date, 5000)
}

func testProfileMatchInit_whenCreatedDateIsDefault_thenResultingProfileMatchDatesAreBothBasedOnEventDates() {
func testProfileMatch_whenInit_thenResultingProfileMatchDatesAreBothBasedOnEventDates() {

// Given
let extractedProfile = ExtractedProfile.mockWithRemovedDate
Expand Down Expand Up @@ -86,7 +157,7 @@ final class DBPUICommunicationModelTests: XCTestCase {
XCTAssertEqual(profileMatch.optOutSubmittedDate, submittedEventDate.timeIntervalSince1970)
}

func testProfileMatchInit_whenCreatedDateIsDefaultAndThereAreMultipleEventsOfTheSameType_thenResultingProfileMatchDatesAreBothBasedOnFirstEventDates() {
func testProfileMatch_whenInit_thenResultingProfileMatchDatesAreBothBasedOnFirstEventDates() {

// Given
let extractedProfile = ExtractedProfile.mockWithRemovedDate
Expand Down Expand Up @@ -298,3 +369,13 @@ final class DBPUICommunicationModelTests: XCTestCase {
XCTAssertEqual(childProfile?.dataBroker.optOutUrl, "parent.com/optout")
}
}

extension DBPUIDataBrokerProfileMatch {
var foundDate: Double? {
timelineEvents.first(where: { $0.type == .recordFound })?.date
}

var optOutSubmittedDate: Double? {
timelineEvents.first(where: { $0.type == .optOutSubmitted })?.date
}
}
Loading