From 2a3dc29c9f0a2d90465a75afe47083a78ecaafe8 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 14 Sep 2023 11:37:39 +0200 Subject: [PATCH] Switch Sync environment to production (#503) Task/Issue URL: https://app.asana.com/0/0/1205489036222324/f Description: This patch allows to control Sync server environment from client apps. * ServerEnvironment enum is added * DDGSync dependencies is now mutable * Endpoints is now a class (it's shared between AccountManager and SyncQueue so it's handy to have it passed around as a reference), can be initialized with ServerEnvironment and can have baseURL updated after creation * DDGSyncingDebuggingSupport and SyncDependenciesDebuggingSupport protocols were added to allow overriding server environment --- Sources/DDGSync/DDGSync.swift | 47 +++++++-------- Sources/DDGSync/DDGSyncing.swift | 48 +++++++++++++-- Sources/DDGSync/internal/Endpoints.swift | 59 +++++++++++++------ .../internal/ProductionDependencies.swift | 11 ++-- .../DDGSync/internal/SyncDependencies.swift | 6 +- Tests/DDGSyncTests/DDGSyncTests.swift | 18 +++--- Tests/DDGSyncTests/Mocks/Mocks.swift | 6 +- Tests/DDGSyncTests/SyncOperationTests.swift | 2 +- Tests/DDGSyncTests/SyncQueueTests.swift | 2 +- 9 files changed, 132 insertions(+), 67 deletions(-) diff --git a/Sources/DDGSync/DDGSync.swift b/Sources/DDGSync/DDGSync.swift index 9d1d76f30..b90276c23 100644 --- a/Sources/DDGSync/DDGSync.swift +++ b/Sources/DDGSync/DDGSync.swift @@ -26,12 +26,6 @@ public class DDGSync: DDGSyncing { public static let bundle = Bundle.module enum Constants { - // #if DEBUG - public static let baseUrl = URL(string: "https://dev-sync-use.duckduckgo.com")! - // #else - // public static let baseUrl = URL(string: "https://sync.duckduckgo.com")! - // #endif - public static let syncEnabledKey = "com.duckduckgo.sync.enabled" } @@ -59,8 +53,9 @@ public class DDGSync: DDGSyncing { /// This is the constructor intended for use by app clients. public convenience init(dataProvidersSource: DataProvidersSource, errorEvents: EventMapping, - log: @escaping @autoclosure () -> OSLog = .disabled) { - let dependencies = ProductionDependencies(baseUrl: Constants.baseUrl, errorEvents: errorEvents, log: log()) + log: @escaping @autoclosure () -> OSLog = .disabled, + environment: ServerEnvironment = .production) { + let dependencies = ProductionDependencies(serverEnvironment: environment, errorEvents: errorEvents, log: log()) self.init(dataProvidersSource: dataProvidersSource, dependencies: dependencies) } @@ -171,36 +166,36 @@ public class DDGSync: DDGSyncing { } } + public var serverEnvironment: ServerEnvironment { + if dependencies.endpoints.baseURL == ServerEnvironment.production.baseURL { + return .production + } + return .development + } + + public func updateServerEnvironment(_ serverEnvironment: ServerEnvironment) { + try? updateAccount(nil) + dependencies.updateServerEnvironment(serverEnvironment) + authState = .initializing + initializeIfNeeded() + } + // MARK: - - let dependencies: SyncDependencies + var dependencies: SyncDependencies init(dataProvidersSource: DataProvidersSource, dependencies: SyncDependencies) { self.dataProvidersSource = dataProvidersSource self.dependencies = dependencies } - public func initializeIfNeeded(isInternalUser: Bool) { + public func initializeIfNeeded() { guard authState == .initializing else { return } let syncEnabled = dependencies.keyValueStore.object(forKey: Constants.syncEnabledKey) != nil guard syncEnabled else { - // This is for initial tests only - if isInternalUser { - // Migrate and start using user defaults flag - do { - let account = try dependencies.secureStore.account() - authState = account?.state ?? .inactive - try updateAccount(account) - - } catch { - dependencies.errorEvents.fire(.failedToMigrate, error: error) - } - } else { - try? dependencies.secureStore.removeAccount() - authState = .inactive - } - + try? dependencies.secureStore.removeAccount() + authState = .inactive return } diff --git a/Sources/DDGSync/DDGSyncing.swift b/Sources/DDGSync/DDGSyncing.swift index ab6b60716..bd0f619e2 100644 --- a/Sources/DDGSync/DDGSyncing.swift +++ b/Sources/DDGSync/DDGSyncing.swift @@ -44,7 +44,7 @@ public protocol DataProvidersSource: AnyObject { func makeDataProviders() -> [DataProviding] } -public protocol DDGSyncing { +public protocol DDGSyncing: DDGSyncingDebuggingSupport { var dataProvidersSource: DataProvidersSource? { get set } @@ -87,7 +87,7 @@ public protocol DDGSyncing { /** Initializes Sync object, loads account info and prepares internal state. */ - func initializeIfNeeded(isInternalUser: Bool) + func initializeIfNeeded() /** Creates an account. @@ -111,7 +111,7 @@ public protocol DDGSyncing { func login(_ recoveryKey: SyncCode.RecoveryKey, deviceName: String, deviceType: String) async throws -> [RegisteredDevice] /** - Returns a device id and temporary secret key ready for display and allows callers attempt to fetch the transmitted recovery key. + Returns a device id and temporary secret key ready for display and allows callers attempt to fetch the transmitted recovery key. */ func remoteConnect() throws -> RemoteConnecting @@ -129,7 +129,7 @@ public protocol DDGSyncing { Disconnect the specified device from the sync service. - Parameter deviceId: ID of the device to be disconnected. - */ + */ func disconnect(deviceId: String) async throws /** @@ -138,7 +138,7 @@ public protocol DDGSyncing { func fetchDevices() async throws -> [RegisteredDevice] /** - Updated the device name. + Updated the device name. */ func updateDeviceName(_ name: String) async throws -> [RegisteredDevice] @@ -146,7 +146,45 @@ public protocol DDGSyncing { Deletes this account, but does not affect locally stored data. */ func deleteAccount() async throws +} + +public protocol DDGSyncingDebuggingSupport { + var serverEnvironment: ServerEnvironment { get } + func updateServerEnvironment(_ serverEnvironment: ServerEnvironment) +} +public enum ServerEnvironment: LosslessStringConvertible { + case development + case production + + var baseURL: URL { + switch self { + case .development: + return URL(string: "https://dev-sync-use.duckduckgo.com")! + case .production: + return URL(string: "https://sync.duckduckgo.com")! + } + } + + public var description: String { + switch self { + case .development: + return "Development" + case .production: + return "Production" + } + } + + public init?(_ description: String) { + switch description { + case "Development": + self = .development + case "Production": + self = .production + default: + return nil + } + } } public protocol Crypting { diff --git a/Sources/DDGSync/internal/Endpoints.swift b/Sources/DDGSync/internal/Endpoints.swift index 06cd6ad34..137506aa3 100644 --- a/Sources/DDGSync/internal/Endpoints.swift +++ b/Sources/DDGSync/internal/Endpoints.swift @@ -18,12 +18,17 @@ import Foundation -struct Endpoints { +class Endpoints { - let signup: URL - let login: URL - let logoutDevice: URL - let connect: URL + private(set) var baseURL: URL + + private(set) var signup: URL + private(set) var login: URL + private(set) var logoutDevice: URL + private(set) var connect: URL + + private(set) var syncGet: URL + private(set) var syncPatch: URL /// Constructs sync GET URL for specific data type(s), e.g. `sync/type1,type2,type3` func syncGet(features: [String]) throws -> URL { @@ -32,18 +37,36 @@ struct Endpoints { } return syncGet.appendingPathComponent(features.joined(separator: ",")) } - - let syncGet: URL - let syncPatch: URL - - init(baseUrl: URL) { - signup = baseUrl.appendingPathComponent("sync/signup") - login = baseUrl.appendingPathComponent("sync/login") - logoutDevice = baseUrl.appendingPathComponent("sync/logout-device") - connect = baseUrl.appendingPathComponent("sync/connect") - - syncGet = baseUrl.appendingPathComponent("sync") - syncPatch = baseUrl.appendingPathComponent("sync/data") + + convenience init(serverEnvironment: ServerEnvironment) { + self.init(baseURL: serverEnvironment.baseURL) } - + + init(baseURL: URL) { + self.baseURL = baseURL + signup = baseURL.appendingPathComponent("sync/signup") + login = baseURL.appendingPathComponent("sync/login") + logoutDevice = baseURL.appendingPathComponent("sync/logout-device") + connect = baseURL.appendingPathComponent("sync/connect") + + syncGet = baseURL.appendingPathComponent("sync") + syncPatch = baseURL.appendingPathComponent("sync/data") + } +} + +// MARK: - Debugging Support + +extension Endpoints { + + func updateBaseURL(for serverEnvironment: ServerEnvironment) { + baseURL = serverEnvironment.baseURL + signup = baseURL.appendingPathComponent("sync/signup") + login = baseURL.appendingPathComponent("sync/login") + logoutDevice = baseURL.appendingPathComponent("sync/logout-device") + connect = baseURL.appendingPathComponent("sync/connect") + + syncGet = baseURL.appendingPathComponent("sync") + syncPatch = baseURL.appendingPathComponent("sync/data") + } + } diff --git a/Sources/DDGSync/internal/ProductionDependencies.swift b/Sources/DDGSync/internal/ProductionDependencies.swift index d9ca10e65..06682ac8e 100644 --- a/Sources/DDGSync/internal/ProductionDependencies.swift +++ b/Sources/DDGSync/internal/ProductionDependencies.swift @@ -36,10 +36,10 @@ struct ProductionDependencies: SyncDependencies { } private let getLog: () -> OSLog - init(baseUrl: URL, errorEvents: EventMapping, log: @escaping @autoclosure () -> OSLog = .disabled) { + init(serverEnvironment: ServerEnvironment, errorEvents: EventMapping, log: @escaping @autoclosure () -> OSLog = .disabled) { self.init(fileStorageUrl: FileManager.default.applicationSupportDirectoryForComponent(named: "Sync"), - baseUrl: baseUrl, + serverEnvironment: serverEnvironment, keyValueStore: KeyValueStore(), secureStore: SecureStorage(), errorEvents: errorEvents, @@ -48,14 +48,14 @@ struct ProductionDependencies: SyncDependencies { init( fileStorageUrl: URL, - baseUrl: URL, + serverEnvironment: ServerEnvironment, keyValueStore: KeyValueStoring, secureStore: SecureStoring, errorEvents: EventMapping, log: @escaping @autoclosure () -> OSLog = .disabled ) { self.fileStorageUrl = fileStorageUrl - self.endpoints = Endpoints(baseUrl: baseUrl) + self.endpoints = Endpoints(serverEnvironment: serverEnvironment) self.keyValueStore = keyValueStore self.secureStore = secureStore self.errorEvents = errorEvents @@ -76,4 +76,7 @@ struct ProductionDependencies: SyncDependencies { return RecoveryKeyTransmitter(endpoints: endpoints, api: api, storage: secureStore, crypter: crypter) } + func updateServerEnvironment(_ serverEnvironment: ServerEnvironment) { + endpoints.updateBaseURL(for: serverEnvironment) + } } diff --git a/Sources/DDGSync/internal/SyncDependencies.swift b/Sources/DDGSync/internal/SyncDependencies.swift index c6f86d1fd..937614cb2 100644 --- a/Sources/DDGSync/internal/SyncDependencies.swift +++ b/Sources/DDGSync/internal/SyncDependencies.swift @@ -20,7 +20,11 @@ import Foundation import Combine import Common -protocol SyncDependencies { +protocol SyncDependenciesDebuggingSupport { + func updateServerEnvironment(_ serverEnvironment: ServerEnvironment) +} + +protocol SyncDependencies: SyncDependenciesDebuggingSupport { var endpoints: Endpoints { get } var account: AccountManaging { get } diff --git a/Tests/DDGSyncTests/DDGSyncTests.swift b/Tests/DDGSyncTests/DDGSyncTests.swift index acd490726..4e235811f 100644 --- a/Tests/DDGSyncTests/DDGSyncTests.swift +++ b/Tests/DDGSyncTests/DDGSyncTests.swift @@ -117,7 +117,7 @@ final class DDGSyncTests: XCTestCase { dataProvidersSource.dataProviders = [dataProvider] let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) - syncService.initializeIfNeeded(isInternalUser: false) + syncService.initializeIfNeeded() bindInProgressPublisher(for: syncService) syncService.scheduler.requestSyncImmediately() @@ -155,7 +155,7 @@ final class DDGSyncTests: XCTestCase { dataProvidersSource.dataProviders = [dataProvider] let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) - syncService.initializeIfNeeded(isInternalUser: false) + syncService.initializeIfNeeded() bindInProgressPublisher(for: syncService) syncService.scheduler.requestSyncImmediately() @@ -200,7 +200,7 @@ final class DDGSyncTests: XCTestCase { dataProvidersSource.dataProviders = [bookmarksDataProvider, credentialsDataProvider] let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) - syncService.initializeIfNeeded(isInternalUser: false) + syncService.initializeIfNeeded() bindInProgressPublisher(for: syncService) syncService.scheduler.requestSyncImmediately() @@ -235,7 +235,7 @@ final class DDGSyncTests: XCTestCase { dataProvidersSource.dataProviders = [bookmarksDataProvider, credentialsDataProvider] let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) - syncService.initializeIfNeeded(isInternalUser: false) + syncService.initializeIfNeeded() bindInProgressPublisher(for: syncService) syncService.scheduler.requestSyncImmediately() @@ -282,7 +282,7 @@ final class DDGSyncTests: XCTestCase { dataProvidersSource.dataProviders = [bookmarksDataProvider, credentialsDataProvider] let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) - syncService.initializeIfNeeded(isInternalUser: false) + syncService.initializeIfNeeded() bindInProgressPublisher(for: syncService) syncService.scheduler.requestSyncImmediately() @@ -316,7 +316,7 @@ final class DDGSyncTests: XCTestCase { dataProvidersSource.dataProviders = [dataProvider] let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) - syncService.initializeIfNeeded(isInternalUser: false) + syncService.initializeIfNeeded() isInProgressCancellable = syncService.isSyncInProgressPublisher.sink { [weak syncService] isInProgress in if isInProgress { @@ -364,7 +364,7 @@ final class DDGSyncTests: XCTestCase { dataProvidersSource.dataProviders = [dataProvider] let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) - syncService.initializeIfNeeded(isInternalUser: false) + syncService.initializeIfNeeded() bindInProgressPublisher(for: syncService) syncService.scheduler.cancelSyncAndSuspendSyncQueue() @@ -390,7 +390,7 @@ final class DDGSyncTests: XCTestCase { dataProvidersSource.dataProviders = [dataProvider] let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) - syncService.initializeIfNeeded(isInternalUser: false) + syncService.initializeIfNeeded() bindInProgressPublisher(for: syncService) syncService.scheduler.cancelSyncAndSuspendSyncQueue() @@ -420,7 +420,7 @@ final class DDGSyncTests: XCTestCase { dataProvidersSource.dataProviders = [dataProvider] let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) - syncService.initializeIfNeeded(isInternalUser: false) + syncService.initializeIfNeeded() bindInProgressPublisher(for: syncService) syncService.scheduler.requestSyncImmediately() diff --git a/Tests/DDGSyncTests/Mocks/Mocks.swift b/Tests/DDGSyncTests/Mocks/Mocks.swift index 22c11f55b..13c470312 100644 --- a/Tests/DDGSyncTests/Mocks/Mocks.swift +++ b/Tests/DDGSyncTests/Mocks/Mocks.swift @@ -139,8 +139,8 @@ class MockKeyValueStore: KeyValueStoring { } } -struct MockSyncDepenencies: SyncDependencies { - var endpoints: Endpoints = Endpoints(baseUrl: URL(string: "https://dev.null")!) +struct MockSyncDepenencies: SyncDependencies, SyncDependenciesDebuggingSupport { + var endpoints: Endpoints = Endpoints(baseURL: URL(string: "https://dev.null")!) var account: AccountManaging = AccountManagingMock() var api: RemoteAPIRequestCreating = RemoteAPIRequestCreatingMock() var secureStore: SecureStoring = SecureStorageStub() @@ -163,6 +163,8 @@ struct MockSyncDepenencies: SyncDependencies { func createRecoveryKeyTransmitter() throws -> RecoveryKeyTransmitting { RecoveryKeyTransmitter(endpoints: endpoints, api: api, storage: secureStore, crypter: crypter) } + + func updateServerEnvironment(_ serverEnvironment: ServerEnvironment) {} } final class MockDataProvidersSource: DataProvidersSource { diff --git a/Tests/DDGSyncTests/SyncOperationTests.swift b/Tests/DDGSyncTests/SyncOperationTests.swift index 04b51f61c..37a94ac4e 100644 --- a/Tests/DDGSyncTests/SyncOperationTests.swift +++ b/Tests/DDGSyncTests/SyncOperationTests.swift @@ -34,7 +34,7 @@ class SyncOperationTests: XCTestCase { apiMock = RemoteAPIRequestCreatingMock() request = HTTPRequestingMock() apiMock.request = request - endpoints = Endpoints(baseUrl: URL(string: "https://example.com")!) + endpoints = Endpoints(baseURL: URL(string: "https://example.com")!) storage = SecureStorageStub() crypter = CryptingMock() try storage.persistAccount( diff --git a/Tests/DDGSyncTests/SyncQueueTests.swift b/Tests/DDGSyncTests/SyncQueueTests.swift index f0a1777f1..107b998da 100644 --- a/Tests/DDGSyncTests/SyncQueueTests.swift +++ b/Tests/DDGSyncTests/SyncQueueTests.swift @@ -33,7 +33,7 @@ class SyncQueueTests: XCTestCase { apiMock = RemoteAPIRequestCreatingMock() request = HTTPRequestingMock() apiMock.request = request - endpoints = Endpoints(baseUrl: URL(string: "https://example.com")!) + endpoints = Endpoints(baseURL: URL(string: "https://example.com")!) storage = SecureStorageStub() crypter = CryptingMock() try storage.persistAccount(