diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 67f1a296e7..bd36aa9e47 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -1786,6 +1786,9 @@ D9AE0ADB29188A170063488B /* LegacyMessageDecryptJobRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9AE0ADA29188A170063488B /* LegacyMessageDecryptJobRecord.swift */; }; D9AE0ADD2918B2960063488B /* JobRecord+Columns.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9AE0ADC2918B2960063488B /* JobRecord+Columns.swift */; }; D9B0AC7429EF42960070F31C /* TSInfoMessage+DisplayableGroupUpdateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B0AC7329EF42960070F31C /* TSInfoMessage+DisplayableGroupUpdateItem.swift */; }; + D9B6FD752BF2B94A00BB7DF1 /* account-data.jsonproto in Resources */ = {isa = PBXBuildFile; fileRef = D9B6FD742BF2B94A00BB7DF1 /* account-data.jsonproto */; }; + D9B6FD7A2BF40A7200BB7DF1 /* unregistered-contact.jsonproto in Resources */ = {isa = PBXBuildFile; fileRef = D9B6FD782BF40A7200BB7DF1 /* unregistered-contact.jsonproto */; }; + D9B6FD852BF53FCF00BB7DF1 /* registered-blocked-contact.jsonproto in Resources */ = {isa = PBXBuildFile; fileRef = D9B6FD7C2BF53C0100BB7DF1 /* registered-blocked-contact.jsonproto */; }; D9B8541229137C150058F97B /* JobRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B8541129137C150058F97B /* JobRecord.swift */; }; D9B95A9629E6830B00D7CB95 /* JobRecordTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B95A9429E682E900D7CB95 /* JobRecordTest.swift */; }; D9B95A9829E8906200D7CB95 /* OWSDeviceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B95A9729E8906200D7CB95 /* OWSDeviceTest.swift */; }; @@ -1814,6 +1817,8 @@ D9C964092BE44D700058F143 /* XCTest+Thenable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C964072BE44D510058F143 /* XCTest+Thenable.swift */; }; D9C964102BE451CE0058F143 /* TSMessageStorageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9640F2BE451CE0058F143 /* TSMessageStorageTest.swift */; }; D9C964142BE45A030058F143 /* SignedPreKeyDeletionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C964132BE45A030058F143 /* SignedPreKeyDeletionTests.swift */; }; + D9C964172BE56DFB0058F143 /* MessageBackupIntegrationTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C964162BE56DFB0058F143 /* MessageBackupIntegrationTestCase.swift */; }; + D9C964192BE59E270058F143 /* MessageBackupIntegrationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C964182BE59E270058F143 /* MessageBackupIntegrationTest.swift */; }; D9CA5BF729B3F61E00D9AAD1 /* LegacyChangePhoneNumber+ChangeTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9CA5BF629B3F61E00D9AAD1 /* LegacyChangePhoneNumber+ChangeTokens.swift */; }; D9CA8AB02B698DFF00787167 /* DeletedCallRecordCleanupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9CA8AAF2B698DFF00787167 /* DeletedCallRecordCleanupManager.swift */; }; D9CA8AB32B6ACC0600787167 /* DeletedCallRecordCleanupManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9CA8AB22B6ACC0600787167 /* DeletedCallRecordCleanupManagerTest.swift */; }; @@ -4691,6 +4696,9 @@ D9AE0ADA29188A170063488B /* LegacyMessageDecryptJobRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyMessageDecryptJobRecord.swift; sourceTree = ""; }; D9AE0ADC2918B2960063488B /* JobRecord+Columns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JobRecord+Columns.swift"; sourceTree = ""; }; D9B0AC7329EF42960070F31C /* TSInfoMessage+DisplayableGroupUpdateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+DisplayableGroupUpdateItem.swift"; sourceTree = ""; }; + D9B6FD742BF2B94A00BB7DF1 /* account-data.jsonproto */ = {isa = PBXFileReference; lastKnownFileType = text; path = "account-data.jsonproto"; sourceTree = ""; }; + D9B6FD782BF40A7200BB7DF1 /* unregistered-contact.jsonproto */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "unregistered-contact.jsonproto"; sourceTree = ""; }; + D9B6FD7C2BF53C0100BB7DF1 /* registered-blocked-contact.jsonproto */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "registered-blocked-contact.jsonproto"; sourceTree = ""; }; D9B8541129137C150058F97B /* JobRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRecord.swift; sourceTree = ""; }; D9B91D8D2B17E2A600BCB11A /* GroupCallRecordRingUpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallRecordRingUpdateDelegate.swift; sourceTree = ""; }; D9B95A9429E682E900D7CB95 /* JobRecordTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRecordTest.swift; sourceTree = ""; }; @@ -4721,6 +4729,8 @@ D9C964072BE44D510058F143 /* XCTest+Thenable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTest+Thenable.swift"; sourceTree = ""; }; D9C9640F2BE451CE0058F143 /* TSMessageStorageTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSMessageStorageTest.swift; sourceTree = ""; }; D9C964132BE45A030058F143 /* SignedPreKeyDeletionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedPreKeyDeletionTests.swift; sourceTree = ""; }; + D9C964162BE56DFB0058F143 /* MessageBackupIntegrationTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupIntegrationTestCase.swift; sourceTree = ""; }; + D9C964182BE59E270058F143 /* MessageBackupIntegrationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBackupIntegrationTest.swift; sourceTree = ""; }; D9CA5BF629B3F61E00D9AAD1 /* LegacyChangePhoneNumber+ChangeTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LegacyChangePhoneNumber+ChangeTokens.swift"; sourceTree = ""; }; D9CA8AAF2B698DFF00787167 /* DeletedCallRecordCleanupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedCallRecordCleanupManager.swift; sourceTree = ""; }; D9CA8AB22B6ACC0600787167 /* DeletedCallRecordCleanupManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedCallRecordCleanupManagerTest.swift; sourceTree = ""; }; @@ -9575,6 +9585,16 @@ path = Individual; sourceTree = ""; }; + D9B6FD732BF2B92000BB7DF1 /* TestCases */ = { + isa = PBXGroup; + children = ( + D9B6FD742BF2B94A00BB7DF1 /* account-data.jsonproto */, + D9B6FD7C2BF53C0100BB7DF1 /* registered-blocked-contact.jsonproto */, + D9B6FD782BF40A7200BB7DF1 /* unregistered-contact.jsonproto */, + ); + path = TestCases; + sourceTree = ""; + }; D9B95A9329E682CA00D7CB95 /* JobRecords */ = { isa = PBXGroup; children = ( @@ -9604,6 +9624,16 @@ path = ChangePhoneNumber; sourceTree = ""; }; + D9C964152BE56DE20058F143 /* MessageBackup */ = { + isa = PBXGroup; + children = ( + D9B6FD732BF2B92000BB7DF1 /* TestCases */, + D9C964182BE59E270058F143 /* MessageBackupIntegrationTest.swift */, + D9C964162BE56DFB0058F143 /* MessageBackupIntegrationTestCase.swift */, + ); + path = MessageBackup; + sourceTree = ""; + }; D9CA8AAD2B698B2400787167 /* DeletedCallRecord */ = { isa = PBXGroup; children = ( @@ -10202,6 +10232,7 @@ 5075C21529CA1ED500A260D2 /* GroupMembers */, F94261E2289B1B5400460798 /* Groups */, 5000CA2F2B1F97DC00BB8EFF /* Jobs */, + D9C964152BE56DE20058F143 /* MessageBackup */, F942621C289B1B5500460798 /* Messages */, D9F399B72A9EA1EB001599EC /* Mocks */, F94261CF289B1B5400460798 /* Network */, @@ -11988,10 +12019,13 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D9B6FD752BF2B94A00BB7DF1 /* account-data.jsonproto in Resources */, + D9B6FD852BF53FCF00BB7DF1 /* registered-blocked-contact.jsonproto in Resources */, F942628B289B1B5600460798 /* sample-sticker.encrypted in Resources */, F942628C289B1B5600460798 /* sample-sticker.webp in Resources */, F908AA7D28CE629700472E68 /* test-apng.png in Resources */, F927478828CFE9B10056EAFE /* test-png.png in Resources */, + D9B6FD7A2BF40A7200BB7DF1 /* unregistered-contact.jsonproto in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -14594,6 +14628,8 @@ D938307C2A704338006CDCDE /* LocalUsernameManagerTests.swift in Sources */, F942625F289B1B5500460798 /* LRUCacheTest.swift in Sources */, F9426269289B1B5500460798 /* MathOWSTests.swift in Sources */, + D9C964192BE59E270058F143 /* MessageBackupIntegrationTest.swift in Sources */, + D9C964172BE56DFB0058F143 /* MessageBackupIntegrationTestCase.swift in Sources */, 66FC637229DF7A1500F00DAC /* MessageBodyRangesTests.swift in Sources */, 668444822A3292AB00DBED7C /* MessageBodyStyleTests.swift in Sources */, 66883A3A29D7630A00E898CF /* MessageBodyTests.swift in Sources */, diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.swift b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.swift index cc683fd079..b227fe2e79 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.swift +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.swift @@ -446,7 +446,7 @@ class DebugUIMisc: NSObject, DebugUIPage, Dependencies { updateStorageService: false, /* storage service updated below */ transaction: transaction ) - StoryManager.setHasSetMyStoriesPrivacy(false, transaction: transaction, shouldUpdateStorageService: true) + StoryManager.setHasSetMyStoriesPrivacy(false, shouldUpdateStorageService: true, transaction: transaction) } } diff --git a/Signal/test/PerformanceTests/PerformanceBaseTest.swift b/Signal/test/PerformanceTests/PerformanceBaseTest.swift index 611baae65b..11391c6b96 100644 --- a/Signal/test/PerformanceTests/PerformanceBaseTest.swift +++ b/Signal/test/PerformanceTests/PerformanceBaseTest.swift @@ -12,7 +12,6 @@ public class PerformanceBaseTest: XCTestCase { // MARK: Hooks func setUpIteration() { - SetCurrentAppContext(TestAppContext()) MockSSKEnvironment.activate() } diff --git a/Signal/test/SignalBaseTest.swift b/Signal/test/SignalBaseTest.swift index 38158ebf97..bf516aa579 100644 --- a/Signal/test/SignalBaseTest.swift +++ b/Signal/test/SignalBaseTest.swift @@ -11,8 +11,6 @@ open class SignalBaseTest: XCTestCase { public override func setUp() { super.setUp() - - SetCurrentAppContext(TestAppContext()) MockSSKEnvironment.activate() } diff --git a/Signal/test/util/GRDBFinderTest.swift b/Signal/test/util/GRDBFinderTest.swift index 6ff359dfa5..4bdab7deb0 100644 --- a/Signal/test/util/GRDBFinderTest.swift +++ b/Signal/test/util/GRDBFinderTest.swift @@ -185,7 +185,11 @@ class GRDBFinderTest: SignalBaseTest { var expectedAddresses = Set() self.write { tx in let buildUserProfile = { () -> OWSUserProfile in - return OWSUserProfile.getOrBuildUserProfile(for: .otherUser(Aci.randomForTesting()), tx: tx) + return OWSUserProfile.getOrBuildUserProfile( + for: .otherUser(Aci.randomForTesting()), + userProfileWriter: .tests, + tx: tx + ) } func updateUserProfile( diff --git a/SignalServiceKit/Contacts/Threads/TSPrivateStoryThread.m b/SignalServiceKit/Contacts/Threads/TSPrivateStoryThread.m index 95b40df0ad..9fdcb07dca 100644 --- a/SignalServiceKit/Contacts/Threads/TSPrivateStoryThread.m +++ b/SignalServiceKit/Contacts/Threads/TSPrivateStoryThread.m @@ -138,8 +138,8 @@ lastVisibleSortIdOnScreenPercentageObsolete:lastVisibleSortIdOnScreenPercentageO { if ([self isMyStory]) { [StoryManager setHasSetMyStoriesPrivacy:YES - transaction:transaction - shouldUpdateStorageService:updateStorageService]; + shouldUpdateStorageService:updateStorageService + transaction:transaction]; } [self anyUpdatePrivateStoryThreadWithTransaction:transaction block:^(TSPrivateStoryThread *thread) { diff --git a/SignalServiceKit/Environment/AppSetup.swift b/SignalServiceKit/Environment/AppSetup.swift index ab4c7fa08d..6ae3cc0191 100644 --- a/SignalServiceKit/Environment/AppSetup.swift +++ b/SignalServiceKit/Environment/AppSetup.swift @@ -10,28 +10,85 @@ import SignalCoreKit public class AppSetup { public init() {} + /// Injectable mocks for global singletons accessed by tests for components + /// that cannot be isolated in tests. + /// + /// For example, many legacy tests rely on the globally-available singletons + /// from ``Dependencies`` and its ``NSObject`` extension, and use this type + /// to inject mock versions of those singletons to the global state. + /// + /// Additionally, the integration tests for message backup access these + /// globals transitively through message backup's dependency on managers + /// that use the global state, and similarly use this type to inject a + /// limited set of mock singletons. public struct TestDependencies { - let accountServiceClient: AccountServiceClient - let contactManager: any ContactManager - let groupV2Updates: any GroupV2Updates - let groupsV2: any GroupsV2 - let keyValueStoreFactory: any KeyValueStoreFactory - let messageSender: MessageSender - let modelReadCaches: ModelReadCaches - let networkManager: NetworkManager - let paymentsCurrencies: any PaymentsCurrenciesSwift - let paymentsHelper: any PaymentsHelperSwift - let pendingReceiptRecorder: any PendingReceiptRecorder - let profileManager: any ProfileManager - let reachabilityManager: any SSKReachabilityManager - let remoteConfigManager: any RemoteConfigManager - let signalService: any OWSSignalServiceProtocol - let storageServiceManager: any StorageServiceManager - let subscriptionManager: any SubscriptionManager - let syncManager: any SyncManagerProtocol - let systemStoryManager: any SystemStoryManagerProtocol - let versionedProfiles: any VersionedProfilesSwift - let webSocketFactory: any WebSocketFactory + let accountServiceClient: AccountServiceClient? + let contactManager: (any ContactManager)? + let groupV2Updates: (any GroupV2Updates)? + let groupsV2: (any GroupsV2)? + let keyValueStoreFactory: (any KeyValueStoreFactory)? + let messageSender: MessageSender? + let modelReadCaches: ModelReadCaches? + let networkManager: NetworkManager? + let paymentsCurrencies: (any PaymentsCurrenciesSwift)? + let paymentsHelper: (any PaymentsHelperSwift)? + let pendingReceiptRecorder: (any PendingReceiptRecorder)? + let profileManager: (any ProfileManager)? + let reachabilityManager: (any SSKReachabilityManager)? + let remoteConfigManager: (any RemoteConfigManager)? + let signalService: (any OWSSignalServiceProtocol)? + let storageServiceManager: (any StorageServiceManager)? + let subscriptionManager: (any SubscriptionManager)? + let syncManager: (any SyncManagerProtocol)? + let systemStoryManager: (any SystemStoryManagerProtocol)? + let versionedProfiles: (any VersionedProfilesSwift)? + let webSocketFactory: (any WebSocketFactory)? + + public init( + accountServiceClient: AccountServiceClient? = nil, + contactManager: (any ContactManager)? = nil, + groupV2Updates: (any GroupV2Updates)? = nil, + groupsV2: (any GroupsV2)? = nil, + keyValueStoreFactory: (any KeyValueStoreFactory)? = nil, + messageSender: MessageSender? = nil, + modelReadCaches: ModelReadCaches? = nil, + networkManager: NetworkManager? = nil, + paymentsCurrencies: (any PaymentsCurrenciesSwift)? = nil, + paymentsHelper: (any PaymentsHelperSwift)? = nil, + pendingReceiptRecorder: (any PendingReceiptRecorder)? = nil, + profileManager: (any ProfileManager)? = nil, + reachabilityManager: (any SSKReachabilityManager)? = nil, + remoteConfigManager: (any RemoteConfigManager)? = nil, + signalService: (any OWSSignalServiceProtocol)? = nil, + storageServiceManager: (any StorageServiceManager)? = nil, + subscriptionManager: (any SubscriptionManager)? = nil, + syncManager: (any SyncManagerProtocol)? = nil, + systemStoryManager: (any SystemStoryManagerProtocol)? = nil, + versionedProfiles: (any VersionedProfilesSwift)? = nil, + webSocketFactory: (any WebSocketFactory)? = nil + ) { + self.accountServiceClient = accountServiceClient + self.contactManager = contactManager + self.groupV2Updates = groupV2Updates + self.groupsV2 = groupsV2 + self.keyValueStoreFactory = keyValueStoreFactory + self.messageSender = messageSender + self.modelReadCaches = modelReadCaches + self.networkManager = networkManager + self.paymentsCurrencies = paymentsCurrencies + self.paymentsHelper = paymentsHelper + self.pendingReceiptRecorder = pendingReceiptRecorder + self.profileManager = profileManager + self.reachabilityManager = reachabilityManager + self.remoteConfigManager = remoteConfigManager + self.signalService = signalService + self.storageServiceManager = storageServiceManager + self.subscriptionManager = subscriptionManager + self.syncManager = syncManager + self.systemStoryManager = systemStoryManager + self.versionedProfiles = versionedProfiles + self.webSocketFactory = webSocketFactory + } } public func start( @@ -42,7 +99,7 @@ public class AppSetup { callMessageHandler: CallMessageHandler, currentCallThreadProvider: any CurrentCallThreadProvider, notificationPresenter: any NotificationPresenter, - testDependencies: TestDependencies? = nil + testDependencies: TestDependencies = TestDependencies() ) -> AppSetup.DatabaseContinuation { configureUnsatisfiableConstraintLogging() @@ -58,7 +115,7 @@ public class AppSetup { OWSBackgroundTaskManager.shared().observeNotifications() let appVersion = AppVersionImpl.shared - let webSocketFactory = testDependencies?.webSocketFactory ?? WebSocketFactoryNative() + let webSocketFactory = testDependencies.webSocketFactory ?? WebSocketFactoryNative() // AFNetworking (via CFNetworking) spools its attachments in // NSTemporaryDirectory(). If you receive a media message while the device @@ -69,13 +126,13 @@ public class AppSetup { owsAssert(OWSFileSystem.protectFileOrFolder(atPath: temporaryDirectory, fileProtectionType: .completeUntilFirstUserAuthentication)) let tsConstants = TSConstants.shared - let keyValueStoreFactory = testDependencies?.keyValueStoreFactory ?? SDSKeyValueStoreFactory() + let keyValueStoreFactory = testDependencies.keyValueStoreFactory ?? SDSKeyValueStoreFactory() let recipientDatabaseTable = RecipientDatabaseTableImpl() let recipientFetcher = RecipientFetcherImpl(recipientDatabaseTable: recipientDatabaseTable) let recipientIdFinder = RecipientIdFinder(recipientDatabaseTable: recipientDatabaseTable, recipientFetcher: recipientFetcher) - let accountServiceClient = testDependencies?.accountServiceClient ?? AccountServiceClient() + let accountServiceClient = testDependencies.accountServiceClient ?? AccountServiceClient() let aciSignalProtocolStore = SignalProtocolStoreImpl( for: .aci, keyValueStoreFactory: keyValueStoreFactory, @@ -85,34 +142,34 @@ public class AppSetup { let dateProvider = Date.provider let earlyMessageManager = EarlyMessageManager() let messageProcessor = MessageProcessor() - let messageSender = testDependencies?.messageSender ?? MessageSender() + let messageSender = testDependencies.messageSender ?? MessageSender() let messageSenderJobQueue = MessageSenderJobQueue() - let modelReadCaches = testDependencies?.modelReadCaches ?? ModelReadCaches(factory: ModelReadCacheFactory()) - let networkManager = testDependencies?.networkManager ?? NetworkManager() + let modelReadCaches = testDependencies.modelReadCaches ?? ModelReadCaches(factory: ModelReadCacheFactory()) + let networkManager = testDependencies.networkManager ?? NetworkManager() let ows2FAManager = OWS2FAManager() - let paymentsHelper = testDependencies?.paymentsHelper ?? PaymentsHelperImpl() + let paymentsHelper = testDependencies.paymentsHelper ?? PaymentsHelperImpl() let pniSignalProtocolStore = SignalProtocolStoreImpl( for: .pni, keyValueStoreFactory: keyValueStoreFactory, recipientIdFinder: recipientIdFinder ) - let profileManager = testDependencies?.profileManager ?? OWSProfileManager( + let profileManager = testDependencies.profileManager ?? OWSProfileManager( databaseStorage: databaseStorage, swiftValues: OWSProfileManagerSwiftValues() ) - let reachabilityManager = testDependencies?.reachabilityManager ?? SSKReachabilityManagerImpl() + let reachabilityManager = testDependencies.reachabilityManager ?? SSKReachabilityManagerImpl() let receiptManager = OWSReceiptManager() let senderKeyStore = SenderKeyStore() let signalProtocolStoreManager = SignalProtocolStoreManagerImpl( aciProtocolStore: aciSignalProtocolStore, pniProtocolStore: pniSignalProtocolStore ) - let signalService = testDependencies?.signalService ?? OWSSignalService() + let signalService = testDependencies.signalService ?? OWSSignalService() let signalServiceAddressCache = SignalServiceAddressCache() - let storageServiceManager = testDependencies?.storageServiceManager ?? StorageServiceManagerImpl.shared - let syncManager = testDependencies?.syncManager ?? OWSSyncManager(default: ()) + let storageServiceManager = testDependencies.storageServiceManager ?? StorageServiceManagerImpl.shared + let syncManager = testDependencies.syncManager ?? OWSSyncManager(default: ()) let udManager = OWSUDManagerImpl() - let versionedProfiles = testDependencies?.versionedProfiles ?? VersionedProfilesImpl() + let versionedProfiles = testDependencies.versionedProfiles ?? VersionedProfilesImpl() let signalAccountStore = SignalAccountStoreImpl() let threadStore = ThreadStoreImpl() @@ -141,7 +198,7 @@ public class AppSetup { storageServiceManager: storageServiceManager, schedulers: schedulers ) - let contactManager = testDependencies?.contactManager ?? OWSContactsManager(swiftValues: OWSContactsManagerSwiftValues( + let contactManager = testDependencies.contactManager ?? OWSContactsManager(swiftValues: OWSContactsManagerSwiftValues( usernameLookupManager: usernameLookupManager, recipientDatabaseTable: recipientDatabaseTable, nicknameManager: nicknameManager @@ -158,7 +215,7 @@ public class AppSetup { db: db ) - let groupsV2 = testDependencies?.groupsV2 ?? GroupsV2Impl( + let groupsV2 = testDependencies.groupsV2 ?? GroupsV2Impl( authCredentialStore: authCredentialStore, authCredentialManager: authCredentialManager ) @@ -664,8 +721,8 @@ public class AppSetup { let messageBackupKeyMaterial = MessageBackupKeyMaterialImpl(svr: svr) let preferences = Preferences() let storyStore = StoryStoreImpl() - let subscriptionManager = testDependencies?.subscriptionManager ?? SubscriptionManagerImpl() - let systemStoryManager = testDependencies?.systemStoryManager ?? SystemStoryManager() + let subscriptionManager = testDependencies.subscriptionManager ?? SubscriptionManagerImpl() + let systemStoryManager = testDependencies.systemStoryManager ?? SystemStoryManager() let typingIndicators = TypingIndicatorsImpl() let attachmentUploadManager = AttachmentUploadManagerImpl( @@ -687,6 +744,7 @@ public class AppSetup { localUsernameManager: localUsernameManager, phoneNumberDiscoverabilityManager: phoneNumberDiscoverabilityManager, preferences: MessageBackup.AccountData.Wrappers.Preferences(preferences: preferences), + profileManager: MessageBackup.Wrappers.ProfileManager(profileManager), receiptManager: MessageBackup.AccountData.Wrappers.ReceiptManager(receiptManager: receiptManager), reactionManager: MessageBackup.AccountData.Wrappers.ReactionManager(), sskPreferences: MessageBackup.AccountData.Wrappers.SSKPreferences(), @@ -695,8 +753,7 @@ public class AppSetup { systemStoryManager: MessageBackup.AccountData.Wrappers.SystemStoryManager(systemStoryManager: systemStoryManager), typingIndicators: MessageBackup.AccountData.Wrappers.TypingIndicators(typingIndicators: typingIndicators), udManager: MessageBackup.AccountData.Wrappers.UDManager(udManager: udManager), - usernameEducationManager: usernameEducationManager, - userProfile: MessageBackup.AccountData.Wrappers.UserProfile() + usernameEducationManager: usernameEducationManager ), attachmentDownloadManager: attachmentDownloadManager, attachmentUploadManager: attachmentUploadManager, @@ -919,9 +976,9 @@ public class AppSetup { reachabilityManager: reachabilityManager ) - let pendingReceiptRecorder = testDependencies?.pendingReceiptRecorder ?? MessageRequestPendingReceipts() + let pendingReceiptRecorder = testDependencies.pendingReceiptRecorder ?? MessageRequestPendingReceipts() let messageReceiver = MessageReceiver(callMessageHandler: callMessageHandler) - let remoteConfigManager = testDependencies?.remoteConfigManager ?? RemoteConfigManagerImpl( + let remoteConfigManager = testDependencies.remoteConfigManager ?? RemoteConfigManagerImpl( appExpiry: appExpiry, db: db, keyValueStoreFactory: keyValueStoreFactory, @@ -937,7 +994,7 @@ public class AppSetup { ) let stickerManager = StickerManager() let sskPreferences = SSKPreferences() - let groupV2Updates = testDependencies?.groupV2Updates ?? GroupV2UpdatesImpl() + let groupV2Updates = testDependencies.groupV2Updates ?? GroupV2UpdatesImpl() let messageFetcherJob = MessageFetcherJob() let profileFetcher = ProfileFetcherImpl( db: db, @@ -953,7 +1010,7 @@ public class AppSetup { versionedProfiles: versionedProfiles ) let messagePipelineSupervisor = MessagePipelineSupervisor() - let paymentsCurrencies = testDependencies?.paymentsCurrencies ?? PaymentsCurrenciesImpl() + let paymentsCurrencies = testDependencies.paymentsCurrencies ?? PaymentsCurrenciesImpl() let spamChallengeResolver = SpamChallengeResolver() let phoneNumberUtil = PhoneNumberUtil(swiftValues: PhoneNumberUtilSwiftValues()) let legacyChangePhoneNumber = LegacyChangePhoneNumber() diff --git a/SignalServiceKit/MessageBackup/Archivers/AccountData/MessageBackupAccountDataArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/AccountData/MessageBackupAccountDataArchiver.swift index 2aa203e3da..a11f47bab1 100644 --- a/SignalServiceKit/MessageBackup/Archivers/AccountData/MessageBackupAccountDataArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/AccountData/MessageBackupAccountDataArchiver.swift @@ -3,6 +3,8 @@ // SPDX-License-Identifier: AGPL-3.0-only // +import SignalCoreKit + extension MessageBackup { /// An identifier for the ``BackupProto.AccountData`` backup frame. /// @@ -48,6 +50,7 @@ public class MessageBackupAccountDataArchiverImpl: MessageBackupAccountDataArchi private let localUsernameManager: LocalUsernameManager private let phoneNumberDiscoverabilityManager: PhoneNumberDiscoverabilityManager private let preferences: MessageBackup.AccountData.Shims.Preferences + private let profileManager: MessageBackup.Shims.ProfileManager private let receiptManager: MessageBackup.AccountData.Shims.ReceiptManager private let reactionManager: MessageBackup.AccountData.Shims.ReactionManager private let sskPreferences: MessageBackup.AccountData.Shims.SSKPreferences @@ -57,13 +60,13 @@ public class MessageBackupAccountDataArchiverImpl: MessageBackupAccountDataArchi private let typingIndicators: MessageBackup.AccountData.Shims.TypingIndicators private let udManager: MessageBackup.AccountData.Shims.UDManager private let usernameEducationManager: UsernameEducationManager - private let userProfile: MessageBackup.AccountData.Shims.UserProfile public init( disappearingMessageConfigurationStore: DisappearingMessagesConfigurationStore, localUsernameManager: LocalUsernameManager, phoneNumberDiscoverabilityManager: PhoneNumberDiscoverabilityManager, preferences: MessageBackup.AccountData.Shims.Preferences, + profileManager: MessageBackup.Shims.ProfileManager, receiptManager: MessageBackup.AccountData.Shims.ReceiptManager, reactionManager: MessageBackup.AccountData.Shims.ReactionManager, sskPreferences: MessageBackup.AccountData.Shims.SSKPreferences, @@ -72,8 +75,7 @@ public class MessageBackupAccountDataArchiverImpl: MessageBackupAccountDataArchi systemStoryManager: MessageBackup.AccountData.Shims.SystemStoryManager, typingIndicators: MessageBackup.AccountData.Shims.TypingIndicators, udManager: MessageBackup.AccountData.Shims.UDManager, - usernameEducationManager: UsernameEducationManager, - userProfile: MessageBackup.AccountData.Shims.UserProfile + usernameEducationManager: UsernameEducationManager ) { self.disappearingMessageConfigurationStore = disappearingMessageConfigurationStore self.localUsernameManager = localUsernameManager @@ -88,7 +90,7 @@ public class MessageBackupAccountDataArchiverImpl: MessageBackupAccountDataArchi self.typingIndicators = typingIndicators self.udManager = udManager self.usernameEducationManager = usernameEducationManager - self.userProfile = userProfile + self.profileManager = profileManager } public func archiveAccountData( @@ -96,7 +98,7 @@ public class MessageBackupAccountDataArchiverImpl: MessageBackupAccountDataArchi tx: DBReadTransaction ) -> MessageBackup.ArchiveAccountDataResult { - guard let localProfile = userProfile.getLocalProfile(tx: tx) else { + guard let localProfile = profileManager.getUserProfileForLocalUser(tx: tx) else { return .failure(.archiveFrameError(.missingLocalProfile, .localUser)) } guard let profileKeyData = localProfile.profileKey?.keyData else { @@ -224,7 +226,6 @@ public class MessageBackupAccountDataArchiverImpl: MessageBackupAccountDataArchi _ accountData: BackupProto.AccountData, tx: DBWriteTransaction ) -> RestoreFrameResult { - guard let profileKey = OWSAES256Key(data: accountData.profileKey) else { return .failure([.restoreFrameError( .invalidProtoData(.invalidLocalProfileKey), @@ -232,6 +233,16 @@ public class MessageBackupAccountDataArchiverImpl: MessageBackupAccountDataArchi )]) } + // Given name and profile key are required for the local profile. The + // rest are optional. + profileManager.insertLocalUserProfile( + givenName: accountData.givenName, + familyName: accountData.familyName.nilIfEmpty, + avatarUrlPath: accountData.avatarUrlPath.nilIfEmpty, + profileKey: profileKey, + tx: tx + ) + // Restore Subscription data, if nod a default value if accountData.subscriberId.count > 0, accountData.subscriberCurrencyCode.count > 0 { subscriptionManager.setSubscriberID(subscriberID: accountData.subscriberId, tx: tx) @@ -288,16 +299,6 @@ public class MessageBackupAccountDataArchiverImpl: MessageBackupAccountDataArchi udManager.setPhoneNumberSharingMode(phoneNumberSharingMode, tx: tx) } - // Restore Local Profile - // For familyName & avatarUrlPath, pass in `nil` if the value is empty. - userProfile.insertLocalProfile( - givenName: accountData.givenName, - familyName: accountData.familyName.nilIfEmpty, - avatarUrlPath: accountData.avatarUrlPath.nilIfEmpty, - profileKey: profileKey, - tx: tx - ) - // Restore username details (username, link, QR color) if let username = accountData.username, let usernameLink = accountData.usernameLink { if diff --git a/SignalServiceKit/MessageBackup/Archivers/Recipient/MessageBackupContactRecipientArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/Recipient/MessageBackupContactRecipientArchiver.swift index f699f803d5..a4014743f7 100644 --- a/SignalServiceKit/MessageBackup/Archivers/Recipient/MessageBackupContactRecipientArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/Recipient/MessageBackupContactRecipientArchiver.swift @@ -51,7 +51,7 @@ public class MessageBackupContactRecipientArchiver: MessageBackupRecipientDestin context: MessageBackup.RecipientArchivingContext, tx: DBReadTransaction ) -> ArchiveMultiFrameResult { - let whitelistedAddresses = Set(profileManager.allWhitelistedRegisteredAddresses(tx: tx)) + let whitelistedAddresses = Set(profileManager.allWhitelistedAddresses(tx: tx)) let blockedAddresses = blockingManager.blockedAddresses(tx: tx) var errors = [ArchiveMultiFrameResult.ArchiveFrameError]() @@ -102,7 +102,10 @@ public class MessageBackupContactRecipientArchiver: MessageBackupRecipientDestin contact.registered = recipient.isRegistered ? .REGISTERED : .NOT_REGISTERED contact.aci = recipient.aci.map(\.rawUUID.data) contact.pni = recipient.pni.map(\.rawUUID.data) - contact.e164 = recipient.address.e164.map(\.uint64Value) + contact.e164 = { () -> UInt64? in + guard let phoneNumberString = recipient.phoneNumber?.stringValue else { return nil } + return E164(phoneNumberString)?.uint64Value + }() if let aci = recipient.aci { contact.username = usernameLookupManager.fetchUsername( @@ -272,7 +275,7 @@ public class MessageBackupContactRecipientArchiver: MessageBackupRecipientDestin // We only need to active hide, since unhidden is the default. if contactProto.hideStory, let aci = address.aci { let storyContext = storyStore.getOrCreateStoryContextAssociatedData(for: aci, tx: tx) - storyStore.updateStoryContext(storyContext, isHidden: true, tx: tx) + storyStore.updateStoryContext(storyContext, updateStorageService: false, isHidden: true, tx: tx) } profileManager.insertOtherUserProfile( diff --git a/SignalServiceKit/MessageBackup/MessageBackupManager+Shims.swift b/SignalServiceKit/MessageBackup/MessageBackupManager+Shims.swift index e3d89fcc5b..08c3c71015 100644 --- a/SignalServiceKit/MessageBackup/MessageBackupManager+Shims.swift +++ b/SignalServiceKit/MessageBackup/MessageBackupManager+Shims.swift @@ -43,7 +43,7 @@ public class _MessageBackup_BlockingManagerWrapper: _MessageBackup_BlockingManag } public func addBlockedAddress(_ address: SignalServiceAddress, tx: DBWriteTransaction) { - blockingManager.addBlockedAddress(address, blockMode: .localShouldNotLeaveGroups, transaction: SDSDB.shimOnlyBridge(tx)) + blockingManager.addBlockedAddress(address, blockMode: .restoreFromBackup, transaction: SDSDB.shimOnlyBridge(tx)) } } @@ -53,11 +53,11 @@ public protocol _MessageBackup_ProfileManagerShim { func getUserProfile(for address: SignalServiceAddress, tx: DBReadTransaction) -> OWSUserProfile? - func getLocalUsersProfile(tx: DBReadTransaction) -> OWSUserProfile? + func getUserProfileForLocalUser(tx: DBReadTransaction) -> OWSUserProfile? func getProfileKeyData(for address: SignalServiceAddress, tx: DBReadTransaction) -> Data? - func allWhitelistedRegisteredAddresses(tx: DBReadTransaction) -> [SignalServiceAddress] + func allWhitelistedAddresses(tx: DBReadTransaction) -> [SignalServiceAddress] func isThread(inProfileWhitelist thread: TSThread, tx: DBReadTransaction) -> Bool @@ -72,6 +72,14 @@ public protocol _MessageBackup_ProfileManagerShim { tx: DBWriteTransaction ) + func insertLocalUserProfile( + givenName: String, + familyName: String?, + avatarUrlPath: String?, + profileKey: OWSAES256Key, + tx: DBWriteTransaction + ) + func insertOtherUserProfile( givenName: String?, familyName: String?, @@ -82,11 +90,6 @@ public protocol _MessageBackup_ProfileManagerShim { } public class _MessageBackup_ProfileManagerWrapper: _MessageBackup_ProfileManagerShim { - private var userProfileWriter: UserProfileWriter { - // [Backups] TODO: add a dedicated profile writer case - return .storageService - } - private let profileManager: ProfileManager public init(_ profileManager: ProfileManager) { @@ -97,7 +100,7 @@ public class _MessageBackup_ProfileManagerWrapper: _MessageBackup_ProfileManager profileManager.getUserProfile(for: address, transaction: SDSDB.shimOnlyBridge(tx)) } - public func getLocalUsersProfile(tx: DBReadTransaction) -> OWSUserProfile? { + public func getUserProfileForLocalUser(tx: any DBReadTransaction) -> OWSUserProfile? { return OWSUserProfile.getUserProfileForLocalUser(tx: SDSDB.shimOnlyBridge(tx)) } @@ -105,8 +108,8 @@ public class _MessageBackup_ProfileManagerWrapper: _MessageBackup_ProfileManager profileManager.profileKeyData(for: address, transaction: SDSDB.shimOnlyBridge(tx)) } - public func allWhitelistedRegisteredAddresses(tx: DBReadTransaction) -> [SignalServiceAddress] { - profileManager.allWhitelistedRegisteredAddresses(tx: SDSDB.shimOnlyBridge(tx)) + public func allWhitelistedAddresses(tx: any DBReadTransaction) -> [SignalServiceAddress] { + profileManager.allWhitelistedAddresses(tx: SDSDB.shimOnlyBridge(tx)) } public func isThread(inProfileWhitelist thread: TSThread, tx: DBReadTransaction) -> Bool { @@ -116,7 +119,7 @@ public class _MessageBackup_ProfileManagerWrapper: _MessageBackup_ProfileManager public func addToWhitelist(_ address: SignalServiceAddress, tx: DBWriteTransaction) { profileManager.addUser( toProfileWhitelist: address, - userProfileWriter: userProfileWriter, + userProfileWriter: .messageBackupRestore, transaction: SDSDB.shimOnlyBridge(tx) ) } @@ -136,13 +139,38 @@ public class _MessageBackup_ProfileManagerWrapper: _MessageBackup_ProfileManager for: aci, onlyFillInIfMissing: false, shouldFetchProfile: false, - userProfileWriter: userProfileWriter, + userProfileWriter: .messageBackupRestore, localIdentifiers: localIdentifiers, authedAccount: .implicit(), tx: tx ) } + public func insertLocalUserProfile( + givenName: String, + familyName: String?, + avatarUrlPath: String?, + profileKey: OWSAES256Key, + tx: DBWriteTransaction + ) { + let sdsTx = SDSDB.shimOnlyBridge(tx) + + let localUserProfile = OWSUserProfile.getOrBuildUserProfileForLocalUser( + userProfileWriter: .messageBackupRestore, + tx: sdsTx + ) + + localUserProfile.update( + givenName: .setTo(givenName), + familyName: .setTo(familyName), + avatarUrlPath: .setTo(avatarUrlPath), + profileKey: .setTo(profileKey), + userProfileWriter: .messageBackupRestore, + transaction: sdsTx, + completion: nil + ) + } + public func insertOtherUserProfile( givenName: String?, familyName: String?, @@ -162,6 +190,8 @@ public class _MessageBackup_ProfileManagerWrapper: _MessageBackup_ProfileManager } } +// MARK: - AccountData + extension MessageBackup { public enum AccountData {} } @@ -177,7 +207,6 @@ extension MessageBackup.AccountData { public typealias SystemStoryManager = _MessageBackup_AccountData_SystemStoryManagerShim public typealias ReactionManager = _MessageBackup_AccountData_ReactionManagerShim public typealias UDManager = _MessageBackup_AccountData_UDManagerShim - public typealias UserProfile = _MessageBackup_AccountData_UserProfileShim } public enum Wrappers { @@ -190,7 +219,6 @@ extension MessageBackup.AccountData { public typealias SystemStoryManager = _MessageBackup_AccountData_SystemStoryManagerWrapper public typealias ReactionManager = _MessageBackup_AccountData_ReactionManagerWrapper public typealias UDManager = _MessageBackup_AccountData_UDManagerWrapper - public typealias UserProfile = _MessageBackup_AccountData_UserProfileWrapper } } @@ -257,7 +285,7 @@ public class _MessageBackup_AccountData_PreferencesWrapper: _MessageBackup_Accou } } -// MARK: - SSKPreferences +// MARK: SSKPreferences public protocol _MessageBackup_AccountData_SSKPreferencesShim { func areLinkPreviewsEnabled(tx: DBReadTransaction) -> Bool @@ -268,8 +296,8 @@ public protocol _MessageBackup_AccountData_SSKPreferencesShim { func shouldKeepMutedChatsArchived(tx: DBReadTransaction) -> Bool func setShouldKeepMutedChatsArchived(value: Bool, tx: DBWriteTransaction) - } + public class _MessageBackup_AccountData_SSKPreferencesWrapper: _MessageBackup_AccountData_SSKPreferencesShim { public func areLinkPreviewsEnabled(tx: DBReadTransaction) -> Bool { SSKPreferences.areLinkPreviewsEnabled(transaction: SDSDB.shimOnlyBridge(tx)) @@ -293,7 +321,7 @@ public class _MessageBackup_AccountData_SSKPreferencesWrapper: _MessageBackup_Ac } } -// MARK: - SubscriptionManager +// MARK: SubscriptionManager public protocol _MessageBackup_AccountData_SubscriptionManagerShim { func displayBadgesOnProfile(tx: DBReadTransaction) -> Bool @@ -305,6 +333,7 @@ public protocol _MessageBackup_AccountData_SubscriptionManagerShim { func userManuallyCancelledSubscription(tx: DBReadTransaction) -> Bool func setUserManuallyCancelledSubscription(value: Bool, tx: DBWriteTransaction) } + public class _MessageBackup_AccountData_SubscriptionManagerWrapper: _MessageBackup_AccountData_SubscriptionManagerShim { let subscriptionManager: SubscriptionManager init(subscriptionManager: SubscriptionManager) { @@ -342,7 +371,7 @@ public class _MessageBackup_AccountData_SubscriptionManagerWrapper: _MessageBack } } -// MARK: - StoryManager +// MARK: StoryManager public protocol _MessageBackup_AccountData_StoryManagerShim { func hasSetMyStoriesPrivacy(tx: DBReadTransaction) -> Bool @@ -352,28 +381,29 @@ public protocol _MessageBackup_AccountData_StoryManagerShim { func areViewReceiptsEnabled(tx: DBReadTransaction) -> Bool func setAreViewReceiptsEnabled(value: Bool, tx: DBWriteTransaction) } + public class _MessageBackup_AccountData_StoryManagerWrapper: _MessageBackup_AccountData_StoryManagerShim { public func hasSetMyStoriesPrivacy(tx: DBReadTransaction) -> Bool { StoryManager.hasSetMyStoriesPrivacy(transaction: SDSDB.shimOnlyBridge(tx)) } public func setHasSetMyStoriesPrivacy(value: Bool, tx: DBWriteTransaction) { - StoryManager.setHasSetMyStoriesPrivacy(value, transaction: SDSDB.shimOnlyBridge(tx), shouldUpdateStorageService: false) + StoryManager.setHasSetMyStoriesPrivacy(value, shouldUpdateStorageService: false, transaction: SDSDB.shimOnlyBridge(tx)) } public func areStoriesEnabled(tx: DBReadTransaction) -> Bool { StoryManager.areStoriesEnabled(transaction: SDSDB.shimOnlyBridge(tx)) } public func setAreStoriesEnabled(value: Bool, tx: DBWriteTransaction) { - StoryManager.setAreStoriesEnabled(value, transaction: SDSDB.shimOnlyBridge(tx)) + StoryManager.setAreStoriesEnabled(value, shouldUpdateStorageService: false, transaction: SDSDB.shimOnlyBridge(tx)) } public func areViewReceiptsEnabled(tx: DBReadTransaction) -> Bool { StoryManager.areViewReceiptsEnabled(transaction: SDSDB.shimOnlyBridge(tx)) } public func setAreViewReceiptsEnabled(value: Bool, tx: DBWriteTransaction) { - StoryManager.setAreViewReceiptsEnabled(value, transaction: SDSDB.shimOnlyBridge(tx)) + StoryManager.setAreViewReceiptsEnabled(value, shouldUpdateStorageService: false, transaction: SDSDB.shimOnlyBridge(tx)) } } -// MARK: - SystemStoryManager +// MARK: SystemStoryManager public protocol _MessageBackup_AccountData_SystemStoryManagerShim { func isOnboardingStoryViewed(tx: DBReadTransaction) -> Bool @@ -381,6 +411,7 @@ public protocol _MessageBackup_AccountData_SystemStoryManagerShim { func hasSeenGroupStoryEducationSheet(tx: DBReadTransaction) -> Bool func setHasSeenGroupStoryEducationSheet(value: Bool, tx: DBWriteTransaction) } + public class _MessageBackup_AccountData_SystemStoryManagerWrapper: _MessageBackup_AccountData_SystemStoryManagerShim { let systemStoryManager: SystemStoryManagerProtocol init(systemStoryManager: SystemStoryManagerProtocol) { @@ -404,12 +435,13 @@ public class _MessageBackup_AccountData_SystemStoryManagerWrapper: _MessageBacku } } -// MARK: - ReactionManager +// MARK: ReactionManager public protocol _MessageBackup_AccountData_ReactionManagerShim { func customEmojiSet(tx: DBReadTransaction) -> [String]? func setCustomEmojiSet(emojis: [String]?, tx: DBWriteTransaction) } + public class _MessageBackup_AccountData_ReactionManagerWrapper: _MessageBackup_AccountData_ReactionManagerShim { public func customEmojiSet(tx: DBReadTransaction) -> [String]? { ReactionManager.customEmojiSet(transaction: SDSDB.shimOnlyBridge(tx)) @@ -419,12 +451,13 @@ public class _MessageBackup_AccountData_ReactionManagerWrapper: _MessageBackup_A } } -// MARK: - UDManager +// MARK: UDManager public protocol _MessageBackup_AccountData_UDManagerShim { func phoneNumberSharingMode(tx: DBReadTransaction) -> PhoneNumberSharingMode? func setPhoneNumberSharingMode(_ mode: PhoneNumberSharingMode, tx: DBWriteTransaction) } + public class _MessageBackup_AccountData_UDManagerWrapper: _MessageBackup_AccountData_UDManagerShim { let udManager: OWSUDManager init(udManager: OWSUDManager) { @@ -437,37 +470,3 @@ public class _MessageBackup_AccountData_UDManagerWrapper: _MessageBackup_Account udManager.setPhoneNumberSharingMode(mode, updateStorageServiceAndProfile: false, tx: SDSDB.shimOnlyBridge(tx)) } } - -// MARK: - UserProfile - -public protocol _MessageBackup_AccountData_UserProfileShim { - func getLocalProfile(tx: DBReadTransaction) -> OWSUserProfile? - func insertLocalProfile( - givenName: String, - familyName: String?, - avatarUrlPath: String?, - profileKey: OWSAES256Key, - tx: DBWriteTransaction - ) -} -public class _MessageBackup_AccountData_UserProfileWrapper: _MessageBackup_AccountData_UserProfileShim { - public func getLocalProfile(tx: DBReadTransaction) -> OWSUserProfile? { - return OWSUserProfile.getUserProfileForLocalUser(tx: SDSDB.shimOnlyBridge(tx)) - } - - public func insertLocalProfile( - givenName: String, - familyName: String?, - avatarUrlPath: String?, - profileKey: OWSAES256Key, - tx: DBWriteTransaction - ) { - OWSUserProfile( - address: .localUser, - givenName: givenName, - familyName: familyName, - profileKey: profileKey, - avatarUrlPath: avatarUrlPath - ).anyInsert(transaction: SDSDB.shimOnlyBridge(tx)) - } -} diff --git a/SignalServiceKit/Messages/BlockingManager.swift b/SignalServiceKit/Messages/BlockingManager.swift index 1903895a78..33b3b1e347 100644 --- a/SignalServiceKit/Messages/BlockingManager.swift +++ b/SignalServiceKit/Messages/BlockingManager.swift @@ -9,16 +9,15 @@ import SignalCoreKit public enum BlockMode: UInt { case remote + case restoreFromBackup case localShouldLeaveGroups case localShouldNotLeaveGroups var locallyInitiated: Bool { switch self { - case .remote: + case .remote, .restoreFromBackup: return false - case .localShouldLeaveGroups: - return true - case .localShouldNotLeaveGroups: + case .localShouldLeaveGroups, .localShouldNotLeaveGroups: return true } } diff --git a/SignalServiceKit/Messages/Stories/StoryManager.swift b/SignalServiceKit/Messages/Stories/StoryManager.swift index c859d3ed37..36e802461f 100644 --- a/SignalServiceKit/Messages/Stories/StoryManager.swift +++ b/SignalServiceKit/Messages/Stories/StoryManager.swift @@ -213,9 +213,9 @@ public class StoryManager: NSObject { @objc public class func setHasSetMyStoriesPrivacy( - _ hasSet: Bool = true, - transaction: SDSAnyWriteTransaction, - shouldUpdateStorageService: Bool = true + _ hasSet: Bool, + shouldUpdateStorageService: Bool, + transaction: SDSAnyWriteTransaction ) { guard hasSet != hasSetMyStoriesPrivacy(transaction: transaction) else { // Don't trigger account record updates unneccesarily! diff --git a/SignalServiceKit/Messages/Stories/SystemStoryManager.swift b/SignalServiceKit/Messages/Stories/SystemStoryManager.swift index a0e3e5d427..4b767bf525 100644 --- a/SignalServiceKit/Messages/Stories/SystemStoryManager.swift +++ b/SignalServiceKit/Messages/Stories/SystemStoryManager.swift @@ -162,16 +162,16 @@ public class SystemStoryManager: NSObject, Dependencies, SystemStoryManagerProto public func isOnboardingOverlayViewed(transaction: SDSAnyReadTransaction) -> Bool { if overlayKvStore.getBool(Constants.kvStoreOnboardingOverlayViewedKey, defaultValue: false, transaction: transaction) { - return false + return true } if isOnboardingStoryViewed(transaction: transaction) { // We don't sync view state for the onboarding overlay. But we can use // viewing of the onboarding story as an imperfect proxy; if they viewed it // that means they also definitely saw the viewer overlay. - return false + return true } - return true + return false } public func setOnboardingOverlayViewed(value: Bool, transaction: SDSAnyWriteTransaction) { diff --git a/SignalServiceKit/Profiles/OWSProfileManager.m b/SignalServiceKit/Profiles/OWSProfileManager.m index 89268e5091..5728f5bc90 100644 --- a/SignalServiceKit/Profiles/OWSProfileManager.m +++ b/SignalServiceKit/Profiles/OWSProfileManager.m @@ -177,7 +177,9 @@ NSString *const kNSNotificationKey_UserProfileWriter = @"kNSNotificationKey_User } DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) { - localUserProfile = [OWSUserProfile getOrBuildUserProfileForLocalUserWithTx:transaction]; + localUserProfile = + [OWSUserProfile getOrBuildUserProfileForLocalUserWithUserProfileWriter:UserProfileWriter_LocalUser + tx:transaction]; OWSAssertDebug(localUserProfile.profileKey); }); @@ -416,8 +418,9 @@ NSString *const kNSNotificationKey_UserProfileWriter = @"kNSNotificationKey_User // by the time this method is called. If it's not, we've changed our caching // logic and should re-evaluate this method. OWSFailDebug(@"Missing local profile when setting key."); - - localUserProfile = [OWSUserProfile getOrBuildUserProfileForLocalUserWithTx:transaction]; + localUserProfile = + [OWSUserProfile getOrBuildUserProfileForLocalUserWithUserProfileWriter:UserProfileWriter_LocalUser + tx:transaction]; _localUserProfile = localUserProfile; } else { @@ -931,11 +934,6 @@ NSString *const kNSNotificationKey_UserProfileWriter = @"kNSNotificationKey_User return userProfile.avatarUrlPath; } -- (NSArray *)allWhitelistedRegisteredAddressesWithTx:(SDSAnyReadTransaction *)tx -{ - return [self objc_allWhitelistedRegisteredAddressesWithTx:tx]; -} - - (nullable NSString *)profileBioForDisplayForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction { diff --git a/SignalServiceKit/Profiles/OWSProfileManager.swift b/SignalServiceKit/Profiles/OWSProfileManager.swift index a3b842ba5f..0875e29323 100644 --- a/SignalServiceKit/Profiles/OWSProfileManager.swift +++ b/SignalServiceKit/Profiles/OWSProfileManager.swift @@ -10,8 +10,7 @@ import SignalCoreKit public class OWSProfileManagerSwiftValues { fileprivate let pendingUpdateRequests = AtomicValue<[OWSProfileManager.ProfileUpdateRequest]>([], lock: .init()) - public init() { - } + public init() {} } extension OWSProfileManager: ProfileManager, Dependencies { @@ -38,7 +37,11 @@ extension OWSProfileManager: ProfileManager, Dependencies { ) { AssertNotOnMainThread() - let userProfile = OWSUserProfile.getOrBuildUserProfile(for: address, tx: tx) + let userProfile = OWSUserProfile.getOrBuildUserProfile( + for: address, + userProfileWriter: userProfileWriter, + tx: tx + ) var givenNameChange: OptionalChange = .noChange var familyNameChange: OptionalChange = .noChange @@ -164,9 +167,9 @@ extension OWSProfileManager: ProfileManager, Dependencies { ) } - @objc - @available(swift, obsoleted: 1.0) - func objc_allWhitelistedRegisteredAddresses(tx: SDSAnyReadTransaction) -> [SignalServiceAddress] { + // MARK: - + + public func allWhitelistedAddresses(tx: SDSAnyReadTransaction) -> [SignalServiceAddress] { var addresses = Set() for serviceIdString in whitelistedServiceIdsStore.allKeys(transaction: tx) { addresses.insert(SignalServiceAddress(serviceIdString: serviceIdString)) @@ -175,12 +178,20 @@ extension OWSProfileManager: ProfileManager, Dependencies { addresses.insert(SignalServiceAddress.legacyAddress(serviceId: nil, phoneNumber: phoneNumber)) } - return Array( - SignalRecipientFinder().signalRecipients(for: Array(addresses), tx: tx) - .lazy.filter { $0.isRegistered }.map { $0.address } - ) + return Array(addresses) } + public func allWhitelistedRegisteredAddresses(tx: SDSAnyReadTransaction) -> [SignalServiceAddress] { + return SignalRecipientFinder().signalRecipients( + for: allWhitelistedAddresses(tx: tx), + tx: tx + ) + .lazy + .filter { $0.isRegistered }.map { $0.address } + } + + // MARK: - + @objc internal func rotateLocalProfileKeyIfNecessary() { DispatchQueue.global().async { @@ -618,7 +629,11 @@ extension OWSProfileManager: ProfileManager, Dependencies { return } - let userProfile = OWSUserProfile.getOrBuildUserProfile(for: address, tx: SDSDB.shimOnlyBridge(tx)) + let userProfile = OWSUserProfile.getOrBuildUserProfile( + for: address, + userProfileWriter: userProfileWriter, + tx: SDSDB.shimOnlyBridge(tx) + ) if onlyFillInIfMissing, userProfile.profileKey != nil { return @@ -982,7 +997,11 @@ extension OWSProfileManager: ProfileManager, Dependencies { await databaseStorage.awaitableWrite { tx in self.tryToDequeueProfileChanges(profileChanges, tx: tx) // Apply the changes to our local profile. - let userProfile = OWSUserProfile.getOrBuildUserProfile(for: .localUser, tx: tx) + let userProfile = OWSUserProfile.getOrBuildUserProfile( + for: .localUser, + userProfileWriter: .localUser, + tx: tx + ) userProfile.update( givenName: .setTo(newGivenName?.stringValue.rawValue), familyName: .setTo(newFamilyName?.stringValue.rawValue), @@ -1214,6 +1233,8 @@ extension OWSProfileManager: ProfileManager, Dependencies { localIdentifiers: LocalIdentifiers, tx: DBWriteTransaction ) { + let userProfileWriter: UserProfileWriter = .metadataUpdate + let address = OWSUserProfile.insertableAddress(for: serviceId, localIdentifiers: localIdentifiers) switch address { case .localUser: @@ -1221,7 +1242,11 @@ extension OWSProfileManager: ProfileManager, Dependencies { case .otherUser: break } - let userProfile = OWSUserProfile.getOrBuildUserProfile(for: address, tx: SDSDB.shimOnlyBridge(tx)) + let userProfile = OWSUserProfile.getOrBuildUserProfile( + for: address, + userProfileWriter: userProfileWriter, + tx: SDSDB.shimOnlyBridge(tx) + ) // lastMessagingDate is coarse; we don't need to track every single message // sent or received. It is sufficient to update it only when the value @@ -1232,7 +1257,7 @@ extension OWSProfileManager: ProfileManager, Dependencies { userProfile.update( lastMessagingDate: .setTo(Date()), - userProfileWriter: .metadataUpdate, + userProfileWriter: userProfileWriter, transaction: SDSDB.shimOnlyBridge(tx), completion: nil ) diff --git a/SignalServiceKit/Protocols/ProfileManager.swift b/SignalServiceKit/Protocols/ProfileManager.swift index 71c6cbdf40..13c04a5425 100644 --- a/SignalServiceKit/Protocols/ProfileManager.swift +++ b/SignalServiceKit/Protocols/ProfileManager.swift @@ -79,11 +79,23 @@ public enum OptionalAvatarChange: Equatable { } public protocol ProfileManager: ProfileManagerProtocol { + + // MARK: - + func fetchLocalUsersProfile(authedAccount: AuthedAccount) -> Promise func fetchUserProfiles(for addresses: [SignalServiceAddress], tx: SDSAnyReadTransaction) -> [OWSUserProfile?] + func reuploadLocalProfile( + unsavedRotatedProfileKey: OWSAES256Key?, + mustReuploadAvatar: Bool, + authedAccount: AuthedAccount, + tx: DBWriteTransaction + ) -> Promise + func downloadAndDecryptLocalUserAvatarIfNeeded(authedAccount: AuthedAccount) async throws + // MARK: - + /// Downloads & decrypts the avatar at a particular URL. /// /// While this method de-dupes in-flight requests, it won't de-dupe requests @@ -119,13 +131,6 @@ public protocol ProfileManager: ProfileManagerProtocol { tx: SDSAnyWriteTransaction ) -> Promise - func reuploadLocalProfile( - unsavedRotatedProfileKey: OWSAES256Key?, - mustReuploadAvatar: Bool, - authedAccount: AuthedAccount, - tx: DBWriteTransaction - ) -> Promise - func didSendOrReceiveMessage( serviceId: ServiceId, localIdentifiers: LocalIdentifiers, @@ -150,4 +155,9 @@ public protocol ProfileManager: ProfileManagerProtocol { localIdentifiers: LocalIdentifiers, tx: DBWriteTransaction ) + + // MARK: - + + func allWhitelistedAddresses(tx: SDSAnyReadTransaction) -> [SignalServiceAddress] + func allWhitelistedRegisteredAddresses(tx: SDSAnyReadTransaction) -> [SignalServiceAddress] } diff --git a/SignalServiceKit/Protocols/ProfileManagerProtocol.h b/SignalServiceKit/Protocols/ProfileManagerProtocol.h index 2a212401fe..15f6cbcd0b 100644 --- a/SignalServiceKit/Protocols/ProfileManagerProtocol.h +++ b/SignalServiceKit/Protocols/ProfileManagerProtocol.h @@ -115,9 +115,6 @@ typedef NS_ENUM(NSUInteger, UserProfileWriter) { - (nullable ModelReadCacheSizeLease *)leaseCacheSize:(NSInteger)size; -- (NSArray *)allWhitelistedRegisteredAddressesWithTx:(SDSAnyReadTransaction *)tx - NS_SWIFT_NAME(allWhitelistedRegisteredAddresses(tx:)); - /** * Rotates the local profile key. Intended specifically for the * use case of recipient hiding. diff --git a/SignalServiceKit/StorageService/StorageServiceProto+Sync.swift b/SignalServiceKit/StorageService/StorageServiceProto+Sync.swift index e2425933af..0018d68d3d 100644 --- a/SignalServiceKit/StorageService/StorageServiceProto+Sync.swift +++ b/SignalServiceKit/StorageService/StorageServiceProto+Sync.swift @@ -525,6 +525,7 @@ class StorageServiceContactRecordUpdater: StorageServiceRecordUpdater { ) let localUserProfile = OWSUserProfile.getOrBuildUserProfile( for: profileAddress, + userProfileWriter: .storageService, tx: SDSDB.shimOnlyBridge(tx) ) localUserProfile.update( @@ -1351,7 +1352,10 @@ class StorageServiceAccountRecordUpdater: StorageServiceRecordUpdater { || localUserProfile?.familyName != normalizedRemoteFamilyName || localAvatarUrl != record.avatarURL ) { - let localUserProfile = OWSUserProfile.getOrBuildUserProfileForLocalUser(tx: transaction) + let localUserProfile = OWSUserProfile.getOrBuildUserProfileForLocalUser( + userProfileWriter: .storageService, + tx: transaction + ) localUserProfile.update( givenName: .setTo(normalizedRemoteGivenName), familyName: .setTo(normalizedRemoteFamilyName), @@ -1555,7 +1559,7 @@ class StorageServiceAccountRecordUpdater: StorageServiceRecordUpdater { let localHasSetMyStoriesPrivacy = StoryManager.hasSetMyStoriesPrivacy(transaction: transaction) if !localHasSetMyStoriesPrivacy && record.myStoryPrivacyHasBeenSet { - StoryManager.setHasSetMyStoriesPrivacy(transaction: transaction, shouldUpdateStorageService: false) + StoryManager.setHasSetMyStoriesPrivacy(true, shouldUpdateStorageService: false, transaction: transaction) } let localHasReadOnboardingStory = systemStoryManager.isOnboardingStoryRead(transaction: transaction) diff --git a/SignalServiceKit/TestUtils/MockSSKEnvironment.swift b/SignalServiceKit/TestUtils/MockSSKEnvironment.swift index a7e2644729..a21ca76062 100644 --- a/SignalServiceKit/TestUtils/MockSSKEnvironment.swift +++ b/SignalServiceKit/TestUtils/MockSSKEnvironment.swift @@ -14,8 +14,11 @@ public class MockSSKEnvironment: NSObject { /// Set up a mock SSK environment as well as ``DependenciesBridge``. @objc public static func activate() { + let testAppContext = TestAppContext() + SetCurrentAppContext(testAppContext) + let finalContinuation = AppSetup().start( - appContext: TestAppContext(), + appContext: testAppContext, databaseStorage: try! SDSDatabaseStorage( databaseFileUrl: SDSDatabaseStorage.grdbDatabaseFileUrl, keychainStorage: MockKeychainStorage() diff --git a/SignalServiceKit/TestUtils/OWSFakeProfileManager.m b/SignalServiceKit/TestUtils/OWSFakeProfileManager.m index 80f41b11cd..00deaad10c 100644 --- a/SignalServiceKit/TestUtils/OWSFakeProfileManager.m +++ b/SignalServiceKit/TestUtils/OWSFakeProfileManager.m @@ -245,11 +245,6 @@ NS_ASSUME_NONNULL_BEGIN // Do nothing. } -- (NSArray *)allWhitelistedRegisteredAddressesWithTx:(SDSAnyReadTransaction *)tx -{ - return @[]; -} - - (void)rotateProfileKeyUponRecipientHideWithTx:(SDSAnyWriteTransaction *)tx { // Do nothing. diff --git a/SignalServiceKit/TestUtils/OWSFakeProfileManager.swift b/SignalServiceKit/TestUtils/OWSFakeProfileManager.swift index 3f42949f90..3554b9c261 100644 --- a/SignalServiceKit/TestUtils/OWSFakeProfileManager.swift +++ b/SignalServiceKit/TestUtils/OWSFakeProfileManager.swift @@ -90,6 +90,9 @@ extension OWSFakeProfileManager: ProfileManager { tx: DBWriteTransaction ) { } + + public func allWhitelistedAddresses(tx: SDSAnyReadTransaction) -> [SignalServiceAddress] { [] } + public func allWhitelistedRegisteredAddresses(tx: SDSAnyReadTransaction) -> [SignalServiceAddress] { [] } } #endif diff --git a/SignalServiceKit/Util/OWSUserProfile.swift b/SignalServiceKit/Util/OWSUserProfile.swift index fbfc99e1e6..8a245906d0 100644 --- a/SignalServiceKit/Util/OWSUserProfile.swift +++ b/SignalServiceKit/Util/OWSUserProfile.swift @@ -789,12 +789,20 @@ public final class OWSUserProfile: NSObject, NSCopying, SDSCodableModel, Decodab } @objc - public class func getOrBuildUserProfileForLocalUser(tx: SDSAnyWriteTransaction) -> OWSUserProfile { - return getOrBuildUserProfile(for: .localUser, tx: tx) + public class func getOrBuildUserProfileForLocalUser( + userProfileWriter: UserProfileWriter, + tx: SDSAnyWriteTransaction + ) -> OWSUserProfile { + return getOrBuildUserProfile( + for: .localUser, + userProfileWriter: userProfileWriter, + tx: tx + ) } public class func getOrBuildUserProfile( for insertableAddress: InsertableAddress, + userProfileWriter: UserProfileWriter, tx: SDSAnyWriteTransaction ) -> OWSUserProfile { // If we already have a profile for this address, return it. @@ -815,7 +823,7 @@ public final class OWSUserProfile: NSObject, NSCopying, SDSCodableModel, Decodab if case .localUser = address { userProfile.update( profileKey: .setTo(OWSAES256Key.generateRandom()), - userProfileWriter: .localUser, + userProfileWriter: userProfileWriter, transaction: tx, completion: nil ) diff --git a/SignalServiceKit/tests/Contacts/SignalRecipientTest.swift b/SignalServiceKit/tests/Contacts/SignalRecipientTest.swift index 749a733ed9..7edfb6b5e3 100644 --- a/SignalServiceKit/tests/Contacts/SignalRecipientTest.swift +++ b/SignalServiceKit/tests/Contacts/SignalRecipientTest.swift @@ -92,6 +92,7 @@ class SignalRecipientTest: SSKBaseTest { write { transaction in let aciProfile = OWSUserProfile.getOrBuildUserProfile( for: .otherUser(aci), + userProfileWriter: .tests, tx: transaction ) aciProfile.anyInsert(transaction: transaction) @@ -154,7 +155,11 @@ class SignalRecipientTest: SSKBaseTest { let oldMessage = messageBuilder.build() oldMessage.anyInsert(transaction: transaction) - let oldPhoneNumberProfile = OWSUserProfile.getOrBuildUserProfile(for: .otherUser(aci), tx: transaction) + let oldPhoneNumberProfile = OWSUserProfile.getOrBuildUserProfile( + for: .otherUser(aci), + userProfileWriter: .tests, + tx: transaction + ) oldPhoneNumberProfile.anyInsert(transaction: transaction) oldPhoneNumberProfile.update( isPhoneNumberShared: .setTo(true), @@ -183,7 +188,11 @@ class SignalRecipientTest: SSKBaseTest { uniqueId: oldMessage.uniqueId, transaction: transaction )! - let newProfile = OWSUserProfile.getOrBuildUserProfile(for: .otherUser(aci), tx: transaction) + let newProfile = OWSUserProfile.getOrBuildUserProfile( + for: .otherUser(aci), + userProfileWriter: .tests, + tx: transaction + ) let newAccount = SignalAccount.anyFetch( uniqueId: oldAccount.uniqueId, transaction: transaction @@ -259,7 +268,11 @@ class SignalRecipientTest: SSKBaseTest { uniqueId: oldMessage.uniqueId, transaction: transaction )! - let newProfile = OWSUserProfile.getOrBuildUserProfile(for: .otherUser(newAci), tx: transaction) + let newProfile = OWSUserProfile.getOrBuildUserProfile( + for: .otherUser(newAci), + userProfileWriter: .tests, + tx: transaction + ) let newAccount = SignalAccount.anyFetch( uniqueId: oldAccount.uniqueId, transaction: transaction diff --git a/SignalServiceKit/tests/MessageBackup/MessageBackupIntegrationTest.swift b/SignalServiceKit/tests/MessageBackup/MessageBackupIntegrationTest.swift new file mode 100644 index 0000000000..fbccdadf2e --- /dev/null +++ b/SignalServiceKit/tests/MessageBackup/MessageBackupIntegrationTest.swift @@ -0,0 +1,177 @@ +// +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import GRDB +import LibSignalClient +import XCTest + +@testable import SignalServiceKit + +private extension MessageBackupIntegrationTestCase { + var depBridge: DependenciesBridge { .shared } + + func skipTestForNow() throws { + // [Backup] TODO: unwind this once we've codified shared integration test cases. + throw XCTSkip("Skipped while we codify shared integration test cases.") + } +} + +final class MessageBackupAccountDataTest: MessageBackupIntegrationTestCase { + func testAccountData() async throws { + try skipTestForNow() + + try await runTest(backupName: "account-data") { sdsTx, tx in + XCTAssertNotNil(profileManager.localProfileKey()) + + switch depBridge.localUsernameManager.usernameState(tx: tx) { + case .available(let username, let usernameLink): + XCTAssertEqual(username, "boba_fett.66") + XCTAssertEqual(usernameLink.handle, UUID("61C101A2-00D5-4217-89C2-0518D8497AF0")) + XCTAssertEqual(depBridge.localUsernameManager.usernameLinkQRCodeColor(tx: tx), .olive) + case .unset, .linkCorrupted, .usernameAndLinkCorrupted: + XCTFail("Unexpected username state!") + } + + XCTAssertEqual(profileManager.localGivenName(), "Boba") + XCTAssertEqual(profileManager.localFamilyName(), "Fett") + XCTAssertNil(profileManager.localProfileAvatarData()) + + XCTAssertNotNil(subscriptionManager.getSubscriberID(transaction: sdsTx)) + XCTAssertEqual(subscriptionManager.getSubscriberCurrencyCode(transaction: sdsTx), "USD") + XCTAssertTrue(subscriptionManager.userManuallyCancelledSubscription(transaction: sdsTx)) + + XCTAssertTrue(receiptManager.areReadReceiptsEnabled(transaction: sdsTx)) + XCTAssertTrue(preferences.shouldShowUnidentifiedDeliveryIndicators(transaction: sdsTx)) + XCTAssertTrue(typingIndicatorsImpl.areTypingIndicatorsEnabled()) + XCTAssertFalse(SSKPreferences.areLinkPreviewsEnabled(transaction: sdsTx)) + XCTAssertEqual(depBridge.phoneNumberDiscoverabilityManager.phoneNumberDiscoverability(tx: tx), .nobody) + XCTAssertTrue(SSKPreferences.preferContactAvatars(transaction: sdsTx)) + let universalExpireConfig = depBridge.disappearingMessagesConfigurationStore.fetch(for: .universal, tx: tx) + XCTAssertEqual(universalExpireConfig?.isEnabled, true) + XCTAssertEqual(universalExpireConfig?.durationSeconds, 3600) + XCTAssertEqual(ReactionManager.customEmojiSet(transaction: sdsTx), ["🏎️"]) + XCTAssertTrue(subscriptionManager.displayBadgesOnProfile(transaction: sdsTx)) + XCTAssertTrue(SSKPreferences.shouldKeepMutedChatsArchived(transaction: sdsTx)) + XCTAssertTrue(StoryManager.hasSetMyStoriesPrivacy(transaction: sdsTx)) + XCTAssertTrue(systemStoryManager.isOnboardingStoryRead(transaction: sdsTx)) + XCTAssertFalse(StoryManager.areStoriesEnabled(transaction: sdsTx)) + XCTAssertTrue(StoryManager.areViewReceiptsEnabled(transaction: sdsTx)) + XCTAssertTrue(systemStoryManager.isOnboardingOverlayViewed(transaction: sdsTx)) + XCTAssertFalse(depBridge.usernameEducationManager.shouldShowUsernameEducation(tx: tx)) + XCTAssertEqual(udManager.phoneNumberSharingMode(tx: tx), .nobody) + } + } +} + +final class MessageBackupContactTest: MessageBackupIntegrationTestCase { + private func assert( + recipient: SignalRecipient, + aci: Aci = .fixture, + pni: Pni = .fixture, + username: String = "han_solo.44", + phoneNumber: String = "+17735550199", + isBlocked: Bool, + isHidden: Bool, + isRegistered: Bool, + unregisteredAtTimestamp: UInt64?, + isWhitelisted: Bool, + givenName: String = "Han", + familyName: String = "Solo", + isStoryHidden: Bool = true, + sdsTx: SDSAnyReadTransaction, + tx: DBReadTransaction + ) { + XCTAssertEqual(recipient.aci, aci) + XCTAssertEqual(recipient.pni, pni) + XCTAssertEqual(depBridge.usernameLookupManager.fetchUsername(forAci: recipient.aci!, transaction: tx), username) + XCTAssertEqual(recipient.phoneNumber?.stringValue, phoneNumber) + XCTAssertEqual(blockingManager.isAddressBlocked(recipient.address, transaction: sdsTx), isBlocked) + XCTAssertEqual(depBridge.recipientHidingManager.isHiddenRecipient(recipient, tx: tx), isHidden) + XCTAssertEqual(recipient.isRegistered, isRegistered) + XCTAssertEqual(recipient.unregisteredAtTimestamp, unregisteredAtTimestamp) + let recipientProfile = profileManager.getUserProfile(for: recipient.address, transaction: sdsTx) + XCTAssertNotNil(recipientProfile?.profileKey) + XCTAssertEqual(profileManager.isUser(inProfileWhitelist: recipient.address, transaction: sdsTx), isWhitelisted) + XCTAssertEqual(recipientProfile?.givenName, givenName) + XCTAssertEqual(recipientProfile?.familyName, familyName) + XCTAssertEqual(StoryStoreImpl().getOrCreateStoryContextAssociatedData(for: recipient.aci!, tx: tx).isHidden, isStoryHidden) + } + + func testRegisteredBlockedContact() async throws { + try skipTestForNow() + + try await runTest(backupName: "registered-blocked-contact") { sdsTx, tx in + let allRecipients = depBridge.recipientDatabaseTable.allRecipients(tx: tx) + XCTAssertEqual(allRecipients.count, 1) + + assert( + recipient: allRecipients.first!, + isBlocked: true, + isHidden: true, + isRegistered: true, + unregisteredAtTimestamp: nil, + isWhitelisted: false, + sdsTx: sdsTx, + tx: tx + ) + } + } + + func testUnregisteredContact() async throws { + try skipTestForNow() + + try await runTest(backupName: "unregistered-contact") { sdsTx, tx in + let allRecipients = depBridge.recipientDatabaseTable.allRecipients(tx: tx) + XCTAssertEqual(allRecipients.count, 1) + + assert( + recipient: allRecipients.first!, + isBlocked: false, + isHidden: false, + isRegistered: false, + unregisteredAtTimestamp: 1713157772000, + isWhitelisted: true, + sdsTx: sdsTx, + tx: tx + ) + } + } +} + +// MARK: - + +private extension RecipientDatabaseTable { + func allRecipients(tx: any DBReadTransaction) -> [SignalRecipient] { + var result = [SignalRecipient]() + enumerateAll(tx: tx) { result.append($0) } + return result + } +} + +// MARK: _ + +private extension UUID { + init(_ string: String) { + self.init(uuidString: string)! + } +} + +private extension Aci { + /// Corresponds to base64 data `QHaZXgUxQEKp5B5np33zWA==`. + static let fixture: Aci = Aci("4076995E-0531-4042-A9E4-1E67A77DF358") + + convenience init(_ string: String) { + self.init(fromUUID: UUID(string)) + } +} + +private extension Pni { + /// Corresponds to base64 data `JvwCorpYSn2wgZ2iOXFXCg==`. + static let fixture: Pni = Pni("26FC02A2-BA58-4A7D-B081-9DA23971570A") + + convenience init(_ string: String) { + self.init(fromUUID: UUID(string)) + } +} diff --git a/SignalServiceKit/tests/MessageBackup/MessageBackupIntegrationTestCase.swift b/SignalServiceKit/tests/MessageBackup/MessageBackupIntegrationTestCase.swift new file mode 100644 index 0000000000..a21c80017b --- /dev/null +++ b/SignalServiceKit/tests/MessageBackup/MessageBackupIntegrationTestCase.swift @@ -0,0 +1,175 @@ +// +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import LibSignalClient +import XCTest + +@testable import SignalServiceKit + +class MessageBackupIntegrationTestCase: XCTestCase { + override func setUp() { + DDLog.add(DDTTYLogger.sharedInstance!) + } + + // MARK: - + + private var messageBackupManager: MessageBackupManager { + DependenciesBridge.shared.messageBackupManager + } + + private var localIdentifiers: LocalIdentifiers { + /// A backup doesn't contain our own local identifiers. Rather, those + /// are determined as part of registration for a backup import, and are + /// already-known for a backup export. + /// + /// Consequently, we can use any local identifiers for our test + /// purposes without worrying about the contents of each test case's + /// backup file. + return .forUnitTests + } + + func runTest( + backupName: String, + assertionsBlock: (SDSAnyReadTransaction, DBReadTransaction) throws -> Void + ) async throws { + try await importAndAssert( + localIdentifiers: localIdentifiers, + backupUrl: backupFileUrl(named: backupName), + assertionsBlock: assertionsBlock + ) + + let exportedBackupUrl = try await messageBackupManager + .exportPlaintextBackup(localIdentifiers: localIdentifiers) + + try await importAndAssert( + localIdentifiers: localIdentifiers, + backupUrl: exportedBackupUrl, + assertionsBlock: assertionsBlock + ) + } + + private func backupFileUrl(named backupName: String) -> URL { + let testBundle = Bundle(for: type(of: self)) + return testBundle.url(forResource: backupName, withExtension: "binproto")! + } + + private func importAndAssert( + localIdentifiers: LocalIdentifiers, + backupUrl: URL, + assertionsBlock: (SDSAnyReadTransaction, DBReadTransaction) throws -> Void + ) async throws { + await initializeApp() + + try await messageBackupManager.importPlaintextBackup( + fileUrl: backupUrl, + localIdentifiers: localIdentifiers + ) + + try NSObject.databaseStorage.read { tx in + try assertionsBlock(tx, tx.asV2Read) + } + } + + // MARK: - + + @MainActor + final func initializeApp() async { + let testAppContext = TestAppContext() + SetCurrentAppContext(testAppContext) + + /// Note that ``SDSDatabaseStorage/grdbDatabaseFileUrl``, through a few + /// layers of abstraction, uses the "current app context" to decide + /// where to put the database, + /// + /// For a ``TestAppContext`` as configured above, this will be a + /// subdirectory of our temp directory unique to the instantiation of + /// the app context. + let databaseStorage = try! SDSDatabaseStorage( + databaseFileUrl: SDSDatabaseStorage.grdbDatabaseFileUrl, + keychainStorage: MockKeychainStorage() + ) + + /// We use crashy versions of dependencies that should never be called + /// during backups, and no-op implementations of payments because those + /// are bound to the SignalUI target. + _ = await AppSetup().start( + appContext: testAppContext, + databaseStorage: databaseStorage, + paymentsEvents: PaymentsEventsNoop(), + mobileCoinHelper: MobileCoinHelperMock(), + callMessageHandler: CrashyMocks.MockCallMessageHandler(), + currentCallThreadProvider: CrashyMocks.MockCurrentCallThreadProvider(), + notificationPresenter: CrashyMocks.MockNotificationPresenter(), + testDependencies: AppSetup.TestDependencies( + networkManager: CrashyMocks.MockNetworkManager(), + webSocketFactory: CrashyMocks.MockWebSocketFactory() + ) + ).prepareDatabase().awaitable() + } +} + +// MARK: - + +private func failTest( + _ type: T.Type, + _ function: StaticString = #function +) -> Never { + let message = "Unexpectedly called \(type)#\(function)!" + XCTFail(message) + owsFail(message) +} + +/// As a rule, integration tests for message backup should not mock out their +/// dependencies as their goal is to validate how the real, production app will +/// behave with respect to Backups. +/// +/// These mocks are the exceptions to that rule, and encompass managers that +/// should never be invoked during Backup import or export. +private enum CrashyMocks { + final class MockNetworkManager: NetworkManager { + override func makePromise(request: TSRequest, canUseWebSocket: Bool = false) -> Promise { failTest(Self.self) } + } + + final class MockWebSocketFactory: WebSocketFactory { + var canBuildWebSocket: Bool { failTest(Self.self) } + func buildSocket(request: WebSocketRequest, callbackScheduler: any Scheduler) -> (any SSKWebSocket)? { failTest(Self.self) } + } + + final class MockCallMessageHandler: CallMessageHandler { + func action(for envelope: SSKProtoEnvelope, callMessage: SSKProtoCallMessage, serverDeliveryTimestamp: UInt64) -> CallMessageAction { failTest(Self.self) } + func receivedOffer(_ offer: SSKProtoCallMessageOffer, from caller: (aci: Aci, deviceId: UInt32), sentAtTimestamp: UInt64, serverReceivedTimestamp: UInt64, serverDeliveryTimestamp: UInt64, tx: SDSAnyWriteTransaction) { failTest(Self.self) } + func receivedAnswer(_ answer: SSKProtoCallMessageAnswer, from caller: (aci: Aci, deviceId: UInt32)) { failTest(Self.self) } + func receivedIceUpdate(_ iceUpdate: [SSKProtoCallMessageIceUpdate], from caller: (aci: Aci, deviceId: UInt32)) { failTest(Self.self) } + func receivedHangup(_ hangup: SSKProtoCallMessageHangup, from caller: (aci: Aci, deviceId: UInt32)) { failTest(Self.self) } + func receivedBusy(_ busy: SSKProtoCallMessageBusy, from caller: (aci: Aci, deviceId: UInt32)) { failTest(Self.self) } + func receivedOpaque(_ opaque: SSKProtoCallMessageOpaque, from caller: (aci: Aci, deviceId: UInt32), serverReceivedTimestamp: UInt64, serverDeliveryTimestamp: UInt64, tx: SDSAnyReadTransaction) { failTest(Self.self) } + func receivedGroupCallUpdateMessage(_ updateMessage: SSKProtoDataMessageGroupCallUpdate, for thread: TSGroupThread, serverReceivedTimestamp: UInt64) async { failTest(Self.self) } + func externallyHandleCallMessage(envelope: SSKProtoEnvelope, plaintextData: Data, wasReceivedByUD: Bool, serverDeliveryTimestamp: UInt64, tx: SDSAnyWriteTransaction) { failTest(Self.self) } + } + + final class MockCurrentCallThreadProvider: CurrentCallThreadProvider { + var currentCallThread: TSThread? { failTest(Self.self) } + } + + final class MockNotificationPresenter: NotificationPresenter { + func notifyUser(forIncomingMessage: TSIncomingMessage, thread: TSThread, transaction: SDSAnyReadTransaction) { failTest(Self.self) } + func notifyUser(forIncomingMessage: TSIncomingMessage, editTarget: TSIncomingMessage, thread: TSThread, transaction: SDSAnyReadTransaction) { failTest(Self.self) } + func notifyUser(forReaction: OWSReaction, onOutgoingMessage: TSOutgoingMessage, thread: TSThread, transaction: SDSAnyReadTransaction) { failTest(Self.self) } + func notifyUser(forErrorMessage: TSErrorMessage, thread: TSThread, transaction: SDSAnyWriteTransaction) { failTest(Self.self) } + func notifyUser(forTSMessage: TSMessage, thread: TSThread, wantsSound: Bool, transaction: SDSAnyWriteTransaction) { failTest(Self.self) } + func notifyUser(forPreviewableInteraction: any TSInteraction & OWSPreviewText, thread: TSThread, wantsSound: Bool, transaction: SDSAnyWriteTransaction) { failTest(Self.self) } + func notifyTestPopulation(ofErrorMessage errorString: String) { failTest(Self.self) } + func notifyUser(forFailedStorySend: StoryMessage, to: TSThread, transaction: SDSAnyWriteTransaction) { failTest(Self.self) } + func notifyUserToRelaunchAfterTransfer(completion: (() -> Void)?) { failTest(Self.self) } + func notifyUserOfDeregistration(transaction: SDSAnyWriteTransaction) { failTest(Self.self) } + func clearAllNotifications() { failTest(Self.self) } + func cancelNotifications(threadId: String) { failTest(Self.self) } + func cancelNotifications(messageIds: [String]) { failTest(Self.self) } + func cancelNotifications(reactionId: String) { failTest(Self.self) } + func cancelNotificationsForMissedCalls(threadUniqueId: String) { failTest(Self.self) } + func cancelNotifications(for storyMessage: StoryMessage) { failTest(Self.self) } + func notifyUserOfDeregistration(tx: any DBWriteTransaction) { failTest(Self.self) } + } +} diff --git a/SignalServiceKit/tests/MessageBackup/Scripts/generate-base64-bytes.zsh b/SignalServiceKit/tests/MessageBackup/Scripts/generate-base64-bytes.zsh new file mode 100755 index 0000000000..c348dbeb53 --- /dev/null +++ b/SignalServiceKit/tests/MessageBackup/Scripts/generate-base64-bytes.zsh @@ -0,0 +1,9 @@ +#!/usr/bin/env zsh + +if [ "$#" -ne 1 ]; then + length=32 +else + length="$1" +fi + +openssl rand -base64 "$length" diff --git a/SignalServiceKit/tests/MessageBackup/Scripts/generate-base64-uuid.zsh b/SignalServiceKit/tests/MessageBackup/Scripts/generate-base64-uuid.zsh new file mode 100755 index 0000000000..294ea4e7af --- /dev/null +++ b/SignalServiceKit/tests/MessageBackup/Scripts/generate-base64-uuid.zsh @@ -0,0 +1,6 @@ +#!/usr/bin/env zsh + +uuid=$(uuidgen) + +echo "UUID: $uuid" +echo "UUID Base64: $(echo $uuid | xxd -r -p | base64)" diff --git a/SignalServiceKit/tests/MessageBackup/TestCases/account-data.jsonproto b/SignalServiceKit/tests/MessageBackup/TestCases/account-data.jsonproto new file mode 100644 index 0000000000..2fd4f36788 --- /dev/null +++ b/SignalServiceKit/tests/MessageBackup/TestCases/account-data.jsonproto @@ -0,0 +1,42 @@ +[ + { + "version": "1", + "backupTimeMs": "1715636551000" + }, + { + "account": { + "profileKey": "YQKRq+3DQklInaOaMcmlzZnN0m/1hzLiaONX7gB12dg=", + "username": "boba_fett.66", + "usernameLink": { + "entropy": "ZWdcc9AOsBAF47t8SkfylstlVPeJgSOIFekV2CT9LpM=", + "serverId": "YcEBogDVQheJwgUY2El68A==", + "color": "OLIVE" + }, + "givenName": "Boba", + "familyName": "Fett", + "avatarUrlPath": "", + "subscriberId": "7LtoxzQzGi6jM82nR8mMRVNlImFYK0/OWuDeqE3OZRk=", + "subscriberCurrencyCode": "USD", + "subscriptionManuallyCancelled": true, + "accountSettings": { + "readReceipts": true, + "sealedSenderIndicators": true, + "typingIndicators": true, + "linkPreviews": false, + "notDiscoverableByPhoneNumber": true, + "preferContactAvatars": true, + "universalExpireTimer": 3600, + "preferredReactionEmoji": ["🏎️"], + "displayBadgesOnProfile": true, + "keepMutedChatsArchived": true, + "hasSetMyStoriesPrivacy": true, + "hasViewedOnboardingStory": true, + "storiesDisabled": true, + "storyViewReceiptsEnabled": true, + "hasSeenGroupStoryEducationSheet": true, + "hasCompletedUsernameOnboarding": true, + "phoneNumberSharingMode": "NOBODY" + } + } + } +] diff --git a/SignalServiceKit/tests/MessageBackup/TestCases/registered-blocked-contact.jsonproto b/SignalServiceKit/tests/MessageBackup/TestCases/registered-blocked-contact.jsonproto new file mode 100644 index 0000000000..466ba140cf --- /dev/null +++ b/SignalServiceKit/tests/MessageBackup/TestCases/registered-blocked-contact.jsonproto @@ -0,0 +1,74 @@ +[ + { + "version": "1", + "backupTimeMs": "1715636551000" + }, + { + "account": { + "profileKey": "YQKRq+3DQklInaOaMcmlzZnN0m/1hzLiaONX7gB12dg=", + "givenName": "Boba", + "familyName": "Fett", + "avatarUrlPath": "", + "subscriberId": "", + "subscriberCurrencyCode": "", + "subscriptionManuallyCancelled": false, + "accountSettings": { + "readReceipts": false, + "sealedSenderIndicators": true, + "typingIndicators": false, + "linkPreviews": false, + "notDiscoverableByPhoneNumber": false, + "preferContactAvatars": false, + "universalExpireTimer": 0, + "preferredReactionEmoji": [], + "displayBadgesOnProfile": false, + "keepMutedChatsArchived": false, + "hasSetMyStoriesPrivacy": false, + "hasViewedOnboardingStory": false, + "storiesDisabled": false, + "storyViewReceiptsEnabled": false, + "hasSeenGroupStoryEducationSheet": false, + "hasCompletedUsernameOnboarding": false, + "phoneNumberSharingMode": "NOBODY" + } + } + }, + { + "recipient": { + "id": "1", + "self": {} + } + }, + { + "recipient": { + "id": "2", + "contact": { + "aci": "QHaZXgUxQEKp5B5np33zWA==", + "pni": "JvwCorpYSn2wgZ2iOXFXCg==", + "username": "han_solo.44", + "e164": "17735550199", + "blocked": true, + "hidden": true, + "registered": "REGISTERED", + "unregisteredTimestamp": 0, + "profileKey": "nH0NX5+LqtIe85lAy958oyRNH9INMHFn2eb1VF6i4/o=", + "profileSharing": false, + "profileGivenName": "Han", + "profileFamilyName": "Solo", + "hideStory": true + } + } + }, + { + "chat": { + "id": "1", + "recipientId": "1" + } + }, + { + "chat": { + "id": "2", + "recipientId": "2" + } + } +] diff --git a/SignalServiceKit/tests/MessageBackup/TestCases/unregistered-contact.jsonproto b/SignalServiceKit/tests/MessageBackup/TestCases/unregistered-contact.jsonproto new file mode 100644 index 0000000000..a982c46c7d --- /dev/null +++ b/SignalServiceKit/tests/MessageBackup/TestCases/unregistered-contact.jsonproto @@ -0,0 +1,74 @@ +[ + { + "version": "1", + "backupTimeMs": "1715636551000" + }, + { + "account": { + "profileKey": "YQKRq+3DQklInaOaMcmlzZnN0m/1hzLiaONX7gB12dg=", + "givenName": "Boba", + "familyName": "Fett", + "avatarUrlPath": "", + "subscriberId": "", + "subscriberCurrencyCode": "", + "subscriptionManuallyCancelled": false, + "accountSettings": { + "readReceipts": false, + "sealedSenderIndicators": true, + "typingIndicators": false, + "linkPreviews": false, + "notDiscoverableByPhoneNumber": false, + "preferContactAvatars": false, + "universalExpireTimer": 0, + "preferredReactionEmoji": [], + "displayBadgesOnProfile": false, + "keepMutedChatsArchived": false, + "hasSetMyStoriesPrivacy": false, + "hasViewedOnboardingStory": false, + "storiesDisabled": false, + "storyViewReceiptsEnabled": false, + "hasSeenGroupStoryEducationSheet": false, + "hasCompletedUsernameOnboarding": false, + "phoneNumberSharingMode": "NOBODY" + } + } + }, + { + "recipient": { + "id": "1", + "self": {} + } + }, + { + "recipient": { + "id": "2", + "contact": { + "aci": "QHaZXgUxQEKp5B5np33zWA==", + "pni": "JvwCorpYSn2wgZ2iOXFXCg==", + "username": "han_solo.44", + "e164": "17735550199", + "blocked": false, + "hidden": false, + "registered": "NOT_REGISTERED", + "unregisteredTimestamp": 1713157772000, + "profileKey": "nH0NX5+LqtIe85lAy958oyRNH9INMHFn2eb1VF6i4/o=", + "profileSharing": true, + "profileGivenName": "Han", + "profileFamilyName": "Solo", + "hideStory": true + } + } + }, + { + "chat": { + "id": "1", + "recipientId": "1" + } + }, + { + "chat": { + "id": "2", + "recipientId": "2" + } + } +] diff --git a/SignalServiceKit/tests/SSKBaseTest.swift b/SignalServiceKit/tests/SSKBaseTest.swift index 837be3e4c3..72c31cbd74 100644 --- a/SignalServiceKit/tests/SSKBaseTest.swift +++ b/SignalServiceKit/tests/SSKBaseTest.swift @@ -10,7 +10,6 @@ import CocoaLumberjack public class SSKBaseTest: XCTestCase { public override func setUp() { DDLog.add(DDTTYLogger.sharedInstance!) - SetCurrentAppContext(TestAppContext()) MockSSKEnvironment.activate() }