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

Remote Messaging Framework for macOS #876

Merged
merged 40 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a7adf0f
Add numberOfBookmarks and numberOfFavorites to BookmarkUtils
ayoy Jun 25, 2024
419e6df
Add RemoteMessagingClientBase class
ayoy Jun 25, 2024
dfe0f9c
Update data source
ayoy Jun 26, 2024
fcdfa1d
Update RemoteMessagingClientBase.fetchAndProcess
ayoy Jun 26, 2024
861d19c
Fix formatting
ayoy Jun 26, 2024
59935c9
Rename RemoteMessagingDataSource as RemoteMessagingConfigMatcherProvi…
ayoy Jun 26, 2024
cc6afff
Replace RemoteMessagingClientBase with RemoteMessagingProcessing prot…
ayoy Jun 27, 2024
61f6379
Make some RemoteMessagingConfigProcessor APIs private
ayoy Jun 27, 2024
65e1d06
Use ConfigurationFetcher for fetching RMF config
ayoy Jun 27, 2024
d484ebc
Use proper ConfigurationFetcher API
ayoy Jun 27, 2024
80a8a79
Add mobile and desktop user attribute matchers
ayoy Jun 29, 2024
088bf37
Abstract attribute matchers in RemoteMessagingConfigMatcher
ayoy Jun 30, 2024
172e980
Move all RMF tests to RemoteMessagingTests
ayoy Jul 1, 2024
70aa7b1
Clean up imports in tests
ayoy Jul 1, 2024
37f51ef
Add remoteMessaging feature flag
ayoy Jul 1, 2024
ccb40d0
Add privacyConfigurationManager to RMF store
ayoy Jul 1, 2024
df1d513
Add more mocks for Remote Messaging
ayoy Jul 2, 2024
3a9e5dd
Add PrivacyConfigurationRemoteMessagingAvailabilityProvider
ayoy Jul 2, 2024
1181fa6
Replace UserDefaults with KeyValueStore in RemoteMessagingPercentileS…
ayoy Jul 3, 2024
2d6d684
Add RemoteMessagingProcessingTests
ayoy Jul 3, 2024
c8778cb
Add feature flag tests to RemoteMessagingStore
ayoy Jul 3, 2024
fd1358c
Fix SwiftLint violations
ayoy Jul 3, 2024
687cdcc
Fix more SwiftLint violations
ayoy Jul 3, 2024
687bfba
Fix more SwiftLint violations
ayoy Jul 3, 2024
9644245
Add unit tests for numberOfBookmarks and numberOfFavorites
ayoy Jul 3, 2024
aadcd91
Revert a change to EmailUrls.Url.emailAlias
ayoy Jul 3, 2024
7fd8d07
AttributeMatcher -> AttributeMatching
ayoy Jul 3, 2024
8334115
Add documentation to RemoteMessagingAvailabilityProviding
ayoy Jul 3, 2024
99023c9
Remote commented out code
ayoy Jul 3, 2024
e7c7b09
Add documentation
ayoy Jul 3, 2024
7c39e61
Merge branch 'main' into dominik/rmf-macos
ayoy Jul 4, 2024
4d7daa3
Merge branch 'main' into dominik/rmf-macos
ayoy Jul 4, 2024
23b0dc1
EmailTestsUtils -> BrowserServicesKitTestsUtils
ayoy Jul 5, 2024
8e742e3
Add RMF availability flag publisher and delete scheduled messages whe…
ayoy Jul 7, 2024
4d111ca
Add documentation for isRemoteMessagingAvailablePublisher
ayoy Jul 7, 2024
324baf8
Drop first PrivacyConfigurationManager update in isRemoteMessagingAva…
ayoy Jul 7, 2024
1cc19c3
Rename configurationFetcher as configFetcher and make fetchAndProcess…
ayoy Jul 8, 2024
d27f306
Id -> ID in RemoteMessagingStoring APIs
ayoy Jul 8, 2024
2319ec8
Merge branch 'main' into dominik/rmf-macos
ayoy Jul 8, 2024
ed3cf77
Add DefaultRemoteMessagingSurveyURLBuilder (moved from client repos)
ayoy Jul 8, 2024
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
24 changes: 23 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ let package = Package(
.library(name: "Common", targets: ["Common"]),
.library(name: "TestUtils", targets: ["TestUtils"]),
.library(name: "DDGSync", targets: ["DDGSync"]),
.library(name: "BrowserServicesKitTestsUtils", targets: ["BrowserServicesKitTestsUtils"]),
.library(name: "Persistence", targets: ["Persistence"]),
.library(name: "Bookmarks", targets: ["Bookmarks"]),
.library(name: "BloomFilterWrapper", targets: ["BloomFilterWrapper"]),
Expand All @@ -26,6 +27,7 @@ let package = Package(
.library(name: "Configuration", targets: ["Configuration"]),
.library(name: "Networking", targets: ["Networking"]),
.library(name: "RemoteMessaging", targets: ["RemoteMessaging"]),
.library(name: "RemoteMessagingTestsUtils", targets: ["RemoteMessagingTestsUtils"]),
.library(name: "Navigation", targets: ["Navigation"]),
.library(name: "SyncDataProviders", targets: ["SyncDataProviders"]),
.library(name: "NetworkProtection", targets: ["NetworkProtection"]),
Expand Down Expand Up @@ -76,6 +78,12 @@ let package = Package(
.define("DEBUG", .when(configuration: .debug))
]
),
.target(
name: "BrowserServicesKitTestsUtils",
dependencies: [
"BrowserServicesKit",
]
),
.target(
name: "Persistence",
dependencies: [
Expand Down Expand Up @@ -261,9 +269,11 @@ let package = Package(
name: "RemoteMessaging",
dependencies: [
"Common",
"Configuration",
"BrowserServicesKit",
"Networking",
"Persistence",
"Subscription"
],
resources: [
.process("CoreData/RemoteMessaging.xcdatamodeld")
Expand All @@ -272,6 +282,12 @@ let package = Package(
.define("DEBUG", .when(configuration: .debug))
]
),
.target(
name: "RemoteMessagingTestsUtils",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

RemoteMessagingTestsUtils contains mocks for RemoteMessaging related protocols

dependencies: [
"RemoteMessaging",
]
),
.target(
name: "SyncDataProviders",
dependencies: [
Expand Down Expand Up @@ -400,7 +416,7 @@ let package = Package(
name: "BrowserServicesKitTests",
dependencies: [
"BrowserServicesKit",
"RemoteMessaging", // Move tests later (lots of test dependencies in BSK)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

RemoteMessaging has its own tests target now.

"BrowserServicesKitTestsUtils",
"SecureStorageTestsUtils",
"TestUtils",
"Subscription"
Expand Down Expand Up @@ -482,10 +498,16 @@ let package = Package(
.testTarget(
name: "RemoteMessagingTests",
dependencies: [
"BrowserServicesKitTestsUtils",
"RemoteMessaging",
"RemoteMessagingTestsUtils",
"TestUtils",
],
resources: [
.copy("Resources/remote-messaging-config-example.json"),
.copy("Resources/remote-messaging-config-malformed.json"),
.copy("Resources/remote-messaging-config-unsupported-items.json"),
.copy("Resources/remote-messaging-config.json"),
]
),
.testTarget(
Expand Down
25 changes: 25 additions & 0 deletions Sources/Bookmarks/BookmarkUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,31 @@ public struct BookmarkUtils {
return result.compactMap { $0[#keyPath(BookmarkEntity.title)] as? String }
}

public static func numberOfBookmarks(in context: NSManagedObjectContext) -> Int {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These 2 functions were used exclusively for RMF in the iOS repo. Now they're in BookmarkUtils and can be reused by macOS.

let request = BookmarkEntity.fetchRequest()
request.predicate = NSPredicate(
format: "%K == false AND %K == false AND (%K == NO OR %K == nil)",
#keyPath(BookmarkEntity.isFolder),
#keyPath(BookmarkEntity.isPendingDeletion),
#keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub))
return (try? context.count(for: request)) ?? 0
}

public static func numberOfFavorites(for displayMode: FavoritesDisplayMode, in context: NSManagedObjectContext) -> Int {
guard let displayedFavoritesFolder = BookmarkUtils.fetchFavoritesFolder(withUUID: displayMode.displayedFolder.rawValue, in: context) else {
return 0
}

let request = BookmarkEntity.fetchRequest()
request.predicate = NSPredicate(format: "%K CONTAINS %@ AND %K == false AND %K == false AND (%K == NO OR %K == nil)",
#keyPath(BookmarkEntity.favoriteFolders),
displayedFavoritesFolder,
#keyPath(BookmarkEntity.isFolder),
#keyPath(BookmarkEntity.isPendingDeletion),
#keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub))
return (try? context.count(for: request)) ?? 0
}

// MARK: Internal

@discardableResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public enum PrivacyFeature: String {
case sslCertificates
case brokenSiteReportExperiment
case toggleReports
case remoteMessaging
}

/// An abstraction to be implemented by any "subfeature" of a given `PrivacyConfiguration` feature.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// MockEmailManagerRequestDelegate.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.
//

@testable import BrowserServicesKit
import Foundation

public class MockEmailManagerRequestDelegate: EmailManagerRequestDelegate {

public init(didSendMockAliasRequest: @escaping () -> Void = {}) {
self.didSendMockAliasRequest = didSendMockAliasRequest
}

public var activeTask: URLSessionTask?
public var mockAliases: [String] = []
public var waitlistTimestamp: Int = 1
public var didSendMockAliasRequest: () -> Void

// swiftlint:disable function_parameter_count
public func emailManager(_ emailManager: EmailManager, requested url: URL, method: String, headers: [String: String], parameters: [String: String]?, httpBody: Data?, timeoutInterval: TimeInterval) async throws -> Data {
switch url.absoluteString {
case EmailUrls.Url.emailAlias: return try processMockAliasRequest().get()
default: fatalError("\(#file): Unsupported URL passed to mock request delegate: \(url)")
}
}
// swiftlint:enable function_parameter_count

public var keychainAccessErrorAccessType: EmailKeychainAccessType?
public var keychainAccessError: EmailKeychainAccessError?

public func emailManagerKeychainAccessFailed(_ emailManager: EmailManager,
accessType: EmailKeychainAccessType,
error: EmailKeychainAccessError) {
keychainAccessErrorAccessType = accessType
keychainAccessError = error
}

private func processMockAliasRequest() -> Result<Data, Error> {
didSendMockAliasRequest()

if mockAliases.first != nil {
let alias = mockAliases.removeFirst()
let jsonString = "{\"address\":\"\(alias)\"}"
let data = jsonString.data(using: .utf8)!
return .success(data)
} else {
return .failure(AliasRequestError.noDataError)
}
}

}
90 changes: 90 additions & 0 deletions Sources/BrowserServicesKitTestsUtils/MockEmailManagerStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// MockEmailManagerStorage.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 BrowserServicesKit
import Foundation

public class MockEmailManagerStorage: EmailManagerStorage {

public var mockError: EmailKeychainAccessError?

public var mockUsername: String?
public var mockToken: String?
public var mockAlias: String?
public var mockCohort: String?
public var mockLastUseDate: String?

public var storeTokenCallback: ((String, String, String?) -> Void)?
public var storeAliasCallback: ((String) -> Void)?
public var storeLastUseDateCallback: ((String) -> Void)?
public var deleteAliasCallback: (() -> Void)?
public var deleteAuthenticationStateCallback: (() -> Void)?
public var deleteWaitlistStateCallback: (() -> Void)?

public init() {}

public func getUsername() throws -> String? {
if let mockError = mockError { throw mockError }
return mockUsername
}

public func getToken() throws -> String? {
if let mockError = mockError { throw mockError }
return mockToken
}

public func getAlias() throws -> String? {
if let mockError = mockError { throw mockError }
return mockAlias
}

public func getCohort() throws -> String? {
if let mockError = mockError { throw mockError }
return mockCohort
}

public func getLastUseDate() throws -> String? {
if let mockError = mockError { throw mockError }
return mockLastUseDate
}

public func store(token: String, username: String, cohort: String?) throws {
storeTokenCallback?(token, username, cohort)
}

public func store(alias: String) throws {
storeAliasCallback?(alias)
}

public func store(lastUseDate: String) throws {
storeLastUseDateCallback?(lastUseDate)
}

public func deleteAlias() {
deleteAliasCallback?()
}

public func deleteAuthenticationState() {
deleteAuthenticationStateCallback?()
}

public func deleteWaitlistState() {
deleteWaitlistStateCallback?()
}

}
36 changes: 36 additions & 0 deletions Sources/BrowserServicesKitTestsUtils/MockStatisticsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// MockStatisticsStore.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 BrowserServicesKit
import Foundation

public class MockStatisticsStore: StatisticsStore {

public init() {}

public var installDate: Date?
public var atb: String?
public var searchRetentionAtb: String?
public var appRetentionAtb: String?

public var hasInstallStatistics: Bool {
return atb != nil
}

public var variant: String?
}
34 changes: 34 additions & 0 deletions Sources/BrowserServicesKitTestsUtils/MockVariant.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// MockVariant.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 BrowserServicesKit
import Foundation

public class MockVariant: Variant {
public var name: String
public var weight: Int
public var isIncluded: () -> Bool
public var features: [FeatureName]

public init(name: String, weight: Int, isIncluded: @escaping () -> Bool, features: [FeatureName]) {
self.name = name
self.weight = weight
self.isIncluded = isIncluded
self.features = features
}
}
Loading
Loading