From 50dbffd32a1617a4d0867c7215d7a3f6ace49c6f Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 7 Aug 2025 13:13:06 +1000 Subject: [PATCH 01/59] Renamed SessionSnodeKit to SessionNetworkingKit --- Session.xcodeproj/project.pbxproj | 146 +++++++++--------- .../xcshareddata/xcschemes/Session.xcscheme | 4 +- ...xcscheme => SessionNetworkingKit.xcscheme} | 8 +- .../Calls/Call Management/SessionCall.swift | 2 +- Session/Calls/WebRTC/WebRTCSession.swift | 2 +- .../Closed Groups/EditGroupViewModel.swift | 2 +- .../ContextMenuVC+ActionView.swift | 2 +- .../ConversationVC+Interaction.swift | 2 +- .../Conversations/ConversationViewModel.swift | 2 +- .../DisappearingMessageTimerView.swift | 2 +- ...isappearingMessagesSettingsViewModel.swift | 2 +- .../ThreadNotificationSettingsViewModel.swift | 2 +- .../Settings/ThreadSettingsViewModel.swift | 2 +- .../New Conversation/NewMessageScreen.swift | 2 +- .../GIFs/GifPickerCell.swift | 2 +- .../GIFs/GiphyAPI.swift | 2 +- .../MediaPageViewController.swift | 2 +- .../MessageInfoScreen.swift | 2 +- .../PhotoCapture.swift | 2 +- Session/Meta/AppDelegate.swift | 2 +- Session/Meta/Session+SNUIKit.swift | 2 +- .../NotificationActionHandler.swift | 2 +- Session/Notifications/SyncPushTokensJob.swift | 2 +- Session/Onboarding/LoadingScreen.swift | 2 +- Session/Onboarding/Onboarding.swift | 2 +- Session/Onboarding/PNModeScreen.swift | 2 +- Session/Path/PathStatusView.swift | 2 +- Session/Path/PathVC.swift | 2 +- .../Settings/DeveloperSettingsViewModel.swift | 2 +- Session/Settings/NukeDataModal.swift | 2 +- .../SessionNetworkScreen+ViewModel.swift | 2 +- Session/Utilities/BackgroundPoller.swift | 2 +- Session/Utilities/IP2Country.swift | 2 +- .../Crypto/Crypto+SessionMessagingKit.swift | 2 +- .../Migrations/_002_SetupStandardJobs.swift | 2 +- .../Migrations/_004_RemoveLegacyYDB.swift | 2 +- .../_022_GroupsRebuildChanges.swift | 2 +- .../Database/Models/Attachment.swift | 2 +- .../Database/Models/ClosedGroup.swift | 2 +- .../Database/Models/ConfigDump.swift | 2 +- .../DisappearingMessageConfiguration.swift | 2 +- .../Database/Models/Interaction.swift | 2 +- .../Database/Models/LinkPreview.swift | 2 +- .../Database/Models/SessionThread.swift | 2 +- .../Jobs/AttachmentDownloadJob.swift | 2 +- .../Jobs/AttachmentUploadJob.swift | 2 +- .../Jobs/CheckForAppUpdatesJob.swift | 2 +- .../Jobs/ConfigMessageReceiveJob.swift | 2 +- .../Jobs/ConfigurationSyncJob.swift | 2 +- .../Jobs/DisappearingMessagesJob.swift | 2 +- .../Jobs/DisplayPictureDownloadJob.swift | 2 +- .../Jobs/ExpirationUpdateJob.swift | 2 +- .../Jobs/GarbageCollectionJob.swift | 2 +- .../Jobs/GetExpirationJob.swift | 2 +- .../Jobs/GroupInviteMemberJob.swift | 2 +- .../Jobs/GroupLeavingJob.swift | 2 +- .../Jobs/GroupPromoteMemberJob.swift | 2 +- SessionMessagingKit/Jobs/MessageSendJob.swift | 2 +- ...ProcessPendingGroupMemberRemovalsJob.swift | 2 +- .../Jobs/SendReadReceiptsJob.swift | 2 +- .../LibSession+GroupInfo.swift | 2 +- .../LibSession+GroupMembers.swift | 2 +- .../Config Handling/LibSession+Shared.swift | 2 +- .../LibSession+SharedGroup.swift | 2 +- .../LibSession+UserGroups.swift | 2 +- .../LibSession+SessionMessagingKit.swift | 2 +- .../LibSession/Types/Config.swift | 2 +- .../Messages/Message+Destination.swift | 2 +- .../Message+DisappearingMessages.swift | 2 +- .../Messages/Message+Origin.swift | 2 +- SessionMessagingKit/Messages/Message.swift | 2 +- .../Open Groups/Models/SOGSMessage.swift | 2 +- .../Open Groups/OpenGroupAPI.swift | 2 +- .../Open Groups/OpenGroupManager.swift | 2 +- .../Types/HTTPHeader+OpenGroup.swift | 2 +- .../Types/HTTPQueryParam+OpenGroup.swift | 2 +- .../Types/Request+OpenGroupAPI.swift | 2 +- .../Open Groups/Types/SOGSEndpoint.swift | 2 +- .../MessageReceiver+Calls.swift | 2 +- ...eReceiver+DataExtractionNotification.swift | 2 +- .../MessageReceiver+Groups.swift | 2 +- .../MessageReceiver+LegacyClosedGroups.swift | 2 +- .../MessageReceiver+LibSession.swift | 2 +- .../MessageReceiver+MessageRequests.swift | 2 +- .../MessageReceiver+UnsendRequests.swift | 2 +- .../MessageReceiver+VisibleMessages.swift | 2 +- .../MessageSender+Groups.swift | 2 +- .../Sending & Receiving/MessageReceiver.swift | 2 +- .../MessageSender+Convenience.swift | 2 +- .../Sending & Receiving/MessageSender.swift | 2 +- .../Models/LegacyUnsubscribeRequest.swift | 2 +- .../Models/NotificationMetadata.swift | 2 +- .../Models/SubscribeRequest.swift | 2 +- .../Models/UnsubscribeRequest.swift | 2 +- .../Notifications/PushNotificationAPI.swift | 2 +- .../Types/PushNotificationAPIEndpoint.swift | 2 +- .../Types/Request+PushNotificationAPI.swift | 2 +- .../Pollers/CommunityPoller.swift | 2 +- .../Pollers/CurrentUserPoller.swift | 2 +- .../Pollers/GroupPoller.swift | 2 +- .../Pollers/PollerType.swift | 2 +- .../Pollers/SwarmPoller.swift | 2 +- .../Typing Indicators/TypingIndicators.swift | 2 +- .../MessageViewModel+DeletionActions.swift | 2 +- .../Authentication+SessionMessagingKit.swift | 2 +- .../Utilities/DisplayPictureManager.swift | 2 +- .../Utilities/MessageWrapper.swift | 2 +- .../SNProtoEnvelope+Conversion.swift | 2 +- .../Jobs/DisplayPictureDownloadJobSpec.swift | 2 +- ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 2 +- .../LibSession/LibSessionGroupInfoSpec.swift | 6 +- .../LibSessionGroupMembersSpec.swift | 2 +- .../LibSession/LibSessionSpec.swift | 2 +- .../Open Groups/Models/SOGSMessageSpec.swift | 2 +- .../Open Groups/OpenGroupAPISpec.swift | 2 +- .../Open Groups/OpenGroupManagerSpec.swift | 2 +- .../Open Groups/Types/SOGSEndpointSpec.swift | 2 +- .../MessageReceiverGroupsSpec.swift | 4 +- .../MessageSenderGroupsSpec.swift | 2 +- .../MessageSenderSpec.swift | 2 +- .../Pollers/CommunityPollerSpec.swift | 2 +- .../_TestUtilities/MockPoller.swift | 2 +- .../_TestUtilities/MockSwarmPoller.swift | 2 +- .../Configuration.swift | 4 +- .../Crypto/Crypto+SessionNetworkingKit.swift | 0 .../_001_InitialSetupMigration.swift | 2 +- .../Migrations/_002_SetupStandardJobs.swift | 2 +- .../Migrations/_003_YDBToGRDBMigration.swift | 2 +- ...04_FlagMessageHashAsDeletedOrInvalid.swift | 2 +- ...ddSnodeReveivedMessageInfoPrimaryKey.swift | 2 +- .../Migrations/_006_DropSnodeCache.swift | 2 +- .../_007_SplitSnodeReceivedMessageInfo.swift | 2 +- .../_008_ResetUserConfigLastHashes.swift | 2 +- .../Models/SnodeReceivedMessageInfo.swift | 0 .../LibSession/LibSession+Networking.swift | 0 .../Meta/Info.plist | 0 .../Meta/SessionNetworkingKit.h | 4 + .../Models/AppVersionResponse.swift | 0 .../Models/DeleteAllBeforeRequest.swift | 0 .../Models/DeleteAllBeforeResponse.swift | 0 .../Models/DeleteAllMessagesRequest.swift | 0 .../Models/DeleteAllMessagesResponse.swift | 0 .../Models/DeleteMessagesRequest.swift | 0 .../Models/DeleteMessagesResponse.swift | 0 .../Models/FileUploadResponse.swift | 0 .../Models/GetExpiriesRequest.swift | 0 .../Models/GetExpiriesResponse.swift | 0 .../Models/GetMessagesRequest.swift | 0 .../Models/GetMessagesResponse.swift | 0 .../Models/GetNetworkTimestampResponse.swift | 0 .../Models/LegacyGetMessagesRequest.swift | 0 .../Models/LegacySendMessageRequest.swift | 0 .../Models/ONSResolveRequest.swift | 0 .../Models/ONSResolveResponse.swift | 0 .../Models/OxenDaemonRPCRequest.swift | 0 .../Models/RevokeSubaccountRequest.swift | 0 .../Models/RevokeSubaccountResponse.swift | 0 .../Models/SendMessageRequest.swift | 0 .../Models/SendMessageResponse.swift | 0 .../SnodeAuthenticatedRequestBody.swift | 0 .../Models/SnodeBatchRequest.swift | 0 .../Models/SnodeMessage.swift | 0 .../Models/SnodeReceivedMessage.swift | 0 .../Models/SnodeRecursiveResponse.swift | 0 .../Models/SnodeRequest.swift | 0 .../Models/SnodeResponse.swift | 0 .../Models/SnodeSwarmItem.swift | 0 .../Models/UnrevokeSubaccountRequest.swift | 0 .../Models/UnrevokeSubaccountResponse.swift | 0 .../Models/UpdateExpiryAllRequest.swift | 0 .../Models/UpdateExpiryAllResponse.swift | 0 .../Models/UpdateExpiryRequest.swift | 0 .../Models/UpdateExpiryResponse.swift | 0 .../Networking/SnodeAPI.swift | 0 .../HTTPHeader+SessionNetwork.swift | 0 .../SessionNetworkAPI+Database.swift | 0 .../SessionNetworkAPI+Models.swift | 0 .../SessionNetworkAPI+Network.swift | 4 +- .../SessionNetworkAPI/SessionNetworkAPI.swift | 0 .../SnodeAPI/Request+SnodeAPI.swift | 0 .../SnodeAPI/ResponseInfo+SnodeAPI.swift | 0 .../SnodeAPI/SnodeAPI.swift | 0 .../SnodeAPI/SnodeAPIEndpoint.swift | 0 .../SnodeAPI/SnodeAPIError.swift | 0 .../SnodeAPI/SnodeAPINamespace.swift | 0 .../Types/BatchRequest.swift | 0 .../Types/BatchResponse.swift | 0 .../Types/BencodeResponse.swift | 0 .../Types/ContentProxy.swift | 0 .../Types/Destination.swift | 0 .../Types/HTTPHeader.swift | 0 .../Types/HTTPMethod.swift | 0 .../Types/HTTPQueryParam.swift | 0 .../Types/IPv4.swift | 0 .../Types/JSON.swift | 0 .../Types/Network.swift | 0 .../Types/NetworkError.swift | 0 .../Types/PreparedRequest+Sending.swift | 0 .../Types/PreparedRequest.swift | 0 .../Types/ProxiedContentDownloader.swift | 0 .../Types/Request.swift | 0 .../Types/RequestCategory.swift | 0 .../Types/ResponseInfo.swift | 0 .../HTTPHeader+SessionNetwork.swift | 9 ++ .../SessionNetworkAPI+Database.swift | 32 ++++ .../SessionNetworkAPI+Models.swift | 75 +++++++++ .../SessionNetworkAPI+Network.swift | 107 +++++++++++++ .../SessionNetworkAPI/SessionNetworkAPI.swift | 131 ++++++++++++++++ .../Types/SwarmDrainBehaviour.swift | 0 .../Types/UpdatableTimestamp.swift | 0 .../Types/ValidatableResponse.swift | 0 .../Utilities/Data+Utilities.swift | 0 .../Utilities/Publisher+Utilities.swift | 0 .../Utilities/RetryWithDependencies.swift | 0 .../Utilities/String+Trimming.swift | 0 .../Utilities/URLResponse+Utilities.swift | 0 .../Models/FileUploadResponseSpec.swift | 2 +- .../Models/SnodeRequestSpec.swift | 2 +- .../SessionNetworkingKit.xctestplan | 2 +- .../Types/BatchRequestSpec.swift | 2 +- .../Types/BatchResponseSpec.swift | 2 +- .../Types/BencodeResponseSpec.swift | 2 +- .../Types/DestinationSpec.swift | 2 +- .../Types/HeaderSpec.swift | 2 +- .../Types/PreparedRequestSendingSpec.swift | 2 +- .../Types/PreparedRequestSpec.swift | 2 +- .../Types/RequestSpec.swift | 2 +- .../CommonSSKMockExtensions.swift | 2 +- .../_TestUtilities/MockNetwork.swift | 2 +- .../_TestUtilities/MockSnodeAPICache.swift | 2 +- .../NotificationServiceExtension.swift | 2 +- .../ShareNavController.swift | 2 +- SessionShareExtension/ThreadPickerVC.swift | 2 +- SessionSnodeKit/Meta/SessionSnodeKit.h | 4 - SessionSnodeKit/Utilities/Threading+SSK.swift | 6 - ...eadDisappearingMessagesViewModelSpec.swift | 4 +- ...eadNotificationSettingsViewModelSpec.swift | 4 +- .../ThreadSettingsViewModelSpec.swift | 4 +- SessionTests/Database/DatabaseSpec.swift | 6 +- SessionTests/Session.xctestplan | 4 +- .../NotificationContentViewModelSpec.swift | 4 +- .../Database/Types/TargetMigrations.swift | 2 +- SessionUtilitiesKit/General/Logging.swift | 139 +++++++++++++---- .../LibSession/LibSession.swift | 59 ++++++- SignalUtilitiesKit/Utilities/AppSetup.swift | 4 +- 245 files changed, 760 insertions(+), 300 deletions(-) rename Session.xcodeproj/xcshareddata/xcschemes/{SessionSnodeKit.xcscheme => SessionNetworkingKit.xcscheme} (91%) rename {SessionSnodeKit => SessionNetworkingKit}/Configuration.swift (89%) rename SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift => SessionNetworkingKit/Crypto/Crypto+SessionNetworkingKit.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_001_InitialSetupMigration.swift (96%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_002_SetupStandardJobs.swift (96%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_003_YDBToGRDBMigration.swift (88%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift (93%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift (97%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_006_DropSnodeCache.swift (94%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift (98%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Migrations/_008_ResetUserConfigLastHashes.swift (94%) rename {SessionSnodeKit => SessionNetworkingKit}/Database/Models/SnodeReceivedMessageInfo.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/LibSession/LibSession+Networking.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Meta/Info.plist (100%) create mode 100644 SessionNetworkingKit/Meta/SessionNetworkingKit.h rename {SessionSnodeKit => SessionNetworkingKit}/Models/AppVersionResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/DeleteAllBeforeRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/DeleteAllBeforeResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/DeleteAllMessagesRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/DeleteAllMessagesResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/DeleteMessagesRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/DeleteMessagesResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/FileUploadResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/GetExpiriesRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/GetExpiriesResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/GetMessagesRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/GetMessagesResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/GetNetworkTimestampResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/LegacyGetMessagesRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/LegacySendMessageRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/ONSResolveRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/ONSResolveResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/OxenDaemonRPCRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/RevokeSubaccountRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/RevokeSubaccountResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SendMessageRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SendMessageResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeAuthenticatedRequestBody.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeBatchRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeMessage.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeReceivedMessage.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeRecursiveResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/SnodeSwarmItem.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/UnrevokeSubaccountRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/UnrevokeSubaccountResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/UpdateExpiryAllRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/UpdateExpiryAllResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/UpdateExpiryRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Models/UpdateExpiryResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Networking/SnodeAPI.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SessionNetworkAPI/SessionNetworkAPI+Database.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SessionNetworkAPI/SessionNetworkAPI+Models.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SessionNetworkAPI/SessionNetworkAPI+Network.swift (96%) rename {SessionSnodeKit => SessionNetworkingKit}/SessionNetworkAPI/SessionNetworkAPI.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SnodeAPI/Request+SnodeAPI.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SnodeAPI/ResponseInfo+SnodeAPI.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SnodeAPI/SnodeAPI.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SnodeAPI/SnodeAPIEndpoint.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SnodeAPI/SnodeAPIError.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/SnodeAPI/SnodeAPINamespace.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/BatchRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/BatchResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/BencodeResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/ContentProxy.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/Destination.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/HTTPHeader.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/HTTPMethod.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/HTTPQueryParam.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/IPv4.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/JSON.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/Network.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/NetworkError.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/PreparedRequest+Sending.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/PreparedRequest.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/ProxiedContentDownloader.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/Request.swift (100%) create mode 100644 SessionNetworkingKit/Types/RequestCategory.swift rename {SessionSnodeKit => SessionNetworkingKit}/Types/ResponseInfo.swift (100%) create mode 100644 SessionNetworkingKit/Types/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift create mode 100644 SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Database.swift create mode 100644 SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Models.swift create mode 100644 SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Network.swift create mode 100644 SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI.swift rename {SessionSnodeKit => SessionNetworkingKit}/Types/SwarmDrainBehaviour.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/UpdatableTimestamp.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Types/ValidatableResponse.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Utilities/Data+Utilities.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Utilities/Publisher+Utilities.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Utilities/RetryWithDependencies.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Utilities/String+Trimming.swift (100%) rename {SessionSnodeKit => SessionNetworkingKit}/Utilities/URLResponse+Utilities.swift (100%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Models/FileUploadResponseSpec.swift (96%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Models/SnodeRequestSpec.swift (98%) rename SessionSnodeKitTests/SessionSnodeKit.xctestplan => SessionNetworkingKitTests/SessionNetworkingKit.xctestplan (90%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/BatchRequestSpec.swift (99%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/BatchResponseSpec.swift (99%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/BencodeResponseSpec.swift (99%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/DestinationSpec.swift (98%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/HeaderSpec.swift (93%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/PreparedRequestSendingSpec.swift (99%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/PreparedRequestSpec.swift (99%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/Types/RequestSpec.swift (99%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/_TestUtilities/CommonSSKMockExtensions.swift (96%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/_TestUtilities/MockNetwork.swift (99%) rename {SessionSnodeKitTests => SessionNetworkingKitTests}/_TestUtilities/MockSnodeAPICache.swift (97%) delete mode 100644 SessionSnodeKit/Meta/SessionSnodeKit.h delete mode 100644 SessionSnodeKit/Utilities/Threading+SSK.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 693291d1cf..f5e0730b6e 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -268,7 +268,7 @@ B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D0A25825E367AC00C1835E /* Notification+MessageReceiver.swift */; }; B8D0A26925E4A2C200C1835E /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D0A26825E4A2C200C1835E /* Onboarding.swift */; }; B8D64FBB25BA78310029CFC0 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; }; - B8D64FBD25BA78310029CFC0 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + B8D64FBD25BA78310029CFC0 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; B8D64FBE25BA78310029CFC0 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; B8D64FC725BA78520029CFC0 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; }; B8D64FCB25BA78A90029CFC0 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; }; @@ -286,7 +286,7 @@ C300A5F22554B09800555489 /* MessageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5F12554B09800555489 /* MessageSender.swift */; }; C300A60D2554B31900555489 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5CE2553860700C340D1 /* Logging.swift */; }; C302093E25DCBF08001F572D /* MentionSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C302093D25DCBF07001F572D /* MentionSelectionView.swift */; }; - C32824D325C9F9790062D0A7 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + C32824D325C9F9790062D0A7 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328250E25CA06020062D0A7 /* VoiceMessageView.swift */; }; C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328251E25CA3A900062D0A7 /* QuoteView.swift */; }; C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328252F25CA55360062D0A7 /* ContextMenuWindow.swift */; }; @@ -314,7 +314,7 @@ C331FFFE2558FF3B00070591 /* FullConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82AA238F669C00BA5194 /* FullConversationCell.swift */; }; C33FD9B3255A548A00E217F9 /* SignalUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C33FD9C2255A54EF00E217F9 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; }; - C33FD9C4255A54EF00E217F9 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + C33FD9C4255A54EF00E217F9 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; C33FD9C5255A54EF00E217F9 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */; }; C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */; }; @@ -329,7 +329,7 @@ C374EEF425DB31D40073A857 /* VoiceMessageRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C374EEF325DB31D40073A857 /* VoiceMessageRecordingView.swift */; }; C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C379DCF3256735770002D4EB /* VisibleMessage+Attachment.swift */; }; C37F5414255BAFA7002AEA92 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; }; - C37F54DC255BB84A002AEA92 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + C37F54DC255BB84A002AEA92 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; C38EF00C255B61CC007E1867 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; }; C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF240255B6D67007E1867 /* UIView+OWS.swift */; }; C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF241255B6D67007E1867 /* Collection+OWS.swift */; }; @@ -373,16 +373,15 @@ C3ADC66126426688005F1414 /* ShareNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareNavController.swift */; }; C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */; }; C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */; }; - C3C2A5A3255385C100C340D1 /* SessionSnodeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + C3C2A5A3255385C100C340D1 /* SessionNetworkingKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A5A1255385C100C340D1 /* SessionNetworkingKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C3C2A5A7255385C100C340D1 /* SessionNetworkingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B9255385ED00C340D1 /* Configuration.swift */; }; - C3C2A5E02553860B00C340D1 /* Threading+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D42553860A00C340D1 /* Threading+SSK.swift */; }; C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */; }; C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; C3C2A6F425539DE700C340D1 /* SessionMessagingKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A6F225539DE700C340D1 /* SessionMessagingKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; C3C2A6F825539DE700C340D1 /* SessionMessagingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C3C2A70B25539E1E00C340D1 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + C3C2A70B25539E1E00C340D1 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; C3C2A74425539EB700C340D1 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A74325539EB700C340D1 /* Message.swift */; }; C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A74C2553A39700C340D1 /* VisibleMessage.swift */; }; C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A75E2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift */; }; @@ -423,7 +422,7 @@ FD01504B2CA243CB005B08A1 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150472CA243CB005B08A1 /* Mock.swift */; }; FD01504C2CA243CB005B08A1 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150472CA243CB005B08A1 /* Mock.swift */; }; FD01504E2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD01504D2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift */; }; - FD0150502CA24468005B08A1 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; platformFilter = ios; }; + FD0150502CA24468005B08A1 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; platformFilter = ios; }; FD0150522CA2446D005B08A1 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = FD0150512CA2446D005B08A1 /* Quick */; }; FD0150542CA24471005B08A1 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD0150532CA24471005B08A1 /* Nimble */; }; FD0150582CA27DF3005B08A1 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */; }; @@ -741,7 +740,7 @@ FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */; }; FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FE2D77F9BE00041530 /* ScreenLock.swift */; }; FD6674002D77F9FD00041530 /* ScreenLockWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockWindow.swift */; }; - FD66CB2A2BF3449B00268FAB /* SessionSnodeKit.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */; }; + FD66CB2A2BF3449B00268FAB /* SessionNetworkingKit.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = FD66CB272BF3449B00268FAB /* SessionNetworkingKit.xctestplan */; }; FD6A38E92C2A630E00762359 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */; }; FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38EB2C2A63B500762359 /* KeychainSwift */; }; FD6A38EF2C2A641200762359 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38EE2C2A641200762359 /* DifferenceKit */; }; @@ -1058,7 +1057,7 @@ FDE754DE2C9BAF8A002A2623 /* Crypto+SessionUtilitiesKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754D82C9BAF89002A2623 /* Crypto+SessionUtilitiesKit.swift */; }; FDE754DF2C9BAF8A002A2623 /* KeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754D92C9BAF89002A2623 /* KeyPair.swift */; }; FDE754E02C9BAF8A002A2623 /* Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754DA2C9BAF8A002A2623 /* Hex.swift */; }; - FDE754E32C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E12C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift */; }; + FDE754E32C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E12C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift */; }; FDE754E52C9BB012002A2623 /* BezierPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E42C9BB012002A2623 /* BezierPathView.swift */; }; FDE754E72C9BB051002A2623 /* OWSViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E62C9BB051002A2623 /* OWSViewController.swift */; }; FDE754F02C9BB08B002A2623 /* Crypto+Attachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754EC2C9BB08B002A2623 /* Crypto+Attachments.swift */; }; @@ -1359,7 +1358,7 @@ files = ( C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */, C33FD9B3255A548A00E217F9 /* SignalUtilitiesKit.framework in Embed Frameworks */, - C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */, + C3C2A5A7255385C100C340D1 /* SessionNetworkingKit.framework in Embed Frameworks */, C331FF232558F9D300070591 /* SessionUIKit.framework in Embed Frameworks */, C3C2A6F825539DE700C340D1 /* SessionMessagingKit.framework in Embed Frameworks */, ); @@ -1743,13 +1742,12 @@ C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C3ADC66026426688005F1414 /* ShareNavController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareNavController.swift; sourceTree = ""; }; C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+BigEndian.swift"; sourceTree = ""; }; - C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionSnodeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionSnodeKit.h; sourceTree = ""; }; + C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionNetworkingKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C3C2A5A1255385C100C340D1 /* SessionNetworkingKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionNetworkingKit.h; sourceTree = ""; }; C3C2A5A2255385C100C340D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C3C2A5B9255385ED00C340D1 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; C3C2A5CE2553860700C340D1 /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; C3C2A5D22553860900C340D1 /* String+Trimming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Trimming.swift"; sourceTree = ""; }; - C3C2A5D42553860A00C340D1 /* Threading+SSK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Threading+SSK.swift"; sourceTree = ""; }; C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Utilities.swift"; sourceTree = ""; }; C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2047,7 +2045,7 @@ FD6531892AA025C500DFEEAA /* TestDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDependencies.swift; sourceTree = ""; }; FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = ""; }; FD6673FE2D77F9BE00041530 /* ScreenLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLock.swift; sourceTree = ""; }; - FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SessionSnodeKit.xctestplan; sourceTree = ""; }; + FD66CB272BF3449B00268FAB /* SessionNetworkingKit.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SessionNetworkingKit.xctestplan; sourceTree = ""; }; FD66CB2B2BF344C600268FAB /* SessionMessageKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SessionMessageKit.xctestplan; sourceTree = ""; }; FD6A38F02C2A66B100762359 /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = ""; }; FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; @@ -2210,7 +2208,7 @@ FDB5DAE52A95D8B0002C8721 /* GroupUpdateDeleteMemberContentMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateDeleteMemberContentMessage.swift; sourceTree = ""; }; FDB5DAE72A95D96C002C8721 /* MessageReceiver+Groups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+Groups.swift"; sourceTree = ""; }; FDB5DAF22A96DD4F002C8721 /* PreparedRequest+Sending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreparedRequest+Sending.swift"; sourceTree = ""; }; - FDB5DAFA2A981C42002C8721 /* SessionSnodeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionSnodeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + FDB5DAFA2A981C42002C8721 /* SessionNetworkingKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionNetworkingKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; FDB5DB052A981C67002C8721 /* PreparedRequestSendingSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreparedRequestSendingSpec.swift; sourceTree = ""; }; FDB6A87B2AD75B7F002D4F96 /* PhotosUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PhotosUI.framework; path = System/Library/Frameworks/PhotosUI.framework; sourceTree = SDKROOT; }; FDB7400A28EB99A70094D718 /* TimeInterval+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Utilities.swift"; sourceTree = ""; }; @@ -2326,7 +2324,7 @@ FDE754D82C9BAF89002A2623 /* Crypto+SessionUtilitiesKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionUtilitiesKit.swift"; sourceTree = ""; }; FDE754D92C9BAF89002A2623 /* KeyPair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPair.swift; sourceTree = ""; }; FDE754DA2C9BAF8A002A2623 /* Hex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hex.swift; sourceTree = ""; }; - FDE754E12C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionSnodeKit.swift"; sourceTree = ""; }; + FDE754E12C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionNetworkingKit.swift"; sourceTree = ""; }; FDE754E42C9BB012002A2623 /* BezierPathView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BezierPathView.swift; sourceTree = ""; }; FDE754E62C9BB051002A2623 /* OWSViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSViewController.swift; sourceTree = ""; }; FDE754EC2C9BB08B002A2623 /* Crypto+Attachments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+Attachments.swift"; sourceTree = ""; }; @@ -2438,7 +2436,7 @@ buildActionMask = 2147483647; files = ( FD860CC92D6ED2ED00BBE29C /* DifferenceKit in Frameworks */, - C32824D325C9F9790062D0A7 /* SessionSnodeKit.framework in Frameworks */, + C32824D325C9F9790062D0A7 /* SessionNetworkingKit.framework in Frameworks */, B8D64FC725BA78520029CFC0 /* SessionMessagingKit.framework in Frameworks */, C3D90A5C25773A25002C9DF5 /* SessionUtilitiesKit.framework in Frameworks */, C3402FE52559036600EA6424 /* SessionUIKit.framework in Frameworks */, @@ -2452,7 +2450,7 @@ files = ( B8D64FBB25BA78310029CFC0 /* SessionMessagingKit.framework in Frameworks */, FD8A5B182DBF47E9004C689B /* SessionUIKit.framework in Frameworks */, - B8D64FBD25BA78310029CFC0 /* SessionSnodeKit.framework in Frameworks */, + B8D64FBD25BA78310029CFC0 /* SessionNetworkingKit.framework in Frameworks */, B8D64FBE25BA78310029CFC0 /* SessionUtilitiesKit.framework in Frameworks */, C38EF00C255B61CC007E1867 /* SignalUtilitiesKit.framework in Frameworks */, ); @@ -2475,7 +2473,7 @@ FD6A39222C2AA91D00762359 /* NVActivityIndicatorView in Frameworks */, FD22866F2C38D42300BC06F7 /* DifferenceKit in Frameworks */, C33FD9C2255A54EF00E217F9 /* SessionMessagingKit.framework in Frameworks */, - C33FD9C4255A54EF00E217F9 /* SessionSnodeKit.framework in Frameworks */, + C33FD9C4255A54EF00E217F9 /* SessionNetworkingKit.framework in Frameworks */, C33FD9C5255A54EF00E217F9 /* SessionUtilitiesKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2510,7 +2508,7 @@ FD6673FA2D7021F800041530 /* SessionUtil in Frameworks */, FD2286732C38D43900BC06F7 /* DifferenceKit in Frameworks */, FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */, - C3C2A70B25539E1E00C340D1 /* SessionSnodeKit.framework in Frameworks */, + C3C2A70B25539E1E00C340D1 /* SessionNetworkingKit.framework in Frameworks */, FD6A39132C2A946A00762359 /* SwiftProtobuf in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2524,7 +2522,7 @@ B8FF8DAE25C0D00F004D1F22 /* SessionMessagingKit.framework in Frameworks */, B8FF8DAF25C0D00F004D1F22 /* SessionUtilitiesKit.framework in Frameworks */, FDB6A87C2AD75B7F002D4F96 /* PhotosUI.framework in Frameworks */, - C37F54DC255BB84A002AEA92 /* SessionSnodeKit.framework in Frameworks */, + C37F54DC255BB84A002AEA92 /* SessionNetworkingKit.framework in Frameworks */, C37F5414255BAFA7002AEA92 /* SignalUtilitiesKit.framework in Frameworks */, 455A16DD1F1FEA0000F86704 /* Metal.framework in Frameworks */, 455A16DE1F1FEA0000F86704 /* MetalKit.framework in Frameworks */, @@ -2563,7 +2561,7 @@ files = ( FD0150542CA24471005B08A1 /* Nimble in Frameworks */, FD0150522CA2446D005B08A1 /* Quick in Frameworks */, - FD0150502CA24468005B08A1 /* SessionSnodeKit.framework in Frameworks */, + FD0150502CA24468005B08A1 /* SessionNetworkingKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3669,27 +3667,27 @@ path = Utilities; sourceTree = ""; }; - C3C2A5A0255385C100C340D1 /* SessionSnodeKit */ = { + C3C2A5A0255385C100C340D1 /* SessionNetworkingKit */ = { isa = PBXGroup; children = ( - 947D7FD32D509FC900E8E413 /* SessionNetworkAPI */, C3C2A5B0255385C700C340D1 /* Meta */, FDE754E22C9BAFF4002A2623 /* Crypto */, FD17D79D27F40CAA00122BE0 /* Database */, + FD7F74682BAB8A5D006DDFD8 /* LibSession */, FDF8489929405C5A007DCAE5 /* Models */, - FDF8488F29405C13007DCAE5 /* Types */, + 947D7FD32D509FC900E8E413 /* SessionNetworkAPI */, FD2272842C33E28D004D8A6C /* SnodeAPI */, - FD7F74682BAB8A5D006DDFD8 /* LibSession */, + FDF8488F29405C13007DCAE5 /* Types */, C3C2A5CD255385F300C340D1 /* Utilities */, C3C2A5B9255385ED00C340D1 /* Configuration.swift */, ); - path = SessionSnodeKit; + path = SessionNetworkingKit; sourceTree = ""; }; C3C2A5B0255385C700C340D1 /* Meta */ = { isa = PBXGroup; children = ( - C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */, + C3C2A5A1255385C100C340D1 /* SessionNetworkingKit.h */, C3C2A5A2255385C100C340D1 /* Info.plist */, ); path = Meta; @@ -3702,7 +3700,6 @@ FD2272BD2C34B710004D8A6C /* Publisher+Utilities.swift */, FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */, C3C2A5D22553860900C340D1 /* String+Trimming.swift */, - C3C2A5D42553860A00C340D1 /* Threading+SSK.swift */, FD2272A82C33E337004D8A6C /* URLResponse+Utilities.swift */, ); path = Utilities; @@ -3859,13 +3856,13 @@ 7BC01A3C241F40AB00BC7C55 /* SessionNotificationServiceExtension */, C331FF1C2558F9D300070591 /* SessionUIKit */, C3C2A6F125539DE700C340D1 /* SessionMessagingKit */, - C3C2A5A0255385C100C340D1 /* SessionSnodeKit */, + C3C2A5A0255385C100C340D1 /* SessionNetworkingKit */, C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */, C33FD9AC255A548A00E217F9 /* SignalUtilitiesKit */, FD83B9BC27CF2215005E1583 /* _SharedTestUtilities */, FD71160A28D00BAE00B47552 /* SessionTests */, FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */, - FDB5DAFB2A981C43002C8721 /* SessionSnodeKitTests */, + FDB5DAFB2A981C43002C8721 /* SessionNetworkingKitTests */, FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */, FDE7214E287E50D50093DF33 /* Scripts */, D221A08C169C9E5E00537ABF /* Frameworks */, @@ -3880,7 +3877,7 @@ D221A089169C9E5E00537ABF /* Session.app */, 453518681FC635DD00210559 /* SessionShareExtension.appex */, 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */, - C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */, + C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */, C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */, C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */, C331FF1B2558F9D300070591 /* SessionUIKit.framework */, @@ -3888,7 +3885,7 @@ FDC4388E27B9FFC700C60D73 /* SessionMessagingKitTests.xctest */, FD83B9AF27CF200A005E1583 /* SessionUtilitiesKitTests.xctest */, FD71160928D00BAE00B47552 /* SessionTests.xctest */, - FDB5DAFA2A981C42002C8721 /* SessionSnodeKitTests.xctest */, + FDB5DAFA2A981C42002C8721 /* SessionNetworkingKitTests.xctest */, ); name = Products; sourceTree = ""; @@ -4751,15 +4748,15 @@ path = "Group Update Messages"; sourceTree = ""; }; - FDB5DAFB2A981C43002C8721 /* SessionSnodeKitTests */ = { + FDB5DAFB2A981C43002C8721 /* SessionNetworkingKitTests */ = { isa = PBXGroup; children = ( - FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */, + FD66CB272BF3449B00268FAB /* SessionNetworkingKit.xctestplan */, FD3765DD2AD8F02300DC1489 /* _TestUtilities */, FDAA16792AC28E2200DDBF77 /* Models */, FD2272C52C34E9D1004D8A6C /* Types */, ); - path = SessionSnodeKitTests; + path = SessionNetworkingKitTests; sourceTree = ""; }; FDC13D4E2A16EE41007267C7 /* Types */ = { @@ -4965,7 +4962,7 @@ FDE754E22C9BAFF4002A2623 /* Crypto */ = { isa = PBXGroup; children = ( - FDE754E12C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift */, + FDE754E12C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift */, ); path = Crypto; sourceTree = ""; @@ -5149,7 +5146,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - C3C2A5A3255385C100C340D1 /* SessionSnodeKit.h in Headers */, + C3C2A5A3255385C100C340D1 /* SessionNetworkingKit.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5270,9 +5267,9 @@ productReference = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; productType = "com.apple.product-type.framework"; }; - C3C2A59E255385C100C340D1 /* SessionSnodeKit */ = { + C3C2A59E255385C100C340D1 /* SessionNetworkingKit */ = { isa = PBXNativeTarget; - buildConfigurationList = C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionSnodeKit" */; + buildConfigurationList = C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionNetworkingKit" */; buildPhases = ( C3C2A59A255385C100C340D1 /* Headers */, C3C2A59B255385C100C340D1 /* Sources */, @@ -5285,12 +5282,12 @@ FDB348822BE86A4400B716C2 /* PBXTargetDependency */, FD7F74622BAAA4C7006DDFD8 /* PBXTargetDependency */, ); - name = SessionSnodeKit; + name = SessionNetworkingKit; packageProductDependencies = ( FD6673F72D7021F200041530 /* SessionUtil */, ); productName = SessionSnodeKit; - productReference = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; + productReference = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; productType = "com.apple.product-type.framework"; }; C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */ = { @@ -5428,9 +5425,9 @@ productReference = FD83B9AF27CF200A005E1583 /* SessionUtilitiesKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - FDB5DAF92A981C42002C8721 /* SessionSnodeKitTests */ = { + FDB5DAF92A981C42002C8721 /* SessionNetworkingKitTests */ = { isa = PBXNativeTarget; - buildConfigurationList = FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionSnodeKitTests" */; + buildConfigurationList = FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionNetworkingKitTests" */; buildPhases = ( FDB5DAF62A981C42002C8721 /* Sources */, FD01504F2CA2445E005B08A1 /* Frameworks */, @@ -5441,9 +5438,9 @@ dependencies = ( FDB5DB002A981C43002C8721 /* PBXTargetDependency */, ); - name = SessionSnodeKitTests; + name = SessionNetworkingKitTests; productName = SessionSnodeKitTests; - productReference = FDB5DAFA2A981C42002C8721 /* SessionSnodeKitTests.xctest */; + productReference = FDB5DAFA2A981C42002C8721 /* SessionNetworkingKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; FDC4388D27B9FFC700C60D73 /* SessionMessagingKitTests */ = { @@ -5614,11 +5611,11 @@ C33FD9AA255A548A00E217F9 /* SignalUtilitiesKit */, C331FF1A2558F9D300070591 /* SessionUIKit */, C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */, - C3C2A59E255385C100C340D1 /* SessionSnodeKit */, + C3C2A59E255385C100C340D1 /* SessionNetworkingKit */, C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */, FD71160828D00BAE00B47552 /* SessionTests */, FDC4388D27B9FFC700C60D73 /* SessionMessagingKitTests */, - FDB5DAF92A981C42002C8721 /* SessionSnodeKitTests */, + FDB5DAF92A981C42002C8721 /* SessionNetworkingKitTests */, FD83B9AE27CF200A005E1583 /* SessionUtilitiesKitTests */, ); }; @@ -5763,7 +5760,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - FD66CB2A2BF3449B00268FAB /* SessionSnodeKit.xctestplan in Resources */, + FD66CB2A2BF3449B00268FAB /* SessionNetworkingKit.xctestplan in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6205,7 +6202,6 @@ FD2272AB2C33E337004D8A6C /* ValidatableResponse.swift in Sources */, FD2272B72C33E337004D8A6C /* Request.swift in Sources */, FD2272B92C33E337004D8A6C /* ResponseInfo.swift in Sources */, - C3C2A5E02553860B00C340D1 /* Threading+SSK.swift in Sources */, FDF848D729405C5B007DCAE5 /* SnodeBatchRequest.swift in Sources */, FDF848CE29405C5B007DCAE5 /* UpdateExpiryAllRequest.swift in Sources */, FDF848C229405C5A007DCAE5 /* OxenDaemonRPCRequest.swift in Sources */, @@ -6227,7 +6223,7 @@ FD5E93D12C100FD70038C25A /* FileUploadResponse.swift in Sources */, FDF848D629405C5B007DCAE5 /* SnodeMessage.swift in Sources */, FD02CC162C3681EF009AB976 /* RevokeSubaccountResponse.swift in Sources */, - FDE754E32C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift in Sources */, + FDE754E32C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift in Sources */, FD2272AD2C33E337004D8A6C /* Network.swift in Sources */, FD2272B32C33E337004D8A6C /* BatchRequest.swift in Sources */, FDF848D129405C5B007DCAE5 /* SnodeSwarmItem.swift in Sources */, @@ -7125,7 +7121,7 @@ }; B8D64FB825BA78270029CFC0 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */; + target = C3C2A59E255385C100C340D1 /* SessionNetworkingKit */; targetProxy = B8D64FB725BA78270029CFC0 /* PBXContainerItemProxy */; }; B8D64FBA25BA78270029CFC0 /* PBXTargetDependency */ = { @@ -7140,7 +7136,7 @@ }; B8D64FC425BA784A0029CFC0 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */; + target = C3C2A59E255385C100C340D1 /* SessionNetworkingKit */; targetProxy = B8D64FC325BA784A0029CFC0 /* PBXContainerItemProxy */; }; B8D64FC625BA784A0029CFC0 /* PBXTargetDependency */ = { @@ -7160,7 +7156,7 @@ }; C3C2A5A5255385C100C340D1 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */; + target = C3C2A59E255385C100C340D1 /* SessionNetworkingKit */; targetProxy = C3C2A5A4255385C100C340D1 /* PBXContainerItemProxy */; }; C3C2A67F255388CC00C340D1 /* PBXTargetDependency */ = { @@ -7222,7 +7218,7 @@ FDB5DB002A981C43002C8721 /* PBXTargetDependency */ = { isa = PBXTargetDependency; platformFilter = ios; - target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */; + target = C3C2A59E255385C100C340D1 /* SessionNetworkingKit */; targetProxy = FDB5DAFF2A981C43002C8721 /* PBXContainerItemProxy */; }; FDC4389427B9FFC700C60D73 /* PBXTargetDependency */ = { @@ -7777,7 +7773,7 @@ GCC_DYNAMIC_NO_PIC = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - INFOPLIST_FILE = SessionSnodeKit/Meta/Info.plist; + INFOPLIST_FILE = SessionNetworkingKit/Meta/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -7788,7 +7784,7 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit"; + PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; @@ -7850,7 +7846,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - INFOPLIST_FILE = SessionSnodeKit/Meta/Info.plist; + INFOPLIST_FILE = SessionNetworkingKit/Meta/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -7861,7 +7857,7 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit"; + PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -8386,8 +8382,8 @@ ENABLE_MODULE_VERIFIER = NO; GCC_DYNAMIC_NO_PIC = NO; GENERATE_INFOPLIST_FILE = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; - PRODUCT_NAME = SessionSnodeKitTests; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionNetworkingKitTests; + PRODUCT_NAME = SessionNetworkingKitTests; }; name = Debug; }; @@ -8396,8 +8392,8 @@ buildSettings = { ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; - PRODUCT_NAME = SessionSnodeKitTests; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionNetworkingKitTests; + PRODUCT_NAME = SessionNetworkingKitTests; }; name = App_Store_Release; }; @@ -8585,8 +8581,8 @@ isa = XCBuildConfiguration; buildSettings = { GENERATE_INFOPLIST_FILE = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; - PRODUCT_NAME = SessionSnodeKitTests; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionNetworkingKitTests; + PRODUCT_NAME = SessionNetworkingKitTests; }; name = Debug_Compile_LibSession; }; @@ -8594,8 +8590,8 @@ isa = XCBuildConfiguration; buildSettings = { GENERATE_INFOPLIST_FILE = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; - PRODUCT_NAME = SessionSnodeKitTests; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionNetworkingKitTests; + PRODUCT_NAME = SessionNetworkingKitTests; }; name = App_Store_Release_Compile_LibSession; }; @@ -9107,7 +9103,7 @@ GCC_DYNAMIC_NO_PIC = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - INFOPLIST_FILE = SessionSnodeKit/Meta/Info.plist; + INFOPLIST_FILE = SessionNetworkingKit/Meta/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -9118,7 +9114,7 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit"; + PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; @@ -9826,7 +9822,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - INFOPLIST_FILE = SessionSnodeKit/Meta/Info.plist; + INFOPLIST_FILE = SessionNetworkingKit/Meta/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -9837,7 +9833,7 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit"; + PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -10143,7 +10139,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = App_Store_Release; }; - C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionSnodeKit" */ = { + C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionNetworkingKit" */ = { isa = XCConfigurationList; buildConfigurations = ( C3C2A5A8255385C100C340D1 /* Debug */, @@ -10198,7 +10194,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = App_Store_Release; }; - FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionSnodeKitTests" */ = { + FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionNetworkingKitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( FD2272502C32910F004D8A6C /* Debug */, diff --git a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme index ad1576e055..e20af5e4f5 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme @@ -77,8 +77,8 @@ @@ -55,8 +55,8 @@ diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index d747fbcdfd..f08656626c 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -9,7 +9,7 @@ import SessionUIKit import SignalUtilitiesKit import SessionMessagingKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { private let dependencies: Dependencies diff --git a/Session/Calls/WebRTC/WebRTCSession.swift b/Session/Calls/WebRTC/WebRTCSession.swift index 99e48d1e25..d579d8a1a5 100644 --- a/Session/Calls/WebRTC/WebRTCSession.swift +++ b/Session/Calls/WebRTC/WebRTCSession.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import WebRTC -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Closed Groups/EditGroupViewModel.swift b/Session/Closed Groups/EditGroupViewModel.swift index 01916f533c..8309d70282 100644 --- a/Session/Closed Groups/EditGroupViewModel.swift +++ b/Session/Closed Groups/EditGroupViewModel.swift @@ -5,7 +5,7 @@ import Combine import GRDB import DifferenceKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit diff --git a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift index 54ae63cbc3..de93e57637 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift @@ -3,7 +3,7 @@ import UIKit import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit extension ContextMenuVC { final class ActionView: UIView { diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 00c27d060f..e8a457990a 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -14,7 +14,7 @@ import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit import SwiftUI -import SessionSnodeKit +import SessionNetworkingKit extension ConversationVC: InputViewDelegate, diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 3c8ede79ed..fe501f887c 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -6,7 +6,7 @@ import UniformTypeIdentifiers import Lucide import GRDB import DifferenceKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit import SessionUIKit diff --git a/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift b/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift index b0f17cc31e..b835fe5e86 100644 --- a/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift +++ b/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift @@ -1,7 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import UIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit class DisappearingMessageTimerView: UIView { diff --git a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift index b7df60ec3c..7ef4514e03 100644 --- a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift @@ -7,7 +7,7 @@ import DifferenceKit import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { typealias TableItem = String diff --git a/Session/Conversations/Settings/ThreadNotificationSettingsViewModel.swift b/Session/Conversations/Settings/ThreadNotificationSettingsViewModel.swift index e89a98303b..f5b51cf455 100644 --- a/Session/Conversations/Settings/ThreadNotificationSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadNotificationSettingsViewModel.swift @@ -7,7 +7,7 @@ import DifferenceKit import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit class ThreadNotificationSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { public let dependencies: Dependencies diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index c2462e2193..5045825fb5 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -9,7 +9,7 @@ import SessionUIKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { public let dependencies: Dependencies diff --git a/Session/Home/New Conversation/NewMessageScreen.swift b/Session/Home/New Conversation/NewMessageScreen.swift index c478eb8331..725fa06ed8 100644 --- a/Session/Home/New Conversation/NewMessageScreen.swift +++ b/Session/Home/New Conversation/NewMessageScreen.swift @@ -5,7 +5,7 @@ import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit struct NewMessageScreen: View { @EnvironmentObject var host: HostWrapper diff --git a/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift b/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift index e3bf699035..81bf19c1c7 100644 --- a/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift +++ b/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift @@ -4,7 +4,7 @@ import UIKit import Combine import UniformTypeIdentifiers import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SignalUtilitiesKit import SessionUtilitiesKit diff --git a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift index 2e9b72936b..eb2cd33430 100644 --- a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift +++ b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift @@ -5,7 +5,7 @@ import Combine import CoreServices import UniformTypeIdentifiers import SignalUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton diff --git a/Session/Media Viewing & Editing/MediaPageViewController.swift b/Session/Media Viewing & Editing/MediaPageViewController.swift index 51523207fb..f00db791d2 100644 --- a/Session/Media Viewing & Editing/MediaPageViewController.swift +++ b/Session/Media Viewing & Editing/MediaPageViewController.swift @@ -6,7 +6,7 @@ import SessionUIKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, MediaDetailViewControllerDelegate, InteractivelyDismissableViewController { class DynamicallySizedView: UIView { diff --git a/Session/Media Viewing & Editing/MessageInfoScreen.swift b/Session/Media Viewing & Editing/MessageInfoScreen.swift index e614b81b5f..f4c447dd46 100644 --- a/Session/Media Viewing & Editing/MessageInfoScreen.swift +++ b/Session/Media Viewing & Editing/MessageInfoScreen.swift @@ -2,7 +2,7 @@ import SwiftUI import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import SessionMessagingKit diff --git a/Session/Media Viewing & Editing/PhotoCapture.swift b/Session/Media Viewing & Editing/PhotoCapture.swift index d4e1bc8d2d..30b38d6a73 100644 --- a/Session/Media Viewing & Editing/PhotoCapture.swift +++ b/Session/Media Viewing & Editing/PhotoCapture.swift @@ -6,7 +6,7 @@ import Foundation import Combine import AVFoundation import CoreServices -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 6299100d40..1dd66f07f8 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -8,7 +8,7 @@ import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/Session/Meta/Session+SNUIKit.swift b/Session/Meta/Session+SNUIKit.swift index 862f7a9eb3..2c7a2aedfe 100644 --- a/Session/Meta/Session+SNUIKit.swift +++ b/Session/Meta/Session+SNUIKit.swift @@ -3,7 +3,7 @@ import UIKit import AVFoundation import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - SessionSNUIKitConfig diff --git a/Session/Notifications/NotificationActionHandler.swift b/Session/Notifications/NotificationActionHandler.swift index 5c0999824b..083aa94356 100644 --- a/Session/Notifications/NotificationActionHandler.swift +++ b/Session/Notifications/NotificationActionHandler.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SignalUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Notifications/SyncPushTokensJob.swift b/Session/Notifications/SyncPushTokensJob.swift index 6a885dfd2c..7cf9efb5a4 100644 --- a/Session/Notifications/SyncPushTokensJob.swift +++ b/Session/Notifications/SyncPushTokensJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Onboarding/LoadingScreen.swift b/Session/Onboarding/LoadingScreen.swift index 4fffd9fe46..b731acafbd 100644 --- a/Session/Onboarding/LoadingScreen.swift +++ b/Session/Onboarding/LoadingScreen.swift @@ -3,7 +3,7 @@ import SwiftUI import Combine import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 52da3682fa..4ca9d056dd 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -5,7 +5,7 @@ import Combine import GRDB import SessionUtilitiesKit import SessionMessagingKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Cache diff --git a/Session/Onboarding/PNModeScreen.swift b/Session/Onboarding/PNModeScreen.swift index d4976596a1..40100bf54e 100644 --- a/Session/Onboarding/PNModeScreen.swift +++ b/Session/Onboarding/PNModeScreen.swift @@ -3,7 +3,7 @@ import SwiftUI import Combine import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SignalUtilitiesKit import SessionUtilitiesKit diff --git a/Session/Path/PathStatusView.swift b/Session/Path/PathStatusView.swift index 907947605f..33108de585 100644 --- a/Session/Path/PathStatusView.swift +++ b/Session/Path/PathStatusView.swift @@ -3,7 +3,7 @@ import UIKit import Combine import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Path/PathVC.swift b/Session/Path/PathVC.swift index ab37d115f3..130224d297 100644 --- a/Session/Path/PathVC.swift +++ b/Session/Path/PathVC.swift @@ -5,7 +5,7 @@ import Combine import NVActivityIndicatorView import SessionMessagingKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit final class PathVC: BaseVC { diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index e61c5570c0..d007a2097e 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -8,7 +8,7 @@ import Compression import GRDB import DifferenceKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index 4ade2b621d..d4162bb02c 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -4,7 +4,7 @@ import UIKit import Combine import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit diff --git a/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift b/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift index 2446f852f2..d7795ea74f 100644 --- a/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift +++ b/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift @@ -5,7 +5,7 @@ import SwiftUI import Combine import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import SessionMessagingKit diff --git a/Session/Utilities/BackgroundPoller.swift b/Session/Utilities/BackgroundPoller.swift index bb30cb2599..b03942a719 100644 --- a/Session/Utilities/BackgroundPoller.swift +++ b/Session/Utilities/BackgroundPoller.swift @@ -5,7 +5,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Utilities/IP2Country.swift b/Session/Utilities/IP2Country.swift index 8ef98ce322..56b9102bba 100644 --- a/Session/Utilities/IP2Country.swift +++ b/Session/Utilities/IP2Country.swift @@ -5,7 +5,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Cache diff --git a/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift b/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift index bea6331051..8621e3a2fa 100644 --- a/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift +++ b/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift @@ -4,7 +4,7 @@ import Foundation import CryptoKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtil import SessionUtilitiesKit diff --git a/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift index bfcdbea5d3..b9b3e31588 100644 --- a/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` diff --git a/SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift b/SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift index 07db8962d8..c5c4c4a4d8 100644 --- a/SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift +++ b/SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit /// This migration used to remove the legacy YapDatabase files (the old logic has been removed and is no longer supported so it now does nothing) enum _004_RemoveLegacyYDB: Migration { diff --git a/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift b/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift index 11894d2d79..e6902e104c 100644 --- a/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift @@ -5,7 +5,7 @@ import Foundation import UIKit.UIImage import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit enum _022_GroupsRebuildChanges: Migration { diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index 601023baf3..4d9cd19e81 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -7,7 +7,7 @@ import Combine import UniformTypeIdentifiers import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUIKit public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index e525398357..b2a680ed7a 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -5,7 +5,7 @@ import Combine import GRDB import DifferenceKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public struct ClosedGroup: Codable, Equatable, Hashable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { diff --git a/SessionMessagingKit/Database/Models/ConfigDump.swift b/SessionMessagingKit/Database/Models/ConfigDump.swift index b264051429..0b429793ad 100644 --- a/SessionMessagingKit/Database/Models/ConfigDump.swift +++ b/SessionMessagingKit/Database/Models/ConfigDump.swift @@ -4,7 +4,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public struct ConfigDump: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { diff --git a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift index 22c7d9a580..d9f804ded0 100644 --- a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift +++ b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift @@ -5,7 +5,7 @@ import GRDB import SessionUIKit import SessionUtil import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public struct DisappearingMessagesConfiguration: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "disappearingMessagesConfiguration" } diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index 59373a3201..47590f8d3a 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public struct Interaction: Codable, Identifiable, Equatable, Hashable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "interaction" } diff --git a/SessionMessagingKit/Database/Models/LinkPreview.swift b/SessionMessagingKit/Database/Models/LinkPreview.swift index 51bfb22b48..0c8cada73f 100644 --- a/SessionMessagingKit/Database/Models/LinkPreview.swift +++ b/SessionMessagingKit/Database/Models/LinkPreview.swift @@ -7,7 +7,7 @@ import Combine import UniformTypeIdentifiers import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public struct LinkPreview: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "linkPreview" } diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 3e069e9268..1684d94c27 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public struct SessionThread: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, IdentifiableTableRecord { public static var databaseTableName: String { "thread" } diff --git a/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift b/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift index 0127ebc746..b52dbe95d5 100644 --- a/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift +++ b/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public enum AttachmentDownloadJob: JobExecutor { public static var maxFailureCount: Int = 3 diff --git a/SessionMessagingKit/Jobs/AttachmentUploadJob.swift b/SessionMessagingKit/Jobs/AttachmentUploadJob.swift index d1dc85ddea..a3cd20c22a 100644 --- a/SessionMessagingKit/Jobs/AttachmentUploadJob.swift +++ b/SessionMessagingKit/Jobs/AttachmentUploadJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift b/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift index aba703204e..45022e0421 100644 --- a/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift +++ b/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift @@ -2,7 +2,7 @@ import Foundation import Combine -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift b/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift index 5720b9acfb..f0ba83de12 100644 --- a/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift +++ b/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift index dc37ac0c02..136b736815 100644 --- a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift +++ b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift b/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift index 0f7e16f812..31a09b3b8a 100644 --- a/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift +++ b/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift index ec9ee87bc3..c5257be82b 100644 --- a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift +++ b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift @@ -6,7 +6,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift b/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift index 0ffd051f5a..03e602be04 100644 --- a/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift +++ b/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public enum ExpirationUpdateJob: JobExecutor { public static var maxFailureCount: Int = -1 diff --git a/SessionMessagingKit/Jobs/GarbageCollectionJob.swift b/SessionMessagingKit/Jobs/GarbageCollectionJob.swift index b799931f85..2d2b0f87ce 100644 --- a/SessionMessagingKit/Jobs/GarbageCollectionJob.swift +++ b/SessionMessagingKit/Jobs/GarbageCollectionJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/GetExpirationJob.swift b/SessionMessagingKit/Jobs/GetExpirationJob.swift index bf15ef97b7..a70bfc5d0a 100644 --- a/SessionMessagingKit/Jobs/GetExpirationJob.swift +++ b/SessionMessagingKit/Jobs/GetExpirationJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public enum GetExpirationJob: JobExecutor { diff --git a/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift b/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift index b40827c6e8..e652f75f13 100644 --- a/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift +++ b/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift @@ -5,7 +5,7 @@ import Combine import GRDB import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/GroupLeavingJob.swift b/SessionMessagingKit/Jobs/GroupLeavingJob.swift index 475a5ea13d..92448ad51b 100644 --- a/SessionMessagingKit/Jobs/GroupLeavingJob.swift +++ b/SessionMessagingKit/Jobs/GroupLeavingJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift b/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift index 10a934451b..d46639ac78 100644 --- a/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift +++ b/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift @@ -5,7 +5,7 @@ import Combine import GRDB import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/MessageSendJob.swift b/SessionMessagingKit/Jobs/MessageSendJob.swift index 590a660a46..49948e7bac 100644 --- a/SessionMessagingKit/Jobs/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/MessageSendJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift b/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift index 3decb55cf9..096d6729ff 100644 --- a/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift +++ b/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift @@ -6,7 +6,7 @@ import GRDB import SessionUtil import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift b/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift index 1da0c5a95c..9196301bfc 100644 --- a/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift +++ b/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public enum SendReadReceiptsJob: JobExecutor { diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift index ee77869c1e..eec8fbd631 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtil -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Size Restrictions diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift index 51215e9344..8774ca003d 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtil -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Size Restrictions diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift index 522957def1..ce4d1986dd 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift @@ -3,7 +3,7 @@ import UIKit import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtil import SessionUtilitiesKit diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift index 54908b4470..64a1f5260a 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift @@ -3,7 +3,7 @@ import UIKit import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtil import SessionUtilitiesKit diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift index 27f7bf7fdf..c9724f15b1 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift @@ -4,7 +4,7 @@ import Foundation import GRDB import SessionUtil import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Size Restrictions diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 8bbd82e93b..217af7d5ae 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUIKit import SessionUtil import SessionUtilitiesKit diff --git a/SessionMessagingKit/LibSession/Types/Config.swift b/SessionMessagingKit/LibSession/Types/Config.swift index 52c8057ef5..7d503d57f0 100644 --- a/SessionMessagingKit/LibSession/Types/Config.swift +++ b/SessionMessagingKit/LibSession/Types/Config.swift @@ -4,7 +4,7 @@ import Foundation import SessionUtil -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension LibSession { diff --git a/SessionMessagingKit/Messages/Message+Destination.swift b/SessionMessagingKit/Messages/Message+Destination.swift index b7d393a02c..e6e7a98934 100644 --- a/SessionMessagingKit/Messages/Message+Destination.swift +++ b/SessionMessagingKit/Messages/Message+Destination.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension Message { diff --git a/SessionMessagingKit/Messages/Message+DisappearingMessages.swift b/SessionMessagingKit/Messages/Message+DisappearingMessages.swift index d511a374fc..4212f74901 100644 --- a/SessionMessagingKit/Messages/Message+DisappearingMessages.swift +++ b/SessionMessagingKit/Messages/Message+DisappearingMessages.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUIKit import SessionUtilitiesKit diff --git a/SessionMessagingKit/Messages/Message+Origin.swift b/SessionMessagingKit/Messages/Message+Origin.swift index 4da490c326..540c6eb5d5 100644 --- a/SessionMessagingKit/Messages/Message+Origin.swift +++ b/SessionMessagingKit/Messages/Message+Origin.swift @@ -1,7 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension Message { diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index c640978d0d..1f775614c2 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit /// Abstract base class for `VisibleMessage` and `ControlMessage`. diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index 219021f442..f50966a7ac 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -1,7 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension OpenGroupAPI { diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 78b97e3f08..9f80cbe964 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public enum OpenGroupAPI { diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 4c6d534abe..9cd978e457 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Singleton diff --git a/SessionMessagingKit/Open Groups/Types/HTTPHeader+OpenGroup.swift b/SessionMessagingKit/Open Groups/Types/HTTPHeader+OpenGroup.swift index 0b3dbc54ab..29189d3cef 100644 --- a/SessionMessagingKit/Open Groups/Types/HTTPHeader+OpenGroup.swift +++ b/SessionMessagingKit/Open Groups/Types/HTTPHeader+OpenGroup.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit public extension HTTPHeader { static let sogsPubKey: HTTPHeader = "X-SOGS-Pubkey" diff --git a/SessionMessagingKit/Open Groups/Types/HTTPQueryParam+OpenGroup.swift b/SessionMessagingKit/Open Groups/Types/HTTPQueryParam+OpenGroup.swift index a9af9824ad..4eb7f6c206 100644 --- a/SessionMessagingKit/Open Groups/Types/HTTPQueryParam+OpenGroup.swift +++ b/SessionMessagingKit/Open Groups/Types/HTTPQueryParam+OpenGroup.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit public extension HTTPQueryParam { static let publicKey: HTTPQueryParam = "public_key" diff --git a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift index 6242a05447..5c8d72187f 100644 --- a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: Request - OpenGroupAPI diff --git a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift index 9da8faf919..e5e0b9bd34 100644 --- a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit extension OpenGroupAPI { public enum Endpoint: EndpointType { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift index 9670210fdc..cdd2921bcf 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift @@ -5,7 +5,7 @@ import AVFAudio import GRDB import WebRTC import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift index 4186c65e0b..4c5f597204 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift index 95a548578b..e771404d9a 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift index 8396652b28..ec2ecadc46 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit extension MessageReceiver { internal static func handleNewLegacyClosedGroup( diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LibSession.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LibSession.swift index acfe478541..f209de30ed 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LibSession.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LibSession.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index b5d2893a4d..1ad39c5a05 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -6,7 +6,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit extension MessageReceiver { internal static func handleMessageRequestResponse( diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift index ae75d068a5..a30f849c81 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index c67f091f60..a5ddfb44bf 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift index 58caa31955..c22b57d576 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit extension MessageSender { private typealias PreparedGroupData = ( diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 912f308871..e17035bdcb 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -4,7 +4,7 @@ import Foundation import GRDB import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift index bb681dd1b5..507a201df1 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageSender { diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 510819e3f4..a31872dccb 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift index 663bafb174..f29520550b 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift @@ -1,7 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit extension PushNotificationAPI { struct LegacyUnsubscribeRequest: Codable { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift index fefdbad9de..2267c9e130 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift @@ -1,7 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit extension PushNotificationAPI { public struct NotificationMetadata: Codable, Equatable { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift index 5138d5d8f5..971b969560 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension PushNotificationAPI { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift index 1d29c882d8..4f4b7da70c 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension PushNotificationAPI { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index 5c4bcd9c7c..2b4201f5a1 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -5,7 +5,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - KeychainStorage diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift index 36ed02e3e2..a5d92afb4c 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension PushNotificationAPI { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift index 78000ad2ce..c63988f5d1 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift @@ -1,7 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: Request - PushNotificationAPI diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift index 835329ec9c..39b2d0956f 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Cache diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift index e9290f1800..804ed95182 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift index 667ec2909c..1ec82b4124 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Cache diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift index 8cdacd5c99..91ef8cf1b9 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift @@ -4,7 +4,7 @@ import Foundation import Combine -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift index b632d6d87d..2f3ca1906a 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - SwarmPollerType diff --git a/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift b/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift index 3f5da3a998..3486db29e6 100644 --- a/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift +++ b/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Singleton diff --git a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift index 05553088ed..89d21fdc30 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension MessageViewModel { diff --git a/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift b/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift index 4a1069c1ca..745e1e4418 100644 --- a/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift +++ b/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Authentication Types diff --git a/SessionMessagingKit/Utilities/DisplayPictureManager.swift b/SessionMessagingKit/Utilities/DisplayPictureManager.swift index ae3b2adf93..6663fcf0f8 100644 --- a/SessionMessagingKit/Utilities/DisplayPictureManager.swift +++ b/SessionMessagingKit/Utilities/DisplayPictureManager.swift @@ -4,7 +4,7 @@ import UIKit import Combine import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton diff --git a/SessionMessagingKit/Utilities/MessageWrapper.swift b/SessionMessagingKit/Utilities/MessageWrapper.swift index 1e7f97ba3c..3ff9796b7b 100644 --- a/SessionMessagingKit/Utilities/MessageWrapper.swift +++ b/SessionMessagingKit/Utilities/MessageWrapper.swift @@ -1,7 +1,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public enum MessageWrapper { diff --git a/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift b/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift index 98d331cd1d..ed46bc066c 100644 --- a/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift +++ b/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift @@ -1,7 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension SNProtoEnvelope { diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index 0ca3ed5345..85d0809b20 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -6,7 +6,7 @@ import GRDB import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit @testable import SessionUtilitiesKit diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index 39d2bff89d..a226aab1df 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -6,7 +6,7 @@ import GRDB import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit @testable import SessionUtilitiesKit diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift index f9de94bac5..07bda1e05f 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift @@ -4,12 +4,12 @@ import Foundation import GRDB import SessionUtil import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class LibSessionGroupInfoSpec: QuickSpec { @@ -31,7 +31,7 @@ class LibSessionGroupInfoSpec: QuickSpec { migrationTargets: [ SNUtilitiesKit.self, SNMessagingKit.self, - SNSnodeKit.self + SNNetworkingKit.self ], using: dependencies, initialData: { db in diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift index 5e2884cd3e..8001ede737 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift @@ -8,7 +8,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class LibSessionGroupMembersSpec: QuickSpec { diff --git a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift index d135064fcf..f23341efdf 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift @@ -8,7 +8,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class LibSessionSpec: QuickSpec { diff --git a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift index db9aa0df35..ee3f4d94ac 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift @@ -1,7 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index fa13eeea02..73ae2a3cc6 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 556a2adee0..24635b39af 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -4,7 +4,7 @@ import UIKit import Combine import GRDB import SessionUtil -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift b/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift index 88e568f21b..b9e37aca22 100644 --- a/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift @@ -1,7 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index debc33045b..be86b9ed76 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -9,7 +9,7 @@ import SessionUtil import SessionUtilitiesKit import SessionUIKit -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class MessageReceiverGroupsSpec: QuickSpec { @@ -31,7 +31,7 @@ class MessageReceiverGroupsSpec: QuickSpec { customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self ], using: dependencies, diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift index 1bd4a1b614..2f55e63806 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift @@ -10,7 +10,7 @@ import Quick import Nimble @testable import SessionMessagingKit -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class MessageSenderGroupsSpec: QuickSpec { override class func spec() { diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift index 5e67562442..6ddb79b9bf 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift index 742f51d783..6f966ebf9c 100644 --- a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift @@ -3,7 +3,7 @@ import UIKit import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/_TestUtilities/MockPoller.swift b/SessionMessagingKitTests/_TestUtilities/MockPoller.swift index b3f044e3bd..794fa81829 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockPoller.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockPoller.swift @@ -2,7 +2,7 @@ import Foundation import Combine -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit @testable import SessionMessagingKit diff --git a/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift b/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift index 43ab428f96..d20f1da875 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift @@ -2,7 +2,7 @@ import Foundation import Combine -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit @testable import SessionMessagingKit diff --git a/SessionSnodeKit/Configuration.swift b/SessionNetworkingKit/Configuration.swift similarity index 89% rename from SessionSnodeKit/Configuration.swift rename to SessionNetworkingKit/Configuration.swift index af6ed9cc8d..18a73d2615 100644 --- a/SessionSnodeKit/Configuration.swift +++ b/SessionNetworkingKit/Configuration.swift @@ -4,10 +4,10 @@ import Foundation import GRDB import SessionUtilitiesKit -public enum SNSnodeKit: MigratableTarget { // Just to make the external API nice +public enum SNNetworkingKit: MigratableTarget { // Just to make the external API nice public static func migrations() -> TargetMigrations { return TargetMigrations( - identifier: .snodeKit, + identifier: .networkingKit, migrations: [ [ _001_InitialSetupMigration.self, diff --git a/SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift b/SessionNetworkingKit/Crypto/Crypto+SessionNetworkingKit.swift similarity index 100% rename from SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift rename to SessionNetworkingKit/Crypto/Crypto+SessionNetworkingKit.swift diff --git a/SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionNetworkingKit/Database/Migrations/_001_InitialSetupMigration.swift similarity index 96% rename from SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift rename to SessionNetworkingKit/Database/Migrations/_001_InitialSetupMigration.swift index 02f160fe0c..9473c323f8 100644 --- a/SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionNetworkingKit/Database/Migrations/_001_InitialSetupMigration.swift @@ -7,7 +7,7 @@ import GRDB import SessionUtilitiesKit enum _001_InitialSetupMigration: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "initialSetup" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionNetworkingKit/Database/Migrations/_002_SetupStandardJobs.swift similarity index 96% rename from SessionSnodeKit/Database/Migrations/_002_SetupStandardJobs.swift rename to SessionNetworkingKit/Database/Migrations/_002_SetupStandardJobs.swift index e92355cc9e..845a42fe16 100644 --- a/SessionSnodeKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionNetworkingKit/Database/Migrations/_002_SetupStandardJobs.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` enum _002_SetupStandardJobs: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "SetupStandardJobs" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionNetworkingKit/Database/Migrations/_003_YDBToGRDBMigration.swift similarity index 88% rename from SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift rename to SessionNetworkingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index 4e826bc308..382874faf3 100644 --- a/SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionNetworkingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -5,7 +5,7 @@ import GRDB import SessionUtilitiesKit enum _003_YDBToGRDBMigration: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "YDBToGRDBMigration" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift b/SessionNetworkingKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift similarity index 93% rename from SessionSnodeKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift rename to SessionNetworkingKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift index 486665167c..989df981c8 100644 --- a/SessionSnodeKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift +++ b/SessionNetworkingKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift @@ -8,7 +8,7 @@ import SessionUtilitiesKit /// ignore their hashes when subsequently trying to fetch new messages (which results in the storage server returning /// messages from the beginning of time) enum _004_FlagMessageHashAsDeletedOrInvalid: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "FlagMessageHashAsDeletedOrInvalid" static let minExpectedRunDuration: TimeInterval = 0.2 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift b/SessionNetworkingKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift similarity index 97% rename from SessionSnodeKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift rename to SessionNetworkingKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift index acc361590d..6565fc40d1 100644 --- a/SessionSnodeKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift +++ b/SessionNetworkingKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit /// This migration adds a primary key to `SnodeReceivedMessageInfo` based on the key and hash to speed up lookup enum _005_AddSnodeReveivedMessageInfoPrimaryKey: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "AddSnodeReveivedMessageInfoPrimaryKey" static let minExpectedRunDuration: TimeInterval = 0.2 static let createdTables: [(TableRecord & FetchableRecord).Type] = [SnodeReceivedMessageInfo.self] diff --git a/SessionSnodeKit/Database/Migrations/_006_DropSnodeCache.swift b/SessionNetworkingKit/Database/Migrations/_006_DropSnodeCache.swift similarity index 94% rename from SessionSnodeKit/Database/Migrations/_006_DropSnodeCache.swift rename to SessionNetworkingKit/Database/Migrations/_006_DropSnodeCache.swift index b2a3d41bd2..6cc16ea4ef 100644 --- a/SessionSnodeKit/Database/Migrations/_006_DropSnodeCache.swift +++ b/SessionNetworkingKit/Database/Migrations/_006_DropSnodeCache.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit /// This migration drops the current `SnodePool` and `SnodeSet` and their associated jobs as they are handled by `libSession` now enum _006_DropSnodeCache: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "DropSnodeCache" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift b/SessionNetworkingKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift similarity index 98% rename from SessionSnodeKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift rename to SessionNetworkingKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift index aa74f45ff4..779eecee5e 100644 --- a/SessionSnodeKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift +++ b/SessionNetworkingKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit /// This migration splits the old `key` structure used for `SnodeReceivedMessageInfo` into separate columns for more efficient querying enum _007_SplitSnodeReceivedMessageInfo: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "SplitSnodeReceivedMessageInfo" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [SnodeReceivedMessageInfo.self] diff --git a/SessionSnodeKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift b/SessionNetworkingKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift similarity index 94% rename from SessionSnodeKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift rename to SessionNetworkingKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift index 468ee6999c..1eb3e6d265 100644 --- a/SessionSnodeKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift +++ b/SessionNetworkingKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit /// This migration resets the `lastHash` value for all user config namespaces to force the app to fetch the latest config /// messages in case there are multi-part config message we had previously seen and failed to merge enum _008_ResetUserConfigLastHashes: Migration { - static let target: TargetMigrations.Identifier = .snodeKit + static let target: TargetMigrations.Identifier = .networkingKit static let identifier: String = "ResetUserConfigLastHashes" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift b/SessionNetworkingKit/Database/Models/SnodeReceivedMessageInfo.swift similarity index 100% rename from SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift rename to SessionNetworkingKit/Database/Models/SnodeReceivedMessageInfo.swift diff --git a/SessionSnodeKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift similarity index 100% rename from SessionSnodeKit/LibSession/LibSession+Networking.swift rename to SessionNetworkingKit/LibSession/LibSession+Networking.swift diff --git a/SessionSnodeKit/Meta/Info.plist b/SessionNetworkingKit/Meta/Info.plist similarity index 100% rename from SessionSnodeKit/Meta/Info.plist rename to SessionNetworkingKit/Meta/Info.plist diff --git a/SessionNetworkingKit/Meta/SessionNetworkingKit.h b/SessionNetworkingKit/Meta/SessionNetworkingKit.h new file mode 100644 index 0000000000..dd9ec08864 --- /dev/null +++ b/SessionNetworkingKit/Meta/SessionNetworkingKit.h @@ -0,0 +1,4 @@ +#import + +FOUNDATION_EXPORT double SessionNetworkingKitVersionNumber; +FOUNDATION_EXPORT const unsigned char SessionNetworkingKitVersionString[]; diff --git a/SessionSnodeKit/Models/AppVersionResponse.swift b/SessionNetworkingKit/Models/AppVersionResponse.swift similarity index 100% rename from SessionSnodeKit/Models/AppVersionResponse.swift rename to SessionNetworkingKit/Models/AppVersionResponse.swift diff --git a/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift b/SessionNetworkingKit/Models/DeleteAllBeforeRequest.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteAllBeforeRequest.swift rename to SessionNetworkingKit/Models/DeleteAllBeforeRequest.swift diff --git a/SessionSnodeKit/Models/DeleteAllBeforeResponse.swift b/SessionNetworkingKit/Models/DeleteAllBeforeResponse.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteAllBeforeResponse.swift rename to SessionNetworkingKit/Models/DeleteAllBeforeResponse.swift diff --git a/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift b/SessionNetworkingKit/Models/DeleteAllMessagesRequest.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteAllMessagesRequest.swift rename to SessionNetworkingKit/Models/DeleteAllMessagesRequest.swift diff --git a/SessionSnodeKit/Models/DeleteAllMessagesResponse.swift b/SessionNetworkingKit/Models/DeleteAllMessagesResponse.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteAllMessagesResponse.swift rename to SessionNetworkingKit/Models/DeleteAllMessagesResponse.swift diff --git a/SessionSnodeKit/Models/DeleteMessagesRequest.swift b/SessionNetworkingKit/Models/DeleteMessagesRequest.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteMessagesRequest.swift rename to SessionNetworkingKit/Models/DeleteMessagesRequest.swift diff --git a/SessionSnodeKit/Models/DeleteMessagesResponse.swift b/SessionNetworkingKit/Models/DeleteMessagesResponse.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteMessagesResponse.swift rename to SessionNetworkingKit/Models/DeleteMessagesResponse.swift diff --git a/SessionSnodeKit/Models/FileUploadResponse.swift b/SessionNetworkingKit/Models/FileUploadResponse.swift similarity index 100% rename from SessionSnodeKit/Models/FileUploadResponse.swift rename to SessionNetworkingKit/Models/FileUploadResponse.swift diff --git a/SessionSnodeKit/Models/GetExpiriesRequest.swift b/SessionNetworkingKit/Models/GetExpiriesRequest.swift similarity index 100% rename from SessionSnodeKit/Models/GetExpiriesRequest.swift rename to SessionNetworkingKit/Models/GetExpiriesRequest.swift diff --git a/SessionSnodeKit/Models/GetExpiriesResponse.swift b/SessionNetworkingKit/Models/GetExpiriesResponse.swift similarity index 100% rename from SessionSnodeKit/Models/GetExpiriesResponse.swift rename to SessionNetworkingKit/Models/GetExpiriesResponse.swift diff --git a/SessionSnodeKit/Models/GetMessagesRequest.swift b/SessionNetworkingKit/Models/GetMessagesRequest.swift similarity index 100% rename from SessionSnodeKit/Models/GetMessagesRequest.swift rename to SessionNetworkingKit/Models/GetMessagesRequest.swift diff --git a/SessionSnodeKit/Models/GetMessagesResponse.swift b/SessionNetworkingKit/Models/GetMessagesResponse.swift similarity index 100% rename from SessionSnodeKit/Models/GetMessagesResponse.swift rename to SessionNetworkingKit/Models/GetMessagesResponse.swift diff --git a/SessionSnodeKit/Models/GetNetworkTimestampResponse.swift b/SessionNetworkingKit/Models/GetNetworkTimestampResponse.swift similarity index 100% rename from SessionSnodeKit/Models/GetNetworkTimestampResponse.swift rename to SessionNetworkingKit/Models/GetNetworkTimestampResponse.swift diff --git a/SessionSnodeKit/Models/LegacyGetMessagesRequest.swift b/SessionNetworkingKit/Models/LegacyGetMessagesRequest.swift similarity index 100% rename from SessionSnodeKit/Models/LegacyGetMessagesRequest.swift rename to SessionNetworkingKit/Models/LegacyGetMessagesRequest.swift diff --git a/SessionSnodeKit/Models/LegacySendMessageRequest.swift b/SessionNetworkingKit/Models/LegacySendMessageRequest.swift similarity index 100% rename from SessionSnodeKit/Models/LegacySendMessageRequest.swift rename to SessionNetworkingKit/Models/LegacySendMessageRequest.swift diff --git a/SessionSnodeKit/Models/ONSResolveRequest.swift b/SessionNetworkingKit/Models/ONSResolveRequest.swift similarity index 100% rename from SessionSnodeKit/Models/ONSResolveRequest.swift rename to SessionNetworkingKit/Models/ONSResolveRequest.swift diff --git a/SessionSnodeKit/Models/ONSResolveResponse.swift b/SessionNetworkingKit/Models/ONSResolveResponse.swift similarity index 100% rename from SessionSnodeKit/Models/ONSResolveResponse.swift rename to SessionNetworkingKit/Models/ONSResolveResponse.swift diff --git a/SessionSnodeKit/Models/OxenDaemonRPCRequest.swift b/SessionNetworkingKit/Models/OxenDaemonRPCRequest.swift similarity index 100% rename from SessionSnodeKit/Models/OxenDaemonRPCRequest.swift rename to SessionNetworkingKit/Models/OxenDaemonRPCRequest.swift diff --git a/SessionSnodeKit/Models/RevokeSubaccountRequest.swift b/SessionNetworkingKit/Models/RevokeSubaccountRequest.swift similarity index 100% rename from SessionSnodeKit/Models/RevokeSubaccountRequest.swift rename to SessionNetworkingKit/Models/RevokeSubaccountRequest.swift diff --git a/SessionSnodeKit/Models/RevokeSubaccountResponse.swift b/SessionNetworkingKit/Models/RevokeSubaccountResponse.swift similarity index 100% rename from SessionSnodeKit/Models/RevokeSubaccountResponse.swift rename to SessionNetworkingKit/Models/RevokeSubaccountResponse.swift diff --git a/SessionSnodeKit/Models/SendMessageRequest.swift b/SessionNetworkingKit/Models/SendMessageRequest.swift similarity index 100% rename from SessionSnodeKit/Models/SendMessageRequest.swift rename to SessionNetworkingKit/Models/SendMessageRequest.swift diff --git a/SessionSnodeKit/Models/SendMessageResponse.swift b/SessionNetworkingKit/Models/SendMessageResponse.swift similarity index 100% rename from SessionSnodeKit/Models/SendMessageResponse.swift rename to SessionNetworkingKit/Models/SendMessageResponse.swift diff --git a/SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift b/SessionNetworkingKit/Models/SnodeAuthenticatedRequestBody.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift rename to SessionNetworkingKit/Models/SnodeAuthenticatedRequestBody.swift diff --git a/SessionSnodeKit/Models/SnodeBatchRequest.swift b/SessionNetworkingKit/Models/SnodeBatchRequest.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeBatchRequest.swift rename to SessionNetworkingKit/Models/SnodeBatchRequest.swift diff --git a/SessionSnodeKit/Models/SnodeMessage.swift b/SessionNetworkingKit/Models/SnodeMessage.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeMessage.swift rename to SessionNetworkingKit/Models/SnodeMessage.swift diff --git a/SessionSnodeKit/Models/SnodeReceivedMessage.swift b/SessionNetworkingKit/Models/SnodeReceivedMessage.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeReceivedMessage.swift rename to SessionNetworkingKit/Models/SnodeReceivedMessage.swift diff --git a/SessionSnodeKit/Models/SnodeRecursiveResponse.swift b/SessionNetworkingKit/Models/SnodeRecursiveResponse.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeRecursiveResponse.swift rename to SessionNetworkingKit/Models/SnodeRecursiveResponse.swift diff --git a/SessionSnodeKit/Models/SnodeRequest.swift b/SessionNetworkingKit/Models/SnodeRequest.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeRequest.swift rename to SessionNetworkingKit/Models/SnodeRequest.swift diff --git a/SessionSnodeKit/Models/SnodeResponse.swift b/SessionNetworkingKit/Models/SnodeResponse.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeResponse.swift rename to SessionNetworkingKit/Models/SnodeResponse.swift diff --git a/SessionSnodeKit/Models/SnodeSwarmItem.swift b/SessionNetworkingKit/Models/SnodeSwarmItem.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeSwarmItem.swift rename to SessionNetworkingKit/Models/SnodeSwarmItem.swift diff --git a/SessionSnodeKit/Models/UnrevokeSubaccountRequest.swift b/SessionNetworkingKit/Models/UnrevokeSubaccountRequest.swift similarity index 100% rename from SessionSnodeKit/Models/UnrevokeSubaccountRequest.swift rename to SessionNetworkingKit/Models/UnrevokeSubaccountRequest.swift diff --git a/SessionSnodeKit/Models/UnrevokeSubaccountResponse.swift b/SessionNetworkingKit/Models/UnrevokeSubaccountResponse.swift similarity index 100% rename from SessionSnodeKit/Models/UnrevokeSubaccountResponse.swift rename to SessionNetworkingKit/Models/UnrevokeSubaccountResponse.swift diff --git a/SessionSnodeKit/Models/UpdateExpiryAllRequest.swift b/SessionNetworkingKit/Models/UpdateExpiryAllRequest.swift similarity index 100% rename from SessionSnodeKit/Models/UpdateExpiryAllRequest.swift rename to SessionNetworkingKit/Models/UpdateExpiryAllRequest.swift diff --git a/SessionSnodeKit/Models/UpdateExpiryAllResponse.swift b/SessionNetworkingKit/Models/UpdateExpiryAllResponse.swift similarity index 100% rename from SessionSnodeKit/Models/UpdateExpiryAllResponse.swift rename to SessionNetworkingKit/Models/UpdateExpiryAllResponse.swift diff --git a/SessionSnodeKit/Models/UpdateExpiryRequest.swift b/SessionNetworkingKit/Models/UpdateExpiryRequest.swift similarity index 100% rename from SessionSnodeKit/Models/UpdateExpiryRequest.swift rename to SessionNetworkingKit/Models/UpdateExpiryRequest.swift diff --git a/SessionSnodeKit/Models/UpdateExpiryResponse.swift b/SessionNetworkingKit/Models/UpdateExpiryResponse.swift similarity index 100% rename from SessionSnodeKit/Models/UpdateExpiryResponse.swift rename to SessionNetworkingKit/Models/UpdateExpiryResponse.swift diff --git a/SessionSnodeKit/Networking/SnodeAPI.swift b/SessionNetworkingKit/Networking/SnodeAPI.swift similarity index 100% rename from SessionSnodeKit/Networking/SnodeAPI.swift rename to SessionNetworkingKit/Networking/SnodeAPI.swift diff --git a/SessionSnodeKit/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift b/SessionNetworkingKit/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift similarity index 100% rename from SessionSnodeKit/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift rename to SessionNetworkingKit/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift diff --git a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Database.swift b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Database.swift similarity index 100% rename from SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Database.swift rename to SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Database.swift diff --git a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Models.swift b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Models.swift similarity index 100% rename from SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Models.swift rename to SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Models.swift diff --git a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift similarity index 96% rename from SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift rename to SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift index fb26688ac1..79feb74a36 100644 --- a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift +++ b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift @@ -21,7 +21,7 @@ extension SessionNetworkAPI { public func initialize(using dependencies: Dependencies) { self.dependencies = dependencies cancellable = getInfo(using: dependencies) - .subscribe(on: Threading.workQueue, using: dependencies) + .subscribe(on: SessionNetworkAPI.workQueue, using: dependencies) .receive(on: SessionNetworkAPI.workQueue) .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) } @@ -32,7 +32,7 @@ extension SessionNetworkAPI { let staleTimestampMs: Int64 = dependencies[singleton: .storage].read { db in db[.staleTimestampMs] }.defaulting(to: 0) guard staleTimestampMs < dependencies[cache: .snodeAPI].currentOffsetTimestampMs() else { return Just(()) - .delay(for: .milliseconds(500), scheduler: Threading.workQueue) + .delay(for: .milliseconds(500), scheduler: SessionNetworkAPI.workQueue) .setFailureType(to: Error.self) .flatMapStorageWritePublisher(using: dependencies) { [dependencies] db, info -> Bool in db[.lastUpdatedTimestampMs] = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() diff --git a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI.swift b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift similarity index 100% rename from SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI.swift rename to SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift diff --git a/SessionSnodeKit/SnodeAPI/Request+SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift similarity index 100% rename from SessionSnodeKit/SnodeAPI/Request+SnodeAPI.swift rename to SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift diff --git a/SessionSnodeKit/SnodeAPI/ResponseInfo+SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/ResponseInfo+SnodeAPI.swift similarity index 100% rename from SessionSnodeKit/SnodeAPI/ResponseInfo+SnodeAPI.swift rename to SessionNetworkingKit/SnodeAPI/ResponseInfo+SnodeAPI.swift diff --git a/SessionSnodeKit/SnodeAPI/SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift similarity index 100% rename from SessionSnodeKit/SnodeAPI/SnodeAPI.swift rename to SessionNetworkingKit/SnodeAPI/SnodeAPI.swift diff --git a/SessionSnodeKit/SnodeAPI/SnodeAPIEndpoint.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPIEndpoint.swift similarity index 100% rename from SessionSnodeKit/SnodeAPI/SnodeAPIEndpoint.swift rename to SessionNetworkingKit/SnodeAPI/SnodeAPIEndpoint.swift diff --git a/SessionSnodeKit/SnodeAPI/SnodeAPIError.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPIError.swift similarity index 100% rename from SessionSnodeKit/SnodeAPI/SnodeAPIError.swift rename to SessionNetworkingKit/SnodeAPI/SnodeAPIError.swift diff --git a/SessionSnodeKit/SnodeAPI/SnodeAPINamespace.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPINamespace.swift similarity index 100% rename from SessionSnodeKit/SnodeAPI/SnodeAPINamespace.swift rename to SessionNetworkingKit/SnodeAPI/SnodeAPINamespace.swift diff --git a/SessionSnodeKit/Types/BatchRequest.swift b/SessionNetworkingKit/Types/BatchRequest.swift similarity index 100% rename from SessionSnodeKit/Types/BatchRequest.swift rename to SessionNetworkingKit/Types/BatchRequest.swift diff --git a/SessionSnodeKit/Types/BatchResponse.swift b/SessionNetworkingKit/Types/BatchResponse.swift similarity index 100% rename from SessionSnodeKit/Types/BatchResponse.swift rename to SessionNetworkingKit/Types/BatchResponse.swift diff --git a/SessionSnodeKit/Types/BencodeResponse.swift b/SessionNetworkingKit/Types/BencodeResponse.swift similarity index 100% rename from SessionSnodeKit/Types/BencodeResponse.swift rename to SessionNetworkingKit/Types/BencodeResponse.swift diff --git a/SessionSnodeKit/Types/ContentProxy.swift b/SessionNetworkingKit/Types/ContentProxy.swift similarity index 100% rename from SessionSnodeKit/Types/ContentProxy.swift rename to SessionNetworkingKit/Types/ContentProxy.swift diff --git a/SessionSnodeKit/Types/Destination.swift b/SessionNetworkingKit/Types/Destination.swift similarity index 100% rename from SessionSnodeKit/Types/Destination.swift rename to SessionNetworkingKit/Types/Destination.swift diff --git a/SessionSnodeKit/Types/HTTPHeader.swift b/SessionNetworkingKit/Types/HTTPHeader.swift similarity index 100% rename from SessionSnodeKit/Types/HTTPHeader.swift rename to SessionNetworkingKit/Types/HTTPHeader.swift diff --git a/SessionSnodeKit/Types/HTTPMethod.swift b/SessionNetworkingKit/Types/HTTPMethod.swift similarity index 100% rename from SessionSnodeKit/Types/HTTPMethod.swift rename to SessionNetworkingKit/Types/HTTPMethod.swift diff --git a/SessionSnodeKit/Types/HTTPQueryParam.swift b/SessionNetworkingKit/Types/HTTPQueryParam.swift similarity index 100% rename from SessionSnodeKit/Types/HTTPQueryParam.swift rename to SessionNetworkingKit/Types/HTTPQueryParam.swift diff --git a/SessionSnodeKit/Types/IPv4.swift b/SessionNetworkingKit/Types/IPv4.swift similarity index 100% rename from SessionSnodeKit/Types/IPv4.swift rename to SessionNetworkingKit/Types/IPv4.swift diff --git a/SessionSnodeKit/Types/JSON.swift b/SessionNetworkingKit/Types/JSON.swift similarity index 100% rename from SessionSnodeKit/Types/JSON.swift rename to SessionNetworkingKit/Types/JSON.swift diff --git a/SessionSnodeKit/Types/Network.swift b/SessionNetworkingKit/Types/Network.swift similarity index 100% rename from SessionSnodeKit/Types/Network.swift rename to SessionNetworkingKit/Types/Network.swift diff --git a/SessionSnodeKit/Types/NetworkError.swift b/SessionNetworkingKit/Types/NetworkError.swift similarity index 100% rename from SessionSnodeKit/Types/NetworkError.swift rename to SessionNetworkingKit/Types/NetworkError.swift diff --git a/SessionSnodeKit/Types/PreparedRequest+Sending.swift b/SessionNetworkingKit/Types/PreparedRequest+Sending.swift similarity index 100% rename from SessionSnodeKit/Types/PreparedRequest+Sending.swift rename to SessionNetworkingKit/Types/PreparedRequest+Sending.swift diff --git a/SessionSnodeKit/Types/PreparedRequest.swift b/SessionNetworkingKit/Types/PreparedRequest.swift similarity index 100% rename from SessionSnodeKit/Types/PreparedRequest.swift rename to SessionNetworkingKit/Types/PreparedRequest.swift diff --git a/SessionSnodeKit/Types/ProxiedContentDownloader.swift b/SessionNetworkingKit/Types/ProxiedContentDownloader.swift similarity index 100% rename from SessionSnodeKit/Types/ProxiedContentDownloader.swift rename to SessionNetworkingKit/Types/ProxiedContentDownloader.swift diff --git a/SessionSnodeKit/Types/Request.swift b/SessionNetworkingKit/Types/Request.swift similarity index 100% rename from SessionSnodeKit/Types/Request.swift rename to SessionNetworkingKit/Types/Request.swift diff --git a/SessionNetworkingKit/Types/RequestCategory.swift b/SessionNetworkingKit/Types/RequestCategory.swift new file mode 100644 index 0000000000..e69de29bb2 diff --git a/SessionSnodeKit/Types/ResponseInfo.swift b/SessionNetworkingKit/Types/ResponseInfo.swift similarity index 100% rename from SessionSnodeKit/Types/ResponseInfo.swift rename to SessionNetworkingKit/Types/ResponseInfo.swift diff --git a/SessionNetworkingKit/Types/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift new file mode 100644 index 0000000000..464a3b1b9e --- /dev/null +++ b/SessionNetworkingKit/Types/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift @@ -0,0 +1,9 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +public extension HTTPHeader { + static let tokenServerPubKey: HTTPHeader = "X-FS-Pubkey" + static let tokenServerTimestamp: HTTPHeader = "X-FS-Timestamp" + static let tokenServerSignature: HTTPHeader = "X-FS-Signature" +} diff --git a/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Database.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Database.swift new file mode 100644 index 0000000000..9316b1a440 --- /dev/null +++ b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Database.swift @@ -0,0 +1,32 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import AudioToolbox +import GRDB +import DifferenceKit +import SessionUtilitiesKit + +public extension KeyValueStore.StringKey { + static let contractAddress: KeyValueStore.StringKey = "contractAddress" +} + +public extension KeyValueStore.DoubleKey { + static let tokenUsd: KeyValueStore.DoubleKey = "tokenUsd" + static let marketCapUsd: KeyValueStore.DoubleKey = "marketCapUsd" + static let stakingRequirement: KeyValueStore.DoubleKey = "stakingRequirement" + static let stakingRewardPool: KeyValueStore.DoubleKey = "stakingRewardPool" + static let networkStakedTokens: KeyValueStore.DoubleKey = "networkStakedTokens" + static let networkStakedUSD: KeyValueStore.DoubleKey = "networkStakedUSD" +} + +public extension KeyValueStore.IntKey { + static let networkSize: KeyValueStore.IntKey = "networkSize" +} + +public extension KeyValueStore.Int64Key { + static let lastUpdatedTimestampMs: KeyValueStore.Int64Key = "lastUpdatedTimestampMs" + static let staleTimestampMs: KeyValueStore.Int64Key = "staleTimestampMs" + static let priceTimestampMs: KeyValueStore.Int64Key = "priceTimestampMs" +} diff --git a/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Models.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Models.swift new file mode 100644 index 0000000000..cd1cfec84c --- /dev/null +++ b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Models.swift @@ -0,0 +1,75 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import GRDB +import SessionUtilitiesKit + +extension SessionNetworkAPI { + + // MARK: - Price + + public struct Price: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case tokenUsd = "usd" + case marketCapUsd = "usd_market_cap" + case priceTimestamp = "t_price" + case staleTimestamp = "t_stale" + } + + public let tokenUsd: Double? // Current token price (USD) + public let marketCapUsd: Double? // Current market cap value in (USD) + public let priceTimestamp: Int64? // The timestamp the price data is accurate at. (seconds) + public let staleTimestamp: Int64? // Stale timestamp for the price data. (seconds) + } + + // MARK: - Token + + public struct Token: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case stakingRequirement = "staking_requirement" + case stakingRewardPool = "staking_reward_pool" + case contractAddress = "contract_address" + } + + public let stakingRequirement: Double? // The number of tokens required to stake a node. This is the effective "token amount" per node (SESH) + public let stakingRewardPool: Double? // The number of tokens in the staking reward pool (SESH) + public let contractAddress: String? // Token contract address (42 char Hexadecimal - Including 0x prefix) + } + + + // MARK: - Network Info + + public struct NetworkInfo: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case networkSize = "network_size" // The number of nodes in the Session Network (integer) + case networkStakedTokens = "network_staked_tokens" // + case networkStakedUSD = "network_staked_usd" // + } + + public let networkSize: Int? + public let networkStakedTokens: Double? + public let networkStakedUSD: Double? + } + + // MARK: - Info + + public struct Info: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case timestamp = "t" + case statusCode = "status_code" + case price + case token + case network + } + + public let timestamp: Int64? // Request timestamp. (seconds) + public let statusCode: Int? // Status code of the request. + public let price: Price? + public let token: Token? + public let network: NetworkInfo? + } +} + diff --git a/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Network.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Network.swift new file mode 100644 index 0000000000..947e55d15d --- /dev/null +++ b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Network.swift @@ -0,0 +1,107 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import GRDB +import SessionUtilitiesKit + +// MARK: - Log.Category + +public extension Log.Category { + static let sessionNetwork: Log.Category = .create("SessionNetwork", defaultLevel: .info) +} + +extension SessionNetworkAPI { + public final class HTTPClient { + private var cancellable: AnyCancellable? + private var dependencies: Dependencies? + + public func initialize(using dependencies: Dependencies) { + self.dependencies = dependencies + cancellable = getInfo(using: dependencies) + .subscribe(on: Threading.workQueue, using: dependencies) + .receive(on: SessionNetworkAPI.workQueue) + .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) + } + + public func getInfo(using dependencies: Dependencies) -> AnyPublisher { + cancellable?.cancel() + + let staleTimestampMs: Int64 = dependencies[singleton: .storage].read { db in db[.staleTimestampMs] }.defaulting(to: 0) + guard staleTimestampMs < dependencies[cache: .snodeAPI].currentOffsetTimestampMs() else { + return Just(()) + .delay(for: .milliseconds(500), scheduler: Threading.workQueue) + .setFailureType(to: Error.self) + .flatMapStorageWritePublisher(using: dependencies) { [dependencies] db, info -> Bool in + db[.lastUpdatedTimestampMs] = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + return true + } + .eraseToAnyPublisher() + } + + return dependencies[singleton: .storage] + .readPublisher { db -> Network.PreparedRequest in + try SessionNetworkAPI + .prepareInfo( + db, + using: dependencies + ) + } + .flatMap { $0.send(using: dependencies) } + .map { _, info in info } + .flatMapStorageWritePublisher(using: dependencies) { [dependencies] db, info -> Bool in + // Token info + db[.lastUpdatedTimestampMs] = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + db[.tokenUsd] = info.price?.tokenUsd + db[.marketCapUsd] = info.price?.marketCapUsd + if let priceTimestamp = info.price?.priceTimestamp { + db[.priceTimestampMs] = priceTimestamp * 1000 + } else { + db[.priceTimestampMs] = nil + } + if let staleTimestamp = info.price?.staleTimestamp { + db[.staleTimestampMs] = staleTimestamp * 1000 + } else { + db[.staleTimestampMs] = nil + } + db[.stakingRequirement] = info.token?.stakingRequirement + db[.stakingRewardPool] = info.token?.stakingRewardPool + db[.contractAddress] = info.token?.contractAddress + // Network info + db[.networkSize] = info.network?.networkSize + db[.networkStakedTokens] = info.network?.networkStakedTokens + db[.networkStakedUSD] = info.network?.networkStakedUSD + + return true + } + .catch { error -> AnyPublisher in + Log.error(.sessionNetwork, "Failed to fetch token info due to error: \(error).") + return self.cleanUpSessionNetworkPageData(using: dependencies) + .map { _ in false } + .eraseToAnyPublisher() + + } + .eraseToAnyPublisher() + } + + private func cleanUpSessionNetworkPageData(using dependencies: Dependencies) -> AnyPublisher { + dependencies[singleton: .storage].writePublisher { db in + // Token info + db[.lastUpdatedTimestampMs] = nil + db[.tokenUsd] = nil + db[.marketCapUsd] = nil + db[.priceTimestampMs] = nil + db[.staleTimestampMs] = nil + db[.stakingRequirement] = nil + db[.stakingRewardPool] = nil + db[.contractAddress] = nil + // Network info + db[.networkSize] = nil + db[.networkStakedTokens] = nil + db[.networkStakedUSD] = nil + } + } + } +} diff --git a/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI.swift new file mode 100644 index 0000000000..1d1891a51d --- /dev/null +++ b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI.swift @@ -0,0 +1,131 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import GRDB +import SessionUtilitiesKit + +public enum SessionNetworkAPI { + public static let workQueue = DispatchQueue(label: "SessionNetworkAPI.workQueue", qos: .userInitiated) + public static let client = HTTPClient() + + // MARK: - Info + + /// General token info. This endpoint combines the `/price` and `/token` endpoint information. + /// + /// `GET/info` + + public static func prepareInfo( + _ db: Database, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try Network.PreparedRequest( + request: Request( + endpoint: Network.NetworkAPI.Endpoint.info, + destination: .server( + method: .get, + server: Network.NetworkAPI.networkAPIServer, + queryParameters: [:], + x25519PublicKey: Network.NetworkAPI.networkAPIServerPublicKey + ) + ), + responseType: Info.self, + requestAndPathBuildTimeout: Network.defaultTimeout, + using: dependencies + ) + .signed(db, with: SessionNetworkAPI.signRequest, using: dependencies) + } + + // MARK: - Authentication + + fileprivate static func signatureHeaders( + _ db: Database, + url: URL, + method: HTTPMethod, + body: Data?, + using dependencies: Dependencies + ) throws -> [HTTPHeader: String] { + let timestamp: UInt64 = UInt64(floor(dependencies.dateNow.timeIntervalSince1970)) + let path: String = url.path + .appending(url.query.map { value in "?\(value)" }) + + let signResult: (publicKey: String, signature: [UInt8]) = try sign( + db, + timestamp: timestamp, + method: method.rawValue, + path: path, + body: body, + using: dependencies + ) + + return [ + HTTPHeader.tokenServerPubKey: signResult.publicKey, + HTTPHeader.tokenServerTimestamp: "\(timestamp)", + HTTPHeader.tokenServerSignature: signResult.signature.toBase64() + ] + } + + private static func sign( + _ db: Database, + timestamp: UInt64, + method: String, + path: String, + body: Data?, + using dependencies: Dependencies + ) throws -> (publicKey: String, signature: [UInt8]) { + let bodyString: String? = { + guard let bodyData: Data = body else { return nil } + return String(data: bodyData, encoding: .utf8) + }() + + guard + let userEdKeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db), + let blinded07KeyPair: KeyPair = dependencies[singleton: .crypto].generate( + .versionBlinded07KeyPair(ed25519SecretKey: userEdKeyPair.secretKey) + ), + let signatureResult: [UInt8] = dependencies[singleton: .crypto].generate( + .signatureVersionBlind07( + timestamp: timestamp, + method: method, + path: path, + body: bodyString, + ed25519SecretKey: userEdKeyPair.secretKey + ) + ) + else { throw NetworkError.signingFailed } + + return ( + publicKey: SessionId(.versionBlinded07, publicKey: blinded07KeyPair.publicKey).hexString, + signature: signatureResult + ) + } + + private static func signRequest( + _ db: Database, + preparedRequest: Network.PreparedRequest, + using dependencies: Dependencies + ) throws -> Network.Destination { + guard let url: URL = preparedRequest.destination.url else { + throw NetworkError.signingFailed + } + + guard case let .server(info) = preparedRequest.destination else { + throw NetworkError.signingFailed + } + + return .server( + info: info.updated( + with: try signatureHeaders( + db, + url: url, + method: preparedRequest.method, + body: preparedRequest.body, + using: dependencies + ) + ) + ) + } +} + diff --git a/SessionSnodeKit/Types/SwarmDrainBehaviour.swift b/SessionNetworkingKit/Types/SwarmDrainBehaviour.swift similarity index 100% rename from SessionSnodeKit/Types/SwarmDrainBehaviour.swift rename to SessionNetworkingKit/Types/SwarmDrainBehaviour.swift diff --git a/SessionSnodeKit/Types/UpdatableTimestamp.swift b/SessionNetworkingKit/Types/UpdatableTimestamp.swift similarity index 100% rename from SessionSnodeKit/Types/UpdatableTimestamp.swift rename to SessionNetworkingKit/Types/UpdatableTimestamp.swift diff --git a/SessionSnodeKit/Types/ValidatableResponse.swift b/SessionNetworkingKit/Types/ValidatableResponse.swift similarity index 100% rename from SessionSnodeKit/Types/ValidatableResponse.swift rename to SessionNetworkingKit/Types/ValidatableResponse.swift diff --git a/SessionSnodeKit/Utilities/Data+Utilities.swift b/SessionNetworkingKit/Utilities/Data+Utilities.swift similarity index 100% rename from SessionSnodeKit/Utilities/Data+Utilities.swift rename to SessionNetworkingKit/Utilities/Data+Utilities.swift diff --git a/SessionSnodeKit/Utilities/Publisher+Utilities.swift b/SessionNetworkingKit/Utilities/Publisher+Utilities.swift similarity index 100% rename from SessionSnodeKit/Utilities/Publisher+Utilities.swift rename to SessionNetworkingKit/Utilities/Publisher+Utilities.swift diff --git a/SessionSnodeKit/Utilities/RetryWithDependencies.swift b/SessionNetworkingKit/Utilities/RetryWithDependencies.swift similarity index 100% rename from SessionSnodeKit/Utilities/RetryWithDependencies.swift rename to SessionNetworkingKit/Utilities/RetryWithDependencies.swift diff --git a/SessionSnodeKit/Utilities/String+Trimming.swift b/SessionNetworkingKit/Utilities/String+Trimming.swift similarity index 100% rename from SessionSnodeKit/Utilities/String+Trimming.swift rename to SessionNetworkingKit/Utilities/String+Trimming.swift diff --git a/SessionSnodeKit/Utilities/URLResponse+Utilities.swift b/SessionNetworkingKit/Utilities/URLResponse+Utilities.swift similarity index 100% rename from SessionSnodeKit/Utilities/URLResponse+Utilities.swift rename to SessionNetworkingKit/Utilities/URLResponse+Utilities.swift diff --git a/SessionSnodeKitTests/Models/FileUploadResponseSpec.swift b/SessionNetworkingKitTests/Models/FileUploadResponseSpec.swift similarity index 96% rename from SessionSnodeKitTests/Models/FileUploadResponseSpec.swift rename to SessionNetworkingKitTests/Models/FileUploadResponseSpec.swift index 1454fbe383..4339984b89 100644 --- a/SessionSnodeKitTests/Models/FileUploadResponseSpec.swift +++ b/SessionNetworkingKitTests/Models/FileUploadResponseSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class FileUploadResponseSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Models/SnodeRequestSpec.swift b/SessionNetworkingKitTests/Models/SnodeRequestSpec.swift similarity index 98% rename from SessionSnodeKitTests/Models/SnodeRequestSpec.swift rename to SessionNetworkingKitTests/Models/SnodeRequestSpec.swift index 3d6836709b..0405d71e7c 100644 --- a/SessionSnodeKitTests/Models/SnodeRequestSpec.swift +++ b/SessionNetworkingKitTests/Models/SnodeRequestSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class SnodeRequestSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/SessionSnodeKit.xctestplan b/SessionNetworkingKitTests/SessionNetworkingKit.xctestplan similarity index 90% rename from SessionSnodeKitTests/SessionSnodeKit.xctestplan rename to SessionNetworkingKitTests/SessionNetworkingKit.xctestplan index ea699b6efd..52ef80ee0c 100644 --- a/SessionSnodeKitTests/SessionSnodeKit.xctestplan +++ b/SessionNetworkingKitTests/SessionNetworkingKit.xctestplan @@ -17,7 +17,7 @@ "target" : { "containerPath" : "container:Session.xcodeproj", "identifier" : "FDB5DAF92A981C42002C8721", - "name" : "SessionSnodeKitTests" + "name" : "SessionNetworkingKitTests" } } ], diff --git a/SessionSnodeKitTests/Types/BatchRequestSpec.swift b/SessionNetworkingKitTests/Types/BatchRequestSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/BatchRequestSpec.swift rename to SessionNetworkingKitTests/Types/BatchRequestSpec.swift index 36b33bd18a..05554a2be1 100644 --- a/SessionSnodeKitTests/Types/BatchRequestSpec.swift +++ b/SessionNetworkingKitTests/Types/BatchRequestSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class BatchRequestSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/BatchResponseSpec.swift b/SessionNetworkingKitTests/Types/BatchResponseSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/BatchResponseSpec.swift rename to SessionNetworkingKitTests/Types/BatchResponseSpec.swift index 3fe9ea5575..eac736ac8b 100644 --- a/SessionSnodeKitTests/Types/BatchResponseSpec.swift +++ b/SessionNetworkingKitTests/Types/BatchResponseSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class BatchResponseSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/BencodeResponseSpec.swift b/SessionNetworkingKitTests/Types/BencodeResponseSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/BencodeResponseSpec.swift rename to SessionNetworkingKitTests/Types/BencodeResponseSpec.swift index e0e6add5f9..0bd213f4ec 100644 --- a/SessionSnodeKitTests/Types/BencodeResponseSpec.swift +++ b/SessionNetworkingKitTests/Types/BencodeResponseSpec.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class BencodeResponseSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/DestinationSpec.swift b/SessionNetworkingKitTests/Types/DestinationSpec.swift similarity index 98% rename from SessionSnodeKitTests/Types/DestinationSpec.swift rename to SessionNetworkingKitTests/Types/DestinationSpec.swift index 6e17460ac5..e226a25f21 100644 --- a/SessionSnodeKitTests/Types/DestinationSpec.swift +++ b/SessionNetworkingKitTests/Types/DestinationSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class DestinationSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/HeaderSpec.swift b/SessionNetworkingKitTests/Types/HeaderSpec.swift similarity index 93% rename from SessionSnodeKitTests/Types/HeaderSpec.swift rename to SessionNetworkingKitTests/Types/HeaderSpec.swift index ef25b0c5a7..9f73a41944 100644 --- a/SessionSnodeKitTests/Types/HeaderSpec.swift +++ b/SessionNetworkingKitTests/Types/HeaderSpec.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class HeaderSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/PreparedRequestSendingSpec.swift b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/PreparedRequestSendingSpec.swift rename to SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift index a75966aa6b..a1ad6f438b 100644 --- a/SessionSnodeKitTests/Types/PreparedRequestSendingSpec.swift +++ b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class PreparedRequestSendingSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/PreparedRequestSpec.swift b/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/PreparedRequestSpec.swift rename to SessionNetworkingKitTests/Types/PreparedRequestSpec.swift index 7ba2a676ac..7b949b5fde 100644 --- a/SessionSnodeKitTests/Types/PreparedRequestSpec.swift +++ b/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class PreparedRequestSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/RequestSpec.swift b/SessionNetworkingKitTests/Types/RequestSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/RequestSpec.swift rename to SessionNetworkingKitTests/Types/RequestSpec.swift index 9888d30083..0b951cf1d3 100644 --- a/SessionSnodeKitTests/Types/RequestSpec.swift +++ b/SessionNetworkingKitTests/Types/RequestSpec.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class RequestSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/_TestUtilities/CommonSSKMockExtensions.swift b/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift similarity index 96% rename from SessionSnodeKitTests/_TestUtilities/CommonSSKMockExtensions.swift rename to SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift index 85e05a67af..d99134ceba 100644 --- a/SessionSnodeKitTests/_TestUtilities/CommonSSKMockExtensions.swift +++ b/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift @@ -2,7 +2,7 @@ import Foundation -@testable import SessionSnodeKit +@testable import SessionNetworkingKit extension NoResponse: Mocked { static var mock: NoResponse = NoResponse() diff --git a/SessionSnodeKitTests/_TestUtilities/MockNetwork.swift b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift similarity index 99% rename from SessionSnodeKitTests/_TestUtilities/MockNetwork.swift rename to SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift index efecb6d9ac..1412423914 100644 --- a/SessionSnodeKitTests/_TestUtilities/MockNetwork.swift +++ b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift @@ -4,7 +4,7 @@ import Foundation import Combine import SessionUtilitiesKit -@testable import SessionSnodeKit +@testable import SessionNetworkingKit // MARK: - MockNetwork diff --git a/SessionSnodeKitTests/_TestUtilities/MockSnodeAPICache.swift b/SessionNetworkingKitTests/_TestUtilities/MockSnodeAPICache.swift similarity index 97% rename from SessionSnodeKitTests/_TestUtilities/MockSnodeAPICache.swift rename to SessionNetworkingKitTests/_TestUtilities/MockSnodeAPICache.swift index 75111962b8..90be7933ed 100644 --- a/SessionSnodeKitTests/_TestUtilities/MockSnodeAPICache.swift +++ b/SessionNetworkingKitTests/_TestUtilities/MockSnodeAPICache.swift @@ -6,7 +6,7 @@ import Foundation import Combine import SessionUtilitiesKit -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class MockSnodeAPICache: Mock, SnodeAPICacheType { var hardfork: Int { diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 295ebf2071..0fe6785ada 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -5,7 +5,7 @@ import CallKit import UserNotifications import SessionUIKit import SessionMessagingKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionShareExtension/ShareNavController.swift b/SessionShareExtension/ShareNavController.swift index 13b036416a..6a39606df5 100644 --- a/SessionShareExtension/ShareNavController.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -7,7 +7,7 @@ import CoreServices import UniformTypeIdentifiers import SignalUtilitiesKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import SessionMessagingKit diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 34246413a9..b460349b73 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -8,7 +8,7 @@ import DifferenceKit import SessionUIKit import SignalUtilitiesKit import SessionMessagingKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate, ThemedNavigation { diff --git a/SessionSnodeKit/Meta/SessionSnodeKit.h b/SessionSnodeKit/Meta/SessionSnodeKit.h deleted file mode 100644 index 698aa516fd..0000000000 --- a/SessionSnodeKit/Meta/SessionSnodeKit.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -FOUNDATION_EXPORT double SessionSnodeKitVersionNumber; -FOUNDATION_EXPORT const unsigned char SessionSnodeKitVersionString[]; diff --git a/SessionSnodeKit/Utilities/Threading+SSK.swift b/SessionSnodeKit/Utilities/Threading+SSK.swift deleted file mode 100644 index 3424ad438e..0000000000 --- a/SessionSnodeKit/Utilities/Threading+SSK.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation -import SessionUtilitiesKit - -public extension Threading { - static let workQueue = DispatchQueue(label: "SessionSnodeKit.workQueue", qos: .userInitiated) // It's important that this is a serial queue -} diff --git a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift index dcf2b6ea26..7028b12544 100644 --- a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift @@ -5,7 +5,7 @@ import GRDB import Quick import Nimble import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit @@ -23,7 +23,7 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: AsyncSpec { customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self, DeprecatedUIKitMigrationTarget.self ], diff --git a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift index b339cbe090..227c06bf86 100644 --- a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift @@ -5,7 +5,7 @@ import GRDB import Quick import Nimble import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit @@ -23,7 +23,7 @@ class ThreadNotificationSettingsViewModelSpec: AsyncSpec { customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self, DeprecatedUIKitMigrationTarget.self ], diff --git a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift index 5f9fdb51ac..ec9d79c0a2 100644 --- a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift @@ -5,7 +5,7 @@ import GRDB import Quick import Nimble import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit @testable import SessionUIKit @@ -31,7 +31,7 @@ class ThreadSettingsViewModelSpec: AsyncSpec { customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self, DeprecatedUIKitMigrationTarget.self ], diff --git a/SessionTests/Database/DatabaseSpec.swift b/SessionTests/Database/DatabaseSpec.swift index 6e0561e696..5c63cd7fae 100644 --- a/SessionTests/Database/DatabaseSpec.swift +++ b/SessionTests/Database/DatabaseSpec.swift @@ -6,7 +6,7 @@ import Quick import Nimble import SessionUtil import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit @testable import Session @testable import SessionMessagingKit @@ -41,7 +41,7 @@ class DatabaseSpec: QuickSpec { let allMigrations: [Storage.KeyedMigration] = SynchronousStorage.sortedMigrationInfo( migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self, DeprecatedUIKitMigrationTarget.self ] @@ -77,7 +77,7 @@ class DatabaseSpec: QuickSpec { mockStorage.perform( migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self, DeprecatedUIKitMigrationTarget.self ], diff --git a/SessionTests/Session.xctestplan b/SessionTests/Session.xctestplan index fcfc4d45ad..a9c2927686 100644 --- a/SessionTests/Session.xctestplan +++ b/SessionTests/Session.xctestplan @@ -34,7 +34,7 @@ { "containerPath" : "container:Session.xcodeproj", "identifier" : "C3C2A59E255385C100C340D1", - "name" : "SessionSnodeKit" + "name" : "SessionNetworkingKit" }, { "containerPath" : "container:Session.xcodeproj", @@ -87,7 +87,7 @@ "target" : { "containerPath" : "container:Session.xcodeproj", "identifier" : "FDB5DAF92A981C42002C8721", - "name" : "SessionSnodeKitTests" + "name" : "SessionNetworkingKitTests" } }, { diff --git a/SessionTests/Settings/NotificationContentViewModelSpec.swift b/SessionTests/Settings/NotificationContentViewModelSpec.swift index ceda704ea2..65c099d860 100644 --- a/SessionTests/Settings/NotificationContentViewModelSpec.swift +++ b/SessionTests/Settings/NotificationContentViewModelSpec.swift @@ -6,7 +6,7 @@ import Quick import Nimble import SessionUtil import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit @@ -23,7 +23,7 @@ class NotificationContentViewModelSpec: AsyncSpec { customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self, DeprecatedUIKitMigrationTarget.self ], diff --git a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift b/SessionUtilitiesKit/Database/Types/TargetMigrations.swift index b320516d5c..860647c618 100644 --- a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift +++ b/SessionUtilitiesKit/Database/Types/TargetMigrations.swift @@ -23,7 +23,7 @@ public struct TargetMigrations: Comparable { // changing them will result in the migrations running again case session case utilitiesKit - case snodeKit + case networkingKit = "snodeKit" case messagingKit case _deprecatedUIKit = "uiKit" case test diff --git a/SessionUtilitiesKit/General/Logging.swift b/SessionUtilitiesKit/General/Logging.swift index 2dff548f87..9bd2646f9e 100644 --- a/SessionUtilitiesKit/General/Logging.swift +++ b/SessionUtilitiesKit/General/Logging.swift @@ -22,6 +22,14 @@ public extension FeatureStorage { ) } + static func logLevel(group: Log.Group) -> FeatureConfig { + return Dependencies.create( + identifier: "\(Log.Group.identifierPrefix)\(group.name)", + groupIdentifier: "logging", + defaultOption: group.defaultLevel + ) + } + static let allLogLevels: FeatureConfig = Dependencies.create( identifier: "allLogLevels", groupIdentifier: "logging" @@ -50,33 +58,66 @@ public enum Log { case off case `default` + + var label: String { + switch self { + case .off: return "off" + case .verbose: return "verbose" + case .debug: return "debug" + case .info: return "info" + case .warn: return "warn" + case .error: return "error" + case .critical: return "critical" + case .default: return "default" + } + } + } + + public struct Group: Hashable { + public let name: String + public let defaultLevel: Log.Level + + fileprivate static let identifierPrefix: String = "group:" + + private init(name: String, defaultLevel: Log.Level) { + self.name = name + + switch AllLoggingCategories.existingGroup(for: name) { + case .some(let existingGroup): self.defaultLevel = existingGroup.defaultLevel + case .none: + self.defaultLevel = defaultLevel + AllLoggingCategories.register(group: self) + } + } + + @discardableResult public static func create( + _ group: String, + defaultLevel: Log.Level + ) -> Log.Group { + return Log.Group(name: group, defaultLevel: defaultLevel) + } } public struct Category: Hashable { public let rawValue: String - fileprivate let customPrefix: String + fileprivate let group: Group? fileprivate let customSuffix: String public let defaultLevel: Log.Level fileprivate static let identifierPrefix: String = "logLevel-" fileprivate var identifier: String { "\(Category.identifierPrefix)\(rawValue)" } - private init(rawValue: String, customPrefix: String, customSuffix: String, defaultLevel: Log.Level) { + private init(rawValue: String, group: Group?, customSuffix: String, defaultLevel: Log.Level) { + self.rawValue = rawValue + self.group = group + self.customSuffix = customSuffix + /// If we've already registered this category then assume the original has the correct `defaultLevel` and only /// modify the `customPrefix` value switch AllLoggingCategories.existingCategory(for: rawValue) { - case .some(let existingCategory): - self.rawValue = existingCategory.rawValue - self.customPrefix = customPrefix - self.customSuffix = customSuffix - self.defaultLevel = existingCategory.defaultLevel - + case .some(let existingCategory): self.defaultLevel = existingCategory.defaultLevel case .none: - self.rawValue = rawValue - self.customPrefix = customPrefix - self.customSuffix = customSuffix self.defaultLevel = defaultLevel - AllLoggingCategories.register(category: self) } } @@ -86,25 +127,25 @@ public enum Log { self.init( rawValue: identifier.substring(from: Category.identifierPrefix.count), - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .default ) } - public init(rawValue: String, customPrefix: String = "", customSuffix: String = "") { - self.init(rawValue: rawValue, customPrefix: customPrefix, customSuffix: customSuffix, defaultLevel: .default) + public init(rawValue: String, group: Group? = nil, customSuffix: String = "") { + self.init(rawValue: rawValue, group: group, customSuffix: customSuffix, defaultLevel: .default) } @discardableResult public static func create( _ rawValue: String, - customPrefix: String = "", + group: Group? = nil, customSuffix: String = "", defaultLevel: Log.Level ) -> Log.Category { return Log.Category( rawValue: rawValue, - customPrefix: customPrefix, + group: group, customSuffix: customSuffix, defaultLevel: defaultLevel ) @@ -653,12 +694,15 @@ public actor Logger: LoggerType { let defaultLogLevel: Log.Level = dependencies[feature: .logLevel(cat: .default)] let lowestCatLevel: Log.Level = categories .reduce(into: [], { result, next in - guard dependencies[feature: .logLevel(cat: next)] != .default else { - result.append(defaultLogLevel) - return - } + let explicitLevel: Log.Level = dependencies[feature: .logLevel(cat: next)] + let groupLevel: Log.Level? = next.group.map { dependencies[feature: .logLevel(group: $0)] } - result.append(dependencies[feature: .logLevel(cat: next)]) + switch (explicitLevel, groupLevel) { + case (.default, .none): result.append(defaultLogLevel) + case (.default, .default): result.append(defaultLogLevel) + case (_, .none): result.append(explicitLevel) + case (_, .some(let groupLevel)): result.append(min(explicitLevel, groupLevel)) + } }) .min() .defaulting(to: defaultLogLevel) @@ -678,15 +722,15 @@ public actor Logger: LoggerType { /// No point doubling up but we want to allow categories which match the `primaryPrefix` so that we /// have a mechanism for providing a different "default" log level for a specific target .filter { $0.rawValue != primaryPrefix } - .map { "\($0.customPrefix)\($0.rawValue)\($0.customSuffix)" } + .map { "\($0.group.map { "\($0.name):" } ?? "")\($0.rawValue)\($0.customSuffix)" } ) .joined(separator: ", ") - return "[\(prefixes)] " + return "[\(prefixes)]" }() /// Clean up the message if needed (replace double periods with single, trim whitespace, truncate pubkeys) - let logMessage: String = logPrefix + let cleanedMessage: String = logPrefix .appending(message) .replacingOccurrences(of: "...", with: "|||") .replacingOccurrences(of: "..", with: ".") @@ -709,16 +753,17 @@ public actor Logger: LoggerType { return updatedText } - + let ddLogMessage: String = "\(logPrefix) ".appending(cleanedMessage) + let consoleLogMessage: String = "\(logPrefix)[\(level)] ".appending(cleanedMessage) switch level { case .off, .default: return - case .verbose: DDLogVerbose("💙 \(logMessage)", file: file, function: function, line: line) - case .debug: DDLogDebug("💚 \(logMessage)", file: file, function: function, line: line) - case .info: DDLogInfo("💛 \(logMessage)", file: file, function: function, line: line) - case .warn: DDLogWarn("🧡 \(logMessage)", file: file, function: function, line: line) - case .error: DDLogError("❤️ \(logMessage)", file: file, function: function, line: line) - case .critical: DDLogError("🔥 \(logMessage)", file: file, function: function, line: line) + case .verbose: DDLogVerbose("💙 \(ddLogMessage)", file: file, function: function, line: line) + case .debug: DDLogDebug("💚 \(ddLogMessage)", file: file, function: function, line: line) + case .info: DDLogInfo("💛 \(ddLogMessage)", file: file, function: function, line: line) + case .warn: DDLogWarn("🧡 \(ddLogMessage)", file: file, function: function, line: line) + case .error: DDLogError("❤️ \(ddLogMessage)", file: file, function: function, line: line) + case .critical: DDLogError("🔥 \(ddLogMessage)", file: file, function: function, line: line) } let mainCategory: String = (categories.first?.rawValue ?? "General") @@ -730,7 +775,7 @@ public actor Logger: LoggerType { } #if DEBUG - systemLogger?.log(level, logMessage) + systemLogger?.log(level, consoleLogMessage) #endif } } @@ -859,6 +904,7 @@ extension Log.Level: FeatureOption { public struct AllLoggingCategories: FeatureOption { public static let allCases: [AllLoggingCategories] = [] + @ThreadSafeObject private static var registeredGroupDefaults: Set = [] @ThreadSafeObject private static var registeredCategoryDefaults: Set = [] // MARK: - Initialization @@ -866,7 +912,21 @@ public struct AllLoggingCategories: FeatureOption { public let rawValue: Int public init(rawValue: Int) { - self.rawValue = -1 // `0` is a protected value so can't use it + _ = Log.Category.default // Access the `default` log category to ensure it exists + self.rawValue = -1 // `0` is a protected value so can't use it + } + + fileprivate static func register(group: Log.Group) { + guard + !registeredGroupDefaults.contains(where: { existingGroup in + /// **Note:** We only want to use the `rawValue` to distinguish between logging categories + /// as the `defaultLevel` can change via the dev settings and any additional metadata could + /// be file/class specific + group.name == existingGroup.name + }) + else { return } + + _registeredGroupDefaults.performUpdate { $0.inserting(group) } } fileprivate static func register(category: Log.Category) { @@ -882,10 +942,21 @@ public struct AllLoggingCategories: FeatureOption { _registeredCategoryDefaults.performUpdate { $0.inserting(category) } } + fileprivate static func existingGroup(for name: String) -> Log.Group? { + return AllLoggingCategories.registeredGroupDefaults.first(where: { $0.name == name }) + } + fileprivate static func existingCategory(for cat: String) -> Log.Category? { return AllLoggingCategories.registeredCategoryDefaults.first(where: { $0.rawValue == cat }) } + public func currentValues(using dependencies: Dependencies) -> [Log.Group: Log.Level] { + return AllLoggingCategories.registeredGroupDefaults + .reduce(into: [:]) { result, group in + result[group] = dependencies[feature: .logLevel(group: group)] + } + } + public func currentValues(using dependencies: Dependencies) -> [Log.Category: Log.Level] { return AllLoggingCategories.registeredCategoryDefaults .reduce(into: [:]) { result, cat in diff --git a/SessionUtilitiesKit/LibSession/LibSession.swift b/SessionUtilitiesKit/LibSession/LibSession.swift index 916686f2f8..e787e7b4db 100644 --- a/SessionUtilitiesKit/LibSession/LibSession.swift +++ b/SessionUtilitiesKit/LibSession/LibSession.swift @@ -17,6 +17,10 @@ public extension Log.Category { static let libSession: Log.Category = .create("LibSession", defaultLevel: .info) } +public extension Log.Group { + static let libSession: Log.Group = .create("libSession", defaultLevel: .info) +} + // MARK: - Logging extension LibSession { @@ -29,7 +33,13 @@ extension LibSession { ObservationBuilder.observe(.featureGroup(.allLogLevels), using: dependencies) { [dependencies] _ in let currentLogLevels: [Log.Category: Log.Level] = dependencies[feature: .allLogLevels] .currentValues(using: dependencies) - let cDefaultLevel: LOG_LEVEL = (currentLogLevels[.default]?.libSession ?? LOG_LEVEL_OFF) + let currentGroupLogLevels: [Log.Group: Log.Level] = dependencies[feature: .allLogLevels] + .currentValues(using: dependencies) + let targetDefault: Log.Level? = min( + (currentLogLevels[.default] ?? .off), + (currentGroupLogLevels[.libSession] ?? .off) + ) + let cDefaultLevel: LOG_LEVEL = (targetDefault?.libSession ?? LOG_LEVEL_OFF) session_logger_set_level_default(cDefaultLevel) session_logger_reset_level(cDefaultLevel) @@ -57,20 +67,45 @@ extension LibSession { DispatchQueue.global(qos: .background).async { /// Logs from libSession come through in the format: /// `[yyyy-MM-dd hh:mm:ss] [+{lifetime}s] [{cat}:{lvl}|log.hpp:{line}] {message}` - /// We want to remove the extra data because it doesn't help the logs + /// + /// We want to simplify the message because our logging already includes category and timestamp information: + /// `[+{lifetime}s] {message}` let processedMessage: String = { - let logParts: [String] = msg.components(separatedBy: "] ") + let trimmedMsg = msg.trimmingCharacters(in: .whitespacesAndNewlines) - guard logParts.count == 4 else { return msg.trimmingCharacters(in: .whitespacesAndNewlines) } + guard + let timestampRegex: NSRegularExpression = LibSession.timestampRegex, + let messageStartRegex: NSRegularExpression = LibSession.messageStartRegex + else { return trimmedMsg } - let message: String = String(logParts[3]).trimmingCharacters(in: .whitespacesAndNewlines) + let fullRange = NSRange(trimmedMsg.startIndex.. Date: Thu, 7 Aug 2025 15:34:13 +1000 Subject: [PATCH 02/59] Fixed some missing renames --- Session.xcodeproj/project.pbxproj | 4 ---- SessionMessagingKit/Crypto/Crypto+Attachments.swift | 2 +- .../Database/Migrations/_026_MessageDeduplicationTable.swift | 2 +- .../Database/Migrations/_028_RenameAttachments.swift | 2 +- .../Database/Models/MessageDeduplication.swift | 2 +- .../Jobs/RetrieveDefaultOpenGroupRoomsJob.swift | 2 +- .../Sending & Receiving/AttachmentUploader.swift | 2 +- SessionMessagingKit/Utilities/AttachmentManager.swift | 2 +- SessionMessagingKit/Utilities/ExtensionHelper.swift | 2 +- .../Database/Models/MessageDeduplicationSpec.swift | 2 +- SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift | 2 +- .../_TestUtilities/MockExtensionHelper.swift | 2 +- SessionTests/Onboarding/OnboardingSpec.swift | 2 +- 13 files changed, 12 insertions(+), 16 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index f5e0730b6e..f669d6409b 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -1019,7 +1019,6 @@ FDE33BBE2D5C3AF100E56F42 /* _023_GroupsExpiredFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE33BBD2D5C3AE800E56F42 /* _023_GroupsExpiredFlag.swift */; }; FDE519F72AB7CDC700450C53 /* Result+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE519F62AB7CDC700450C53 /* Result+Utilities.swift */; }; FDE5218E2E03A06B00061B8E /* AttachmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE5218D2E03A06700061B8E /* AttachmentManager.swift */; }; - FDE521902E04CCEB00061B8E /* AVURLAsset+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE5218F2E04CCE600061B8E /* AVURLAsset+Utilities.swift */; }; FDE521942E050B1100061B8E /* DismissCallbackAVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE521932E050B0800061B8E /* DismissCallbackAVPlayerViewController.swift */; }; FDE5219A2E08DBB800061B8E /* ImageLoading+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE521992E08DBB000061B8E /* ImageLoading+Convenience.swift */; }; FDE5219C2E08E76C00061B8E /* SessionAsyncImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE5219B2E08E76600061B8E /* SessionAsyncImage.swift */; }; @@ -2284,7 +2283,6 @@ FDE33BBD2D5C3AE800E56F42 /* _023_GroupsExpiredFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _023_GroupsExpiredFlag.swift; sourceTree = ""; }; FDE519F62AB7CDC700450C53 /* Result+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Utilities.swift"; sourceTree = ""; }; FDE5218D2E03A06700061B8E /* AttachmentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentManager.swift; sourceTree = ""; }; - FDE5218F2E04CCE600061B8E /* AVURLAsset+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVURLAsset+Utilities.swift"; sourceTree = ""; }; FDE521932E050B0800061B8E /* DismissCallbackAVPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissCallbackAVPlayerViewController.swift; sourceTree = ""; }; FDE521992E08DBB000061B8E /* ImageLoading+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImageLoading+Convenience.swift"; sourceTree = ""; }; FDE5219B2E08E76600061B8E /* SessionAsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionAsyncImage.swift; sourceTree = ""; }; @@ -3952,7 +3950,6 @@ isa = PBXGroup; children = ( FDB3DA892E2482A400148F8D /* AVURLAsset+Utilities.swift */, - FDE5218F2E04CCE600061B8E /* AVURLAsset+Utilities.swift */, 94C58AC82D2E036E00609195 /* Permissions.swift */, FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */, FD78EA052DDEC8F100D55B50 /* AsyncSequence+Utilities.swift */, @@ -6345,7 +6342,6 @@ FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */, FD09797D27FBDB2000936362 /* Notification+Utilities.swift in Sources */, FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */, - FDE521902E04CCEB00061B8E /* AVURLAsset+Utilities.swift in Sources */, FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */, FDB3DA8B2E24834000148F8D /* AVURLAsset+Utilities.swift in Sources */, FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */, diff --git a/SessionMessagingKit/Crypto/Crypto+Attachments.swift b/SessionMessagingKit/Crypto/Crypto+Attachments.swift index 6caca2f459..ceb7cca191 100644 --- a/SessionMessagingKit/Crypto/Crypto+Attachments.swift +++ b/SessionMessagingKit/Crypto/Crypto+Attachments.swift @@ -4,7 +4,7 @@ import Foundation import CommonCrypto -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Encryption diff --git a/SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift b/SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift index fd993ac86b..5addd66869 100644 --- a/SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift +++ b/SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit /// The different platforms use different approaches for message deduplication but in the future we want to shift the database logic into /// `libSession` so it makes sense to try to define a longer-term deduplication approach we we can use in `libSession`, additonally diff --git a/SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift b/SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift index 2c92c21d11..a4e8969695 100644 --- a/SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift +++ b/SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift @@ -3,7 +3,7 @@ import Foundation import UniformTypeIdentifiers import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit /// This migration renames all attachments to use a hash of the download url for the filename instead of a random UUID (means we can diff --git a/SessionMessagingKit/Database/Models/MessageDeduplication.swift b/SessionMessagingKit/Database/Models/MessageDeduplication.swift index 15269d25b2..6a73d52936 100644 --- a/SessionMessagingKit/Database/Models/MessageDeduplication.swift +++ b/SessionMessagingKit/Database/Models/MessageDeduplication.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift index a864b6c2fe..06d2b8e3b9 100644 --- a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift +++ b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift b/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift index 532c5fe5ec..dec4a6495e 100644 --- a/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift +++ b/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - AttachmentUploader diff --git a/SessionMessagingKit/Utilities/AttachmentManager.swift b/SessionMessagingKit/Utilities/AttachmentManager.swift index 5746315ac1..394cb1fef0 100644 --- a/SessionMessagingKit/Utilities/AttachmentManager.swift +++ b/SessionMessagingKit/Utilities/AttachmentManager.swift @@ -7,7 +7,7 @@ import Combine import UniformTypeIdentifiers import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton diff --git a/SessionMessagingKit/Utilities/ExtensionHelper.swift b/SessionMessagingKit/Utilities/ExtensionHelper.swift index 0b04520659..ed44a08963 100644 --- a/SessionMessagingKit/Utilities/ExtensionHelper.swift +++ b/SessionMessagingKit/Utilities/ExtensionHelper.swift @@ -1,7 +1,7 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton diff --git a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift index 458c7cc357..bd36080603 100644 --- a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift +++ b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift index 16d88729ea..ca8c0919b6 100644 --- a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift +++ b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtil -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick diff --git a/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift b/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift index e68a787acd..06a4562e50 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift @@ -1,7 +1,7 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit @testable import SessionMessagingKit diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index e6a4e41559..214fd1d28d 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -9,7 +9,7 @@ import SessionUIKit import SessionUtilitiesKit @testable import Session -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class OnboardingSpec: AsyncSpec { From 4ae24f879a7959d1f88231b55e40c8400f6fedf0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 7 Aug 2025 16:31:14 +1000 Subject: [PATCH 03/59] Fixed a couple of build issues --- .../Utilities/ExtensionHelperSpec.swift | 14 +++++++------- SessionTests/Onboarding/OnboardingSpec.swift | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift index ca8c0919b6..5cd9216cab 100644 --- a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift +++ b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift @@ -738,7 +738,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2332,7 +2332,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2369,7 +2369,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2385,7 +2385,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2422,7 +2422,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2438,7 +2438,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2480,7 +2480,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index 214fd1d28d..e351b5a8b5 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -26,7 +26,7 @@ class OnboardingSpec: AsyncSpec { customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, - SNSnodeKit.self, + SNNetworkingKit.self, SNMessagingKit.self, DeprecatedUIKitMigrationTarget.self ], From 08d01a52ec379f792e3b1a78ea1c40bda20394ce Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 8 Aug 2025 13:04:35 +1000 Subject: [PATCH 04/59] Moved all migrations into one target, simplified migration logic --- Session.xcodeproj/project.pbxproj | 386 +++++++++--------- Session/Meta/AppDelegate.swift | 3 - SessionMessagingKit/Configuration.swift | 99 +++-- .../_001_SUK_InitialSetupMigration.swift | 6 +- .../_002_SUK_SetupStandardJobs.swift | 6 +- .../_003_SUK_YDBToGRDBMigration.swift | 5 +- .../_004_SNK_InitialSetupMigration.swift | 5 +- .../_005_SNK_SetupStandardJobs.swift | 5 +- ...t => _006_SMK_InitialSetupMigration.swift} | 13 +- ...swift => _007_SMK_SetupStandardJobs.swift} | 5 +- .../_008_SNK_YDBToGRDBMigration.swift | 6 +- ...wift => _009_SMK_YDBToGRDBMigration.swift} | 5 +- ...10_FlagMessageHashAsDeletedOrInvalid.swift | 5 +- ...cyYDB.swift => _011_RemoveLegacyYDB.swift} | 5 +- .../Migrations/_012_AddJobPriority.swift | 6 +- ... => _013_FixDeletedMessageReadState.swift} | 5 +- ...ft => _014_FixHiddenModAdminSupport.swift} | 5 +- ...> _015_HomeQueryOptimisationIndexes.swift} | 5 +- .../Migrations/_016_ThemePreferences.swift | 27 +- ...ojiReacts.swift => _017_EmojiReacts.swift} | 5 +- ...n.swift => _018_OpenGroupPermission.swift} | 5 +- ...oFTS.swift => _019_AddThreadIdToFTS.swift} | 7 +- .../Migrations/_020_AddJobUniqueHash.swift | 6 +- ...ddSnodeReveivedMessageInfoPrimaryKey.swift | 6 +- .../Migrations/_022_DropSnodeCache.swift | 5 +- .../_023_SplitSnodeReceivedMessageInfo.swift | 6 +- .../_024_ResetUserConfigLastHashes.swift | 6 +- ...wift => _025_AddPendingReadReceipts.swift} | 5 +- ...Needed.swift => _026_AddFTSIfNeeded.swift} | 7 +- ...es.swift => _027_SessionUtilChanges.swift} | 7 +- ..._028_GenerateInitialUserConfigDumps.swift} | 5 +- ... _029_BlockCommunityMessageRequests.swift} | 5 +- ...MakeBrokenProfileTimestampsNullable.swift} | 5 +- ...ft => _031_RebuildFTSIfNeeded_2_4_5.swift} | 13 +- ...2_DisappearingMessagesConfiguration.swift} | 5 +- ...t => _033_ScheduleAppUpdateCheckJob.swift} | 5 +- ...swift => _034_AddMissingWhisperFlag.swift} | 5 +- ....swift => _035_ReworkRecipientState.swift} | 7 +- ....swift => _036_GroupsRebuildChanges.swift} | 7 +- ...lag.swift => _037_GroupsExpiredFlag.swift} | 5 +- ...=> _038_FixBustedInteractionVariant.swift} | 5 +- ...9_DropLegacyClosedGroupKeyPairTable.swift} | 5 +- ...t => _040_MessageDeduplicationTable.swift} | 9 +- ...41_RenameTableSettingToKeyValueStore.swift | 6 +- ...ft => _042_MoveSettingsToLibSession.swift} | 5 +- ...nts.swift => _043_RenameAttachments.swift} | 5 +- ...lag.swift => _044_AddProMessageFlag.swift} | 5 +- .../Models/MessageDeduplication.swift | 10 +- .../Models/MessageDeduplicationSpec.swift | 4 +- .../Jobs/DisplayPictureDownloadJobSpec.swift | 5 +- .../Jobs/MessageSendJobSpec.swift | 5 +- ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 5 +- .../LibSession/LibSessionGroupInfoSpec.swift | 6 +- .../LibSessionGroupMembersSpec.swift | 5 +- .../LibSession/LibSessionSpec.swift | 5 +- .../Open Groups/OpenGroupManagerSpec.swift | 5 +- .../MessageReceiverGroupsSpec.swift | 6 +- .../MessageSenderGroupsSpec.swift | 5 +- .../MessageSenderSpec.swift | 5 +- .../Pollers/CommunityPollerSpec.swift | 5 +- .../Utilities/ExtensionHelperSpec.swift | 5 +- SessionNetworkingKit/Configuration.swift | 35 -- .../ShareNavController.swift | 1 - ...eadDisappearingMessagesViewModelSpec.swift | 7 +- ...eadNotificationSettingsViewModelSpec.swift | 7 +- .../ThreadSettingsViewModelSpec.swift | 7 +- SessionTests/Database/DatabaseSpec.swift | 178 ++++---- SessionTests/Onboarding/OnboardingSpec.swift | 7 +- .../NotificationContentViewModelSpec.swift | 7 +- SessionUtilitiesKit/Configuration.swift | 32 +- SessionUtilitiesKit/Database/Storage.swift | 71 +--- .../Database/Types/Migration.swift | 16 +- .../Database/Types/TargetMigrations.swift | 76 ---- .../DatabaseMigrator+Utilities.swift | 22 - .../Database/Models/IdentitySpec.swift | 4 +- .../JobRunner/JobRunnerSpec.swift | 12 +- SignalUtilitiesKit/Utilities/AppSetup.swift | 10 +- _SharedTestUtilities/SynchronousStorage.swift | 18 +- 78 files changed, 474 insertions(+), 881 deletions(-) rename SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift => SessionMessagingKit/Database/Migrations/_001_SUK_InitialSetupMigration.swift (94%) rename SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift => SessionMessagingKit/Database/Migrations/_002_SUK_SetupStandardJobs.swift (89%) rename SessionNetworkingKit/Database/Migrations/_003_YDBToGRDBMigration.swift => SessionMessagingKit/Database/Migrations/_003_SUK_YDBToGRDBMigration.swift (70%) rename SessionNetworkingKit/Database/Migrations/_001_InitialSetupMigration.swift => SessionMessagingKit/Database/Migrations/_004_SNK_InitialSetupMigration.swift (90%) rename SessionNetworkingKit/Database/Migrations/_002_SetupStandardJobs.swift => SessionMessagingKit/Database/Migrations/_005_SNK_SetupStandardJobs.swift (91%) rename SessionMessagingKit/Database/Migrations/{_001_InitialSetupMigration.swift => _006_SMK_InitialSetupMigration.swift} (97%) rename SessionMessagingKit/Database/Migrations/{_002_SetupStandardJobs.swift => _007_SMK_SetupStandardJobs.swift} (93%) rename SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift => SessionMessagingKit/Database/Migrations/_008_SNK_YDBToGRDBMigration.swift (69%) rename SessionMessagingKit/Database/Migrations/{_003_YDBToGRDBMigration.swift => _009_SMK_YDBToGRDBMigration.swift} (79%) rename SessionNetworkingKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift => SessionMessagingKit/Database/Migrations/_010_FlagMessageHashAsDeletedOrInvalid.swift (81%) rename SessionMessagingKit/Database/Migrations/{_004_RemoveLegacyYDB.swift => _011_RemoveLegacyYDB.swift} (78%) rename SessionUtilitiesKit/Database/Migrations/_004_AddJobPriority.swift => SessionMessagingKit/Database/Migrations/_012_AddJobPriority.swift (90%) rename SessionMessagingKit/Database/Migrations/{_005_FixDeletedMessageReadState.swift => _013_FixDeletedMessageReadState.swift} (83%) rename SessionMessagingKit/Database/Migrations/{_006_FixHiddenModAdminSupport.swift => _014_FixHiddenModAdminSupport.swift} (85%) rename SessionMessagingKit/Database/Migrations/{_007_HomeQueryOptimisationIndexes.swift => _015_HomeQueryOptimisationIndexes.swift} (81%) rename Session/Database/Migrations/_001_ThemePreferences.swift => SessionMessagingKit/Database/Migrations/_016_ThemePreferences.swift (78%) rename SessionMessagingKit/Database/Migrations/{_008_EmojiReacts.swift => _017_EmojiReacts.swift} (91%) rename SessionMessagingKit/Database/Migrations/{_009_OpenGroupPermission.swift => _018_OpenGroupPermission.swift} (83%) rename SessionMessagingKit/Database/Migrations/{_010_AddThreadIdToFTS.swift => _019_AddThreadIdToFTS.swift} (82%) rename SessionUtilitiesKit/Database/Migrations/_005_AddJobUniqueHash.swift => SessionMessagingKit/Database/Migrations/_020_AddJobUniqueHash.swift (76%) rename SessionNetworkingKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift => SessionMessagingKit/Database/Migrations/_021_AddSnodeReveivedMessageInfoPrimaryKey.swift (91%) rename SessionNetworkingKit/Database/Migrations/_006_DropSnodeCache.swift => SessionMessagingKit/Database/Migrations/_022_DropSnodeCache.swift (86%) rename SessionNetworkingKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift => SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift (96%) rename SessionNetworkingKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift => SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift (84%) rename SessionMessagingKit/Database/Migrations/{_011_AddPendingReadReceipts.swift => _025_AddPendingReadReceipts.swift} (89%) rename SessionMessagingKit/Database/Migrations/{_012_AddFTSIfNeeded.swift => _026_AddFTSIfNeeded.swift} (80%) rename SessionMessagingKit/Database/Migrations/{_013_SessionUtilChanges.swift => _027_SessionUtilChanges.swift} (98%) rename SessionMessagingKit/Database/Migrations/{_014_GenerateInitialUserConfigDumps.swift => _028_GenerateInitialUserConfigDumps.swift} (98%) rename SessionMessagingKit/Database/Migrations/{_015_BlockCommunityMessageRequests.swift => _029_BlockCommunityMessageRequests.swift} (94%) rename SessionMessagingKit/Database/Migrations/{_016_MakeBrokenProfileTimestampsNullable.swift => _030_MakeBrokenProfileTimestampsNullable.swift} (89%) rename SessionMessagingKit/Database/Migrations/{_017_RebuildFTSIfNeeded_2_4_5.swift => _031_RebuildFTSIfNeeded_2_4_5.swift} (85%) rename SessionMessagingKit/Database/Migrations/{_018_DisappearingMessagesConfiguration.swift => _032_DisappearingMessagesConfiguration.swift} (97%) rename SessionMessagingKit/Database/Migrations/{_019_ScheduleAppUpdateCheckJob.swift => _033_ScheduleAppUpdateCheckJob.swift} (84%) rename SessionMessagingKit/Database/Migrations/{_020_AddMissingWhisperFlag.swift => _034_AddMissingWhisperFlag.swift} (81%) rename SessionMessagingKit/Database/Migrations/{_021_ReworkRecipientState.swift => _035_ReworkRecipientState.swift} (97%) rename SessionMessagingKit/Database/Migrations/{_022_GroupsRebuildChanges.swift => _036_GroupsRebuildChanges.swift} (97%) rename SessionMessagingKit/Database/Migrations/{_023_GroupsExpiredFlag.swift => _037_GroupsExpiredFlag.swift} (76%) rename SessionMessagingKit/Database/Migrations/{_024_FixBustedInteractionVariant.swift => _038_FixBustedInteractionVariant.swift} (83%) rename SessionMessagingKit/Database/Migrations/{_025_DropLegacyClosedGroupKeyPairTable.swift => _039_DropLegacyClosedGroupKeyPairTable.swift} (74%) rename SessionMessagingKit/Database/Migrations/{_026_MessageDeduplicationTable.swift => _040_MessageDeduplicationTable.swift} (98%) rename SessionUtilitiesKit/Database/Migrations/_006_RenameTableSettingToKeyValueStore.swift => SessionMessagingKit/Database/Migrations/_041_RenameTableSettingToKeyValueStore.swift (68%) rename SessionMessagingKit/Database/Migrations/{_027_MoveSettingsToLibSession.swift => _042_MoveSettingsToLibSession.swift} (97%) rename SessionMessagingKit/Database/Migrations/{_028_RenameAttachments.swift => _043_RenameAttachments.swift} (99%) rename SessionMessagingKit/Database/Migrations/{_029_AddProMessageFlag.swift => _044_AddProMessageFlag.swift} (76%) delete mode 100644 SessionNetworkingKit/Configuration.swift delete mode 100644 SessionUtilitiesKit/Database/Types/TargetMigrations.swift delete mode 100644 SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index f669d6409b..1fc3020a15 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -98,7 +98,7 @@ 7B4EF25A2934743000CB351D /* SessionTableViewTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4EF2592934743000CB351D /* SessionTableViewTitleView.swift */; }; 7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 7B50D64C28AC7CF80086CCEC /* silence.aiff */; }; 7B5233C42900E90F00F8F375 /* SessionLabelCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C32900E90F00F8F375 /* SessionLabelCarouselView.swift */; }; - 7B5233C6290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C5290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift */; }; + 7B5233C6290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C5290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift */; }; 7B5802992AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5802982AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift */; }; 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; }; 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; }; @@ -107,7 +107,7 @@ 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */; }; 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; }; 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682228A4C1210069F315 /* UpdateTypes.swift */; }; - 7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */; }; + 7B81682828B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift */; }; 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682928B6F1420069F315 /* ReactionResponse.swift */; }; 7B81682C28B72F480069F315 /* PendingChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682B28B72F480069F315 /* PendingChange.swift */; }; 7B81FB5A2AB01B17002FB267 /* LoadingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81FB582AB01AA8002FB267 /* LoadingIndicatorView.swift */; }; @@ -129,7 +129,7 @@ 7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA68908272A27BE00EFC32F /* SessionCall.swift */; }; 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */; }; 7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */; }; - 7BAA7B6628D2DE4700AE1489 /* _009_OpenGroupPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */; }; + 7BAA7B6628D2DE4700AE1489 /* _018_OpenGroupPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAA7B6528D2DE4700AE1489 /* _018_OpenGroupPermission.swift */; }; 7BAADFCC27B0EF23007BCF92 /* CallVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAADFCB27B0EF23007BCF92 /* CallVideoView.swift */; }; 7BAADFCE27B215FE007BCF92 /* UIView+Draggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAADFCD27B215FE007BCF92 /* UIView+Draggable.swift */; }; 7BAF54CF27ACCEEC003D12F8 /* GlobalSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54CC27ACCEEC003D12F8 /* GlobalSearchViewController.swift */; }; @@ -174,7 +174,6 @@ 9422EE2B2B8C3A97004C740D /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9422EE2A2B8C3A97004C740D /* String+Utilities.swift */; }; 942ADDD42D9F9613006E0BB0 /* NewTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 942ADDD32D9F960C006E0BB0 /* NewTagView.swift */; }; 943C6D822B75E061004ACE64 /* Message+DisappearingMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */; }; - 945D9C582D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D9C572D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift */; }; 946F5A732D5DA3AC00A5ADCE /* Punycode in Frameworks */ = {isa = PBXBuildFile; productRef = 946F5A722D5DA3AC00A5ADCE /* Punycode */; }; 9473386E2BDF5F3E00B9E169 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */; }; 9479981C2DD44ADC008F5CD5 /* ThreadNotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9479981B2DD44AC5008F5CD5 /* ThreadNotificationSettingsViewModel.swift */; }; @@ -183,7 +182,6 @@ 947D7FD72D509FC900E8E413 /* SessionNetworkAPI+Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD22D509FC900E8E413 /* SessionNetworkAPI+Network.swift */; }; 947D7FD82D509FC900E8E413 /* SessionNetworkAPI+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD02D509FC900E8E413 /* SessionNetworkAPI+Database.swift */; }; 947D7FDE2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FDB2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift */; }; - 947D7FE32D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE22D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift */; }; 947D7FE72D51837200E8E413 /* ArrowCapsule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE42D51837200E8E413 /* ArrowCapsule.swift */; }; 947D7FE82D51837200E8E413 /* PopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE52D51837200E8E413 /* PopoverView.swift */; }; 947D7FE92D51837200E8E413 /* Text+CopyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE62D51837200E8E413 /* Text+CopyButton.swift */; }; @@ -202,7 +200,7 @@ 94C58AC92D2E037200609195 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C58AC82D2E036E00609195 /* Permissions.swift */; }; 94CD95BB2E08D9E00097754D /* SessionProBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95BA2E08D9D40097754D /* SessionProBadge.swift */; }; 94CD95BD2E09083C0097754D /* LibSession+Pro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95BC2E0908340097754D /* LibSession+Pro.swift */; }; - 94CD95C12E0CBF430097754D /* _029_AddProMessageFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95C02E0CBF1C0097754D /* _029_AddProMessageFlag.swift */; }; + 94CD95C12E0CBF430097754D /* _044_AddProMessageFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95C02E0CBF1C0097754D /* _044_AddProMessageFlag.swift */; }; 94CD962D2E1B85920097754D /* InputViewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD962B2E1B85920097754D /* InputViewButton.swift */; }; 94CD962E2E1B85920097754D /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD962A2E1B85920097754D /* InputTextView.swift */; }; 94CD96302E1B88430097754D /* CGRect+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD962F2E1B88430097754D /* CGRect+Utilities.swift */; }; @@ -375,7 +373,6 @@ C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */; }; C3C2A5A3255385C100C340D1 /* SessionNetworkingKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A5A1255385C100C340D1 /* SessionNetworkingKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; C3C2A5A7255385C100C340D1 /* SessionNetworkingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B9255385ED00C340D1 /* Configuration.swift */; }; C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */; }; C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; @@ -430,7 +427,7 @@ FD02CC142C3677E6009AB976 /* Request+OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */; }; FD02CC162C3681EF009AB976 /* RevokeSubaccountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */; }; FD05593D2DFA3A2800DC48CE /* VoipPayloadKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD05593C2DFA3A2200DC48CE /* VoipPayloadKey.swift */; }; - FD05594E2E012D2700DC48CE /* _028_RenameAttachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD05594D2E012D1A00DC48CE /* _028_RenameAttachments.swift */; }; + FD05594E2E012D2700DC48CE /* _043_RenameAttachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD05594D2E012D1A00DC48CE /* _043_RenameAttachments.swift */; }; FD0559562E026E1B00DC48CE /* ObservingDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0559542E026CC900DC48CE /* ObservingDatabase.swift */; }; FD0606C32BCE13ED00C3816E /* MessageRequestFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0606C22BCE13ED00C3816E /* MessageRequestFooterView.swift */; }; FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */; }; @@ -452,7 +449,7 @@ FD09799927FFC1A300936362 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799827FFC1A300936362 /* Attachment.swift */; }; FD09799B27FFC82D00936362 /* Quote.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799A27FFC82D00936362 /* Quote.swift */; }; FD09B7E328865FDA00ED0B66 /* HighlightMentionBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E228865FDA00ED0B66 /* HighlightMentionBackgroundView.swift */; }; - FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */; }; + FD09B7E5288670BB00ED0B66 /* _017_EmojiReacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E4288670BB00ED0B66 /* _017_EmojiReacts.swift */; }; FD09B7E7288670FD00ED0B66 /* Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E6288670FD00ED0B66 /* Reaction.swift */; }; FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */; }; FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E728264937000CE219 /* MediaDetailViewController.swift */; }; @@ -475,18 +472,12 @@ FD16AB5B2A1DD7CA0083D849 /* PlaceholderIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */; }; FD16AB5F2A1DD98F0083D849 /* ProfilePictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */; }; FD16AB612A1DD9B60083D849 /* ProfilePictureView+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */; }; - FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */; }; - FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */; }; + FD17D79927F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift */; }; FD17D7A127F40D2500122BE0 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD28A4F527EAD44C00FF65E7 /* Storage.swift */; }; - FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */; }; - FD17D7A427F40F8100122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7A327F40F8100122BE0 /* _003_YDBToGRDBMigration.swift */; }; + FD17D7A227F40F0500122BE0 /* _006_SMK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79527F3E04600122BE0 /* _006_SMK_InitialSetupMigration.swift */; }; FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */; }; FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B727F51ECA00122BE0 /* Migration.swift */; }; - FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */; }; - FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */; }; - FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C927F546D900122BE0 /* _001_InitialSetupMigration.swift */; }; FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E427F6A09900122BE0 /* Identity.swift */; }; - FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */; }; FD19363C2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */; }; FD19363F2ACA66DE004BCF0F /* DatabaseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD19363E2ACA66DE004BCF0F /* DatabaseSpec.swift */; }; FD1A553E2E14BE11003761E4 /* PagedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A553D2E14BE0E003761E4 /* PagedData.swift */; }; @@ -494,7 +485,7 @@ FD1A55432E179AED003761E4 /* ObservableKeyEvent+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A55422E179AE6003761E4 /* ObservableKeyEvent+Utilities.swift */; }; FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */; }; FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; }; - FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */; }; + FD1D732E2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D732D2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift */; }; FD22726B2C32911C004D8A6C /* SendReadReceiptsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272532C32911A004D8A6C /* SendReadReceiptsJob.swift */; }; FD22726C2C32911C004D8A6C /* GroupLeavingJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272542C32911A004D8A6C /* GroupLeavingJob.swift */; }; FD22726D2C32911C004D8A6C /* CheckForAppUpdatesJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272552C32911A004D8A6C /* CheckForAppUpdatesJob.swift */; }; @@ -544,9 +535,7 @@ FD2272D42C34ECE1004D8A6C /* BencodeEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272D32C34ECE1004D8A6C /* BencodeEncoder.swift */; }; FD2272D62C34ED6A004D8A6C /* RetryWithDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */; }; FD2272D82C34EDE7004D8A6C /* SnodeAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272D72C34EDE6004D8A6C /* SnodeAPIEndpoint.swift */; }; - FD2272D92C34EED6004D8A6C /* _001_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */; }; FD2272DD2C34EFFA004D8A6C /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */; }; - FD2272DE2C34F11F004D8A6C /* _001_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */; }; FD2272E02C3502BE004D8A6C /* Setting+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DF2C3502BE004D8A6C /* Setting+Theme.swift */; }; FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E52C351378004D8A6C /* SUIKImageFormat.swift */; }; FD2272EA2C351CA7004D8A6C /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E92C351CA7004D8A6C /* Threading.swift */; }; @@ -622,8 +611,8 @@ FD336F742CABB97800C0B51B /* PreparedRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150302CA24310005B08A1 /* PreparedRequestSpec.swift */; }; FD336F752CABB97800C0B51B /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150312CA24310005B08A1 /* RequestSpec.swift */; }; FD336F762CABB97800C0B51B /* BencodeResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD383722AFDD6D7001367F2 /* BencodeResponseSpec.swift */; }; - FD3559462CC1FF200088F2A9 /* _020_AddMissingWhisperFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3559452CC1FF140088F2A9 /* _020_AddMissingWhisperFlag.swift */; }; - FD368A6829DE8F9C000DBF1E /* _012_AddFTSIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */; }; + FD3559462CC1FF200088F2A9 /* _034_AddMissingWhisperFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3559452CC1FF140088F2A9 /* _034_AddMissingWhisperFlag.swift */; }; + FD368A6829DE8F9C000DBF1E /* _026_AddFTSIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6729DE8F9B000DBF1E /* _026_AddFTSIfNeeded.swift */; }; FD368A6A29DE9E30000DBF1E /* UIContextualAction+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */; }; FD3765DF2AD8F03100DC1489 /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; }; FD3765E02AD8F05100DC1489 /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; }; @@ -651,8 +640,8 @@ FD37EA0128A60473003AE748 /* UIKit+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0028A60473003AE748 /* UIKit+Theme.swift */; }; FD37EA0328A9FDCC003AE748 /* HelpViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */; }; FD37EA0528AA00C1003AE748 /* NotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0428AA00C1003AE748 /* NotificationSettingsViewModel.swift */; }; - FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */; }; - FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */; }; + FD37EA0D28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0C28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift */; }; + FD37EA0F28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift */; }; FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */; }; FD37EA1728AC5605003AE748 /* NotificationContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1628AC5605003AE748 /* NotificationContentViewModel.swift */; }; FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */; }; @@ -676,7 +665,7 @@ FD428B192B4B576F006D0888 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B182B4B576F006D0888 /* AppContext.swift */; }; FD428B1B2B4B6098006D0888 /* Notifications+Lifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B1A2B4B6098006D0888 /* Notifications+Lifecycle.swift */; }; FD428B1F2B4B758B006D0888 /* AppReadiness.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B1E2B4B758B006D0888 /* AppReadiness.swift */; }; - FD428B232B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B222B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift */; }; + FD428B232B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B222B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift */; }; FD42ECCE2E287CD4002D03EA /* ThemeColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD42ECCD2E287CD1002D03EA /* ThemeColor.swift */; }; FD42ECD02E289261002D03EA /* ThemeLinearGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD42ECCF2E289257002D03EA /* ThemeLinearGradient.swift */; }; FD42ECD22E3071DE002D03EA /* ThemeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD42ECD12E3071DC002D03EA /* ThemeText.swift */; }; @@ -705,7 +694,7 @@ FD4BB22B2D63F20700D0DC3D /* MigrationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4BB22A2D63F20600D0DC3D /* MigrationHelper.swift */; }; FD4BB22C2D63FA8600D0DC3D /* (null) in Sources */ = {isa = PBXBuildFile; }; FD4C4E9C2B02E2A300C72199 /* DisplayPictureError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C4E9B2B02E2A300C72199 /* DisplayPictureError.swift */; }; - FD4C53AF2CC1D62E003B10F4 /* _021_ReworkRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */; }; + FD4C53AF2CC1D62E003B10F4 /* _035_ReworkRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C53AE2CC1D61E003B10F4 /* _035_ReworkRecipientState.swift */; }; FD52090328B4680F006098F6 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090228B4680F006098F6 /* RadioButton.swift */; }; FD52090528B4915F006098F6 /* PrivacySettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */; }; FD52CB5A2E12166F00A4DA70 /* OnboardingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52CB592E12166D00A4DA70 /* OnboardingSpec.swift */; }; @@ -729,7 +718,6 @@ FD5E93D12C100FD70038C25A /* FileUploadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */; }; FD5E93D22C12B0580038C25A /* AppVersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* AppVersionResponse.swift */; }; FD61FCF92D308CC9005752DE /* GroupMemberSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD61FCF82D308CC5005752DE /* GroupMemberSpec.swift */; }; - FD61FCFB2D34A5EA005752DE /* _007_SplitSnodeReceivedMessageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD61FCFA2D34A5DE005752DE /* _007_SplitSnodeReceivedMessageInfo.swift */; }; FD65318A2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; FD65318B2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; FD65318C2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; @@ -753,17 +741,15 @@ FD6A393B2C2AD3A300762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393A2C2AD3A300762359 /* Nimble */; }; FD6A393D2C2AD3AC00762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393C2C2AD3AC00762359 /* Nimble */; }; FD6A39412C2AD3B600762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39402C2AD3B600762359 /* Nimble */; }; - FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; }; FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */; }; FD6D9CF92CA152B300F706A8 /* Session+SNUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754BB2C9B9A8E002A2623 /* Session+SNUIKit.swift */; }; FD6DA9CF2D015B440092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9CE2D015B440092085A /* Lucide */; }; FD6DA9D22D0160F10092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9D12D0160F10092085A /* Lucide */; }; - FD6DF00B2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */; }; FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */; }; FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; - FD70F25C2DC1F184003729B7 /* _026_MessageDeduplicationTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD70F25B2DC1F176003729B7 /* _026_MessageDeduplicationTable.swift */; }; + FD70F25C2DC1F184003729B7 /* _040_MessageDeduplicationTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD70F25B2DC1F176003729B7 /* _040_MessageDeduplicationTable.swift */; }; FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */; }; - FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */; }; + FD7115F228C6CB3900B47552 /* _019_AddThreadIdToFTS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F128C6CB3900B47552 /* _019_AddThreadIdToFTS.swift */; }; FD7115F428C71EB200B47552 /* ThreadDisappearingMessagesSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F328C71EB200B47552 /* ThreadDisappearingMessagesSettingsViewModel.swift */; }; FD7115F828C8151C00B47552 /* DisposableBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F728C8151C00B47552 /* DisposableBarButtonItem.swift */; }; FD7115FA28C8153400B47552 /* UIBarButtonItem+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F928C8153400B47552 /* UIBarButtonItem+Combine.swift */; }; @@ -817,12 +803,12 @@ FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */; }; FD7728962849E7E90018502F /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728952849E7E90018502F /* String+Utilities.swift */; }; FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728972849E8110018502F /* UITableView+ReusableView.swift */; }; - FD778B6429B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */; }; + FD778B6429B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD778B6329B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift */; }; FD78E9EE2DD6D32500D55B50 /* ImageDataManager+Singleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB11A622DD5BDDD00BEF49F /* ImageDataManager+Singleton.swift */; }; FD78E9F22DDA9EA200D55B50 /* MockImageDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F12DDA9E9B00D55B50 /* MockImageDataManager.swift */; }; FD78E9F42DDABA4F00D55B50 /* AttachmentUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F32DDABA4200D55B50 /* AttachmentUploader.swift */; }; FD78E9F62DDD43AD00D55B50 /* Mutation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F52DDD43AB00D55B50 /* Mutation.swift */; }; - FD78E9FA2DDD74D200D55B50 /* _027_MoveSettingsToLibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F72DDD742100D55B50 /* _027_MoveSettingsToLibSession.swift */; }; + FD78E9FA2DDD74D200D55B50 /* _042_MoveSettingsToLibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F72DDD742100D55B50 /* _042_MoveSettingsToLibSession.swift */; }; FD78E9FD2DDD97F200D55B50 /* Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9FC2DDD97F000D55B50 /* Setting.swift */; }; FD78EA022DDEBC3200D55B50 /* DebounceTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA012DDEBC2C00D55B50 /* DebounceTaskManager.swift */; }; FD78EA042DDEC3C500D55B50 /* MultiTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA032DDEC3C000D55B50 /* MultiTaskManager.swift */; }; @@ -830,7 +816,6 @@ FD78EA0A2DDFE45E00D55B50 /* Interaction+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA092DDFE45900D55B50 /* Interaction+UI.swift */; }; FD78EA0B2DDFE45E00D55B50 /* Interaction+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA092DDFE45900D55B50 /* Interaction+UI.swift */; }; FD78EA0D2DDFEDE200D55B50 /* LibSession+Local.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA0C2DDFEDDF00D55B50 /* LibSession+Local.swift */; }; - FD7F74572BAA9D31006DDFD8 /* _006_DropSnodeCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74562BAA9D31006DDFD8 /* _006_DropSnodeCache.swift */; }; FD7F745B2BAAA35E006DDFD8 /* LibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F745A2BAAA35E006DDFD8 /* LibSession.swift */; }; FD7F745F2BAAA3B4006DDFD8 /* TypeConversion+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F745E2BAAA3B4006DDFD8 /* TypeConversion+Utilities.swift */; }; FD83B9B327CF200A005E1583 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; platformFilter = ios; }; @@ -851,7 +836,7 @@ FD860CB62D66913F00BBE29C /* ThemePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CB52D66913B00BBE29C /* ThemePreviewView.swift */; }; FD860CB82D66BC9900BBE29C /* AppIconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CB72D66BC9500BBE29C /* AppIconViewModel.swift */; }; FD860CBA2D66BF2A00BBE29C /* AppIconGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CB92D66BF2300BBE29C /* AppIconGridView.swift */; }; - FD860CBC2D6E7A9F00BBE29C /* _024_FixBustedInteractionVariant.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CBB2D6E7A9400BBE29C /* _024_FixBustedInteractionVariant.swift */; }; + FD860CBC2D6E7A9F00BBE29C /* _038_FixBustedInteractionVariant.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CBB2D6E7A9400BBE29C /* _038_FixBustedInteractionVariant.swift */; }; FD860CBE2D6E7DAA00BBE29C /* DeveloperSettingsViewModel+Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */; }; FD860CC92D6ED2ED00BBE29C /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD860CC82D6ED2ED00BBE29C /* DifferenceKit */; }; FD86FDA32BC5020600EC251B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */; }; @@ -874,13 +859,11 @@ FD8A5B252DC05B16004C689B /* Number+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B242DC05B16004C689B /* Number+Utilities.swift */; }; FD8A5B292DC060E2004C689B /* Double+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B282DC060DD004C689B /* Double+Utilities.swift */; }; FD8A5B302DC18D61004C689B /* GeneralCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B2F2DC18D5E004C689B /* GeneralCacheSpec.swift */; }; - FD8A5B322DC191B4004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B312DC191AB004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift */; }; - FD8A5B342DC1A732004C689B /* _008_ResetUserConfigLastHashes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B332DC1A726004C689B /* _008_ResetUserConfigLastHashes.swift */; }; + FD8A5B322DC191B4004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B312DC191AB004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift */; }; FD8ECF7B29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7A29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift */; }; - FD8ECF7D2934293A00C0D1BB /* _013_SessionUtilChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */; }; + FD8ECF7D2934293A00C0D1BB /* _027_SessionUtilChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7C2934293A00C0D1BB /* _027_SessionUtilChanges.swift */; }; FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7E2934298100C0D1BB /* ConfigDump.swift */; }; FD8FD7622C37B7BD001E38C7 /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71162B28E1451400B47552 /* Position.swift */; }; - FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */; }; FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7432804EF1B004C14C5 /* JobRunner.swift */; }; FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F68280F9EDA002CFC5D /* JobRunnerError.swift */; }; FD96F3A529DBC3DC00401309 /* MessageSendJobSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */; }; @@ -930,7 +913,7 @@ FDB3DA8D2E24881B00148F8D /* ImageLoading+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB3DA8C2E24881200148F8D /* ImageLoading+Convenience.swift */; }; FDB4BBC72838B91E00B7C95D /* LinkPreviewError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB4BBC62838B91E00B7C95D /* LinkPreviewError.swift */; }; FDB5DAC12A9443A5002C8721 /* MessageSender+Groups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAC02A9443A5002C8721 /* MessageSender+Groups.swift */; }; - FDB5DAC72A9447E7002C8721 /* _022_GroupsRebuildChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAC62A9447E7002C8721 /* _022_GroupsRebuildChanges.swift */; }; + FDB5DAC72A9447E7002C8721 /* _036_GroupsRebuildChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAC62A9447E7002C8721 /* _036_GroupsRebuildChanges.swift */; }; FDB5DAD42A9483F3002C8721 /* GroupUpdateInviteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAD32A9483F3002C8721 /* GroupUpdateInviteMessage.swift */; }; FDB5DADA2A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAD92A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift */; }; FDB5DADC2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DADB2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift */; }; @@ -954,7 +937,6 @@ FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB7400A28EB99A70094D718 /* TimeInterval+Utilities.swift */; }; FDB7400D28EBEC240094D718 /* DateHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB7400C28EBEC240094D718 /* DateHeaderCell.swift */; }; FDBA8A842D597975007C19C0 /* FailedGroupInvitesAndPromotionsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBA8A832D59796F007C19C0 /* FailedGroupInvitesAndPromotionsJob.swift */; }; - FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */; }; FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */; }; FDBEE52E2B6A18B900C143A0 /* UserDefaultsConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBEE52D2B6A18B900C143A0 /* UserDefaultsConfig.swift */; }; FDC0F0042BFECE12002CBFB9 /* TimeUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */; }; @@ -1008,15 +990,33 @@ FDD20C162A09E64A003898FB /* GetExpiriesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C152A09E64A003898FB /* GetExpiriesRequest.swift */; }; FDD20C182A09E7D3003898FB /* GetExpiriesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C172A09E7D3003898FB /* GetExpiriesResponse.swift */; }; FDD20C1A2A0A03AC003898FB /* DeleteInboxResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C192A0A03AC003898FB /* DeleteInboxResponse.swift */; }; + FDD23ADF2E457CAA0057E853 /* _016_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _016_ThemePreferences.swift */; }; + FDD23AE02E457CD40057E853 /* _004_SNK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _004_SNK_InitialSetupMigration.swift */; }; + FDD23AE12E457CDE0057E853 /* _005_SNK_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _005_SNK_SetupStandardJobs.swift */; }; + FDD23AE22E457CE50057E853 /* _008_SNK_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7A327F40F8100122BE0 /* _008_SNK_YDBToGRDBMigration.swift */; }; + FDD23AE32E457CFE0057E853 /* _010_FlagMessageHashAsDeletedOrInvalid.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39353528F7C3390084DADA /* _010_FlagMessageHashAsDeletedOrInvalid.swift */; }; + FDD23AE42E458C810057E853 /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */; }; + FDD23AE52E458C940057E853 /* _022_DropSnodeCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74562BAA9D31006DDFD8 /* _022_DropSnodeCache.swift */; }; + FDD23AE62E458CAA0057E853 /* _023_SplitSnodeReceivedMessageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD61FCFA2D34A5DE005752DE /* _023_SplitSnodeReceivedMessageInfo.swift */; }; + FDD23AE72E458DBC0057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C927F546D900122BE0 /* _001_SUK_InitialSetupMigration.swift */; }; + FDD23AE82E458DD40057E853 /* _002_SUK_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SUK_SetupStandardJobs.swift */; }; + FDD23AE92E458E020057E853 /* _003_SUK_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_SUK_YDBToGRDBMigration.swift */; }; + FDD23AEA2E458EB00057E853 /* _012_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _012_AddJobPriority.swift */; }; + FDD23AEB2E458F4D0057E853 /* _020_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */; }; + FDD23AEC2E458F980057E853 /* _024_ResetUserConfigLastHashes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B332DC1A726004C689B /* _024_ResetUserConfigLastHashes.swift */; }; + FDD23AED2E4590A10057E853 /* _041_RenameTableSettingToKeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE22D5181F400E8E413 /* _041_RenameTableSettingToKeyValueStore.swift */; }; + FDD23AEE2E459E470057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C927F546D900122BE0 /* _001_SUK_InitialSetupMigration.swift */; }; + FDD23AEF2E459EC90057E853 /* _012_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _012_AddJobPriority.swift */; }; + FDD23AF02E459EDD0057E853 /* _020_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */; }; FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; }; FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; }; - FDDD554E2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */; }; + FDDD554E2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDD554D2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift */; }; FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */; }; FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */; }; FDE125232A837E4E002DA685 /* MainAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE125222A837E4E002DA685 /* MainAppContext.swift */; }; FDE33BBC2D5C124900E56F42 /* DispatchTimeInterval+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE33BBB2D5C124300E56F42 /* DispatchTimeInterval+Utilities.swift */; }; - FDE33BBE2D5C3AF100E56F42 /* _023_GroupsExpiredFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE33BBD2D5C3AE800E56F42 /* _023_GroupsExpiredFlag.swift */; }; + FDE33BBE2D5C3AF100E56F42 /* _037_GroupsExpiredFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE33BBD2D5C3AE800E56F42 /* _037_GroupsExpiredFlag.swift */; }; FDE519F72AB7CDC700450C53 /* Result+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE519F62AB7CDC700450C53 /* Result+Utilities.swift */; }; FDE5218E2E03A06B00061B8E /* AttachmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE5218D2E03A06700061B8E /* AttachmentManager.swift */; }; FDE521942E050B1100061B8E /* DismissCallbackAVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE521932E050B0800061B8E /* DismissCallbackAVPlayerViewController.swift */; }; @@ -1067,7 +1067,7 @@ FDE754FA2C9BB0B0002A2623 /* NotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754F52C9BB0AF002A2623 /* NotificationPresenter.swift */; }; FDE754FE2C9BB0D0002A2623 /* Threading+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754FD2C9BB0D0002A2623 /* Threading+SMK.swift */; }; FDE755002C9BB0FA002A2623 /* SessionEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754FF2C9BB0FA002A2623 /* SessionEnvironment.swift */; }; - FDE755022C9BB122002A2623 /* _011_AddPendingReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755012C9BB122002A2623 /* _011_AddPendingReadReceipts.swift */; }; + FDE755022C9BB122002A2623 /* _025_AddPendingReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755012C9BB122002A2623 /* _025_AddPendingReadReceipts.swift */; }; FDE755052C9BB4EE002A2623 /* BencodeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755032C9BB4ED002A2623 /* BencodeDecoder.swift */; }; FDE755062C9BB4EE002A2623 /* Bencode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755042C9BB4ED002A2623 /* Bencode.swift */; }; FDE755182C9BC169002A2623 /* UIAlertAction+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755132C9BC169002A2623 /* UIAlertAction+Utilities.swift */; }; @@ -1088,7 +1088,7 @@ FDEF57712C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 in Resources */ = {isa = PBXBuildFile; fileRef = FDEF57702C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 */; }; FDF01FAD2A9ECC4200CAF969 /* SingletonConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF01FAC2A9ECC4200CAF969 /* SingletonConfig.swift */; }; FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */; }; - FDF0B7422804EA4F004C14C5 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7412804EA4F004C14C5 /* _002_SetupStandardJobs.swift */; }; + FDF0B7422804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7412804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift */; }; FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74828060D13004C14C5 /* QuotedReplyModel.swift */; }; FDF0B74B28061F7A004C14C5 /* InteractionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74A28061F7A004C14C5 /* InteractionAttachment.swift */; }; FDF0B7512807BA56004C14C5 /* NotificationsManagerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7502807BA56004C14C5 /* NotificationsManagerType.swift */; }; @@ -1102,7 +1102,7 @@ FDF2220F281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */; }; FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */; }; FDF2F0222DAE1AF500491E8A /* MessageReceiver+LegacyClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF2F0212DAE1AEF00491E8A /* MessageReceiver+LegacyClosedGroups.swift */; }; - FDF40CDE2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */; }; + FDF40CDE2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF40CDD2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift */; }; FDF71EA32B072C2800A8D6B5 /* LibSessionMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF71EA22B072C2800A8D6B5 /* LibSessionMessage.swift */; }; FDF71EA52B07363500A8D6B5 /* MessageReceiver+LibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF71EA42B07363500A8D6B5 /* MessageReceiver+LibSession.swift */; }; FDF8487F29405994007DCAE5 /* HTTPHeader+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */; }; @@ -1152,7 +1152,7 @@ FDFDE126282D05380098B17F /* MediaInteractiveDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE125282D05380098B17F /* MediaInteractiveDismiss.swift */; }; FDFDE128282D05530098B17F /* MediaPresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE127282D05530098B17F /* MediaPresentationContext.swift */; }; FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */; }; - FDFE75B12ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */; }; + FDFE75B12ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFE75B02ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift */; }; FDFE75B42ABD46B600655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FDFF9FDF2A787F57005E0628 /* JSONEncoder+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */; }; @@ -1459,7 +1459,7 @@ 7B4EF2592934743000CB351D /* SessionTableViewTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableViewTitleView.swift; sourceTree = ""; }; 7B50D64C28AC7CF80086CCEC /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = ""; }; 7B5233C32900E90F00F8F375 /* SessionLabelCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionLabelCarouselView.swift; sourceTree = ""; }; - 7B5233C5290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _018_DisappearingMessagesConfiguration.swift; sourceTree = ""; }; + 7B5233C5290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _032_DisappearingMessagesConfiguration.swift; sourceTree = ""; }; 7B5802982AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupInvitationView_SwiftUI.swift; sourceTree = ""; }; 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = ""; }; 7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; @@ -1468,7 +1468,7 @@ 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.swift; sourceTree = ""; }; 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = ""; }; 7B81682228A4C1210069F315 /* UpdateTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTypes.swift; sourceTree = ""; }; - 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = ""; }; + 7B81682728B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _015_HomeQueryOptimisationIndexes.swift; sourceTree = ""; }; 7B81682928B6F1420069F315 /* ReactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionResponse.swift; sourceTree = ""; }; 7B81682B28B72F480069F315 /* PendingChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingChange.swift; sourceTree = ""; }; 7B81FB582AB01AA8002FB267 /* LoadingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingIndicatorView.swift; sourceTree = ""; }; @@ -1491,7 +1491,7 @@ 7BA68908272A27BE00EFC32F /* SessionCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCall.swift; sourceTree = ""; }; 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXCallController.swift"; sourceTree = ""; }; 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXProvider.swift"; sourceTree = ""; }; - 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _009_OpenGroupPermission.swift; sourceTree = ""; }; + 7BAA7B6528D2DE4700AE1489 /* _018_OpenGroupPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _018_OpenGroupPermission.swift; sourceTree = ""; }; 7BAADFCB27B0EF23007BCF92 /* CallVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVideoView.swift; sourceTree = ""; }; 7BAADFCD27B215FE007BCF92 /* UIView+Draggable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Draggable.swift"; sourceTree = ""; }; 7BAF54CC27ACCEEC003D12F8 /* GlobalSearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalSearchViewController.swift; sourceTree = ""; }; @@ -1543,7 +1543,7 @@ 94367C422C6C828500814252 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+DisappearingMessages.swift"; sourceTree = ""; }; 943C6D832B86B5F1004ACE64 /* Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; - 945D9C572D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_AddJobUniqueHash.swift; sourceTree = ""; }; + 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _020_AddJobUniqueHash.swift; sourceTree = ""; }; 9471CAA72CACFB4E00090FB7 /* GenerateLicenses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateLicenses.swift; sourceTree = ""; }; 9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 9479981B2DD44AC5008F5CD5 /* ThreadNotificationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModel.swift; sourceTree = ""; }; @@ -1555,7 +1555,7 @@ 947D7FD92D5180F200E8E413 /* SessionNetworkScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNetworkScreen.swift; sourceTree = ""; }; 947D7FDA2D5180F200E8E413 /* SessionNetworkScreen+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkScreen+Models.swift"; sourceTree = ""; }; 947D7FDB2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkScreen+ViewModel.swift"; sourceTree = ""; }; - 947D7FE22D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_RenameTableSettingToKeyValueStore.swift; sourceTree = ""; }; + 947D7FE22D5181F400E8E413 /* _041_RenameTableSettingToKeyValueStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _041_RenameTableSettingToKeyValueStore.swift; sourceTree = ""; }; 947D7FE42D51837200E8E413 /* ArrowCapsule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrowCapsule.swift; sourceTree = ""; }; 947D7FE52D51837200E8E413 /* PopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverView.swift; sourceTree = ""; }; 947D7FE62D51837200E8E413 /* Text+CopyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+CopyButton.swift"; sourceTree = ""; }; @@ -1573,7 +1573,7 @@ 94C58AC82D2E036E00609195 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = ""; }; 94CD95BA2E08D9D40097754D /* SessionProBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProBadge.swift; sourceTree = ""; }; 94CD95BC2E0908340097754D /* LibSession+Pro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Pro.swift"; sourceTree = ""; }; - 94CD95C02E0CBF1C0097754D /* _029_AddProMessageFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _029_AddProMessageFlag.swift; sourceTree = ""; }; + 94CD95C02E0CBF1C0097754D /* _044_AddProMessageFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _044_AddProMessageFlag.swift; sourceTree = ""; }; 94CD962A2E1B85920097754D /* InputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = ""; }; 94CD962B2E1B85920097754D /* InputViewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputViewButton.swift; sourceTree = ""; }; 94CD962F2E1B88430097754D /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = ""; }; @@ -1744,7 +1744,6 @@ C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionNetworkingKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C3C2A5A1255385C100C340D1 /* SessionNetworkingKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionNetworkingKit.h; sourceTree = ""; }; C3C2A5A2255385C100C340D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C3C2A5B9255385ED00C340D1 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; C3C2A5CE2553860700C340D1 /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; C3C2A5D22553860900C340D1 /* String+Trimming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Trimming.swift"; sourceTree = ""; }; C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Utilities.swift"; sourceTree = ""; }; @@ -1803,7 +1802,7 @@ FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+OpenGroupAPI.swift"; sourceTree = ""; }; FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevokeSubaccountResponse.swift; sourceTree = ""; }; FD05593C2DFA3A2200DC48CE /* VoipPayloadKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoipPayloadKey.swift; sourceTree = ""; }; - FD05594D2E012D1A00DC48CE /* _028_RenameAttachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _028_RenameAttachments.swift; sourceTree = ""; }; + FD05594D2E012D1A00DC48CE /* _043_RenameAttachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _043_RenameAttachments.swift; sourceTree = ""; }; FD0559542E026CC900DC48CE /* ObservingDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservingDatabase.swift; sourceTree = ""; }; FD0606C22BCE13ED00C3816E /* MessageRequestFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestFooterView.swift; sourceTree = ""; }; FD0969F82A69FFE700C5C365 /* Mocked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocked.swift; sourceTree = ""; }; @@ -1822,7 +1821,7 @@ FD09799827FFC1A300936362 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; FD09799A27FFC82D00936362 /* Quote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quote.swift; sourceTree = ""; }; FD09B7E228865FDA00ED0B66 /* HighlightMentionBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightMentionBackgroundView.swift; sourceTree = ""; }; - FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _008_EmojiReacts.swift; sourceTree = ""; }; + FD09B7E4288670BB00ED0B66 /* _017_EmojiReacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _017_EmojiReacts.swift; sourceTree = ""; }; FD09B7E6288670FD00ED0B66 /* Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reaction.swift; sourceTree = ""; }; FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryViewModel.swift; sourceTree = ""; }; FD09C5E728264937000CE219 /* MediaDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDetailViewController.swift; sourceTree = ""; }; @@ -1842,19 +1841,17 @@ FD12A8462AD63C3400EEBA0D /* PagedObservationSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedObservationSource.swift; sourceTree = ""; }; FD12A8482AD63C4700EEBA0D /* SessionNavItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNavItem.swift; sourceTree = ""; }; FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfilePictureView+Convenience.swift"; sourceTree = ""; }; - FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = ""; }; - FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = ""; }; - FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = ""; }; - FD17D7A327F40F8100122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = ""; }; + FD17D79527F3E04600122BE0 /* _006_SMK_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_SMK_InitialSetupMigration.swift; sourceTree = ""; }; + FD17D79827F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _009_SMK_YDBToGRDBMigration.swift; sourceTree = ""; }; + FD17D79F27F40CC800122BE0 /* _004_SNK_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_SNK_InitialSetupMigration.swift; sourceTree = ""; }; + FD17D7A327F40F8100122BE0 /* _008_SNK_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _008_SNK_YDBToGRDBMigration.swift; sourceTree = ""; }; FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeReceivedMessageInfo.swift; sourceTree = ""; }; FD17D7AF27F4225C00122BE0 /* Set+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+Utilities.swift"; sourceTree = ""; }; FD17D7B727F51ECA00122BE0 /* Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Migration.swift; sourceTree = ""; }; - FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetMigrations.swift; sourceTree = ""; }; FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnExpressible.swift; sourceTree = ""; }; - FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseMigrator+Utilities.swift"; sourceTree = ""; }; - FD17D7C927F546D900122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = ""; }; + FD17D7C927F546D900122BE0 /* _001_SUK_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_SUK_InitialSetupMigration.swift; sourceTree = ""; }; FD17D7E427F6A09900122BE0 /* Identity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identity.swift; sourceTree = ""; }; - FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = ""; }; + FD17D7E627F6A16700122BE0 /* _003_SUK_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_SUK_YDBToGRDBMigration.swift; sourceTree = ""; }; FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResponseInfo+SnodeAPI.swift"; sourceTree = ""; }; FD19363E2ACA66DE004BCF0F /* DatabaseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseSpec.swift; sourceTree = ""; }; FD1A553D2E14BE0E003761E4 /* PagedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedData.swift; sourceTree = ""; }; @@ -1862,7 +1859,7 @@ FD1A55422E179AE6003761E4 /* ObservableKeyEvent+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKeyEvent+Utilities.swift"; sourceTree = ""; }; FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Utilities.swift"; sourceTree = ""; }; FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = ""; }; - FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _015_BlockCommunityMessageRequests.swift; sourceTree = ""; }; + FD1D732D2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _029_BlockCommunityMessageRequests.swift; sourceTree = ""; }; FD2272532C32911A004D8A6C /* SendReadReceiptsJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendReadReceiptsJob.swift; sourceTree = ""; }; FD2272542C32911A004D8A6C /* GroupLeavingJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupLeavingJob.swift; sourceTree = ""; }; FD2272552C32911A004D8A6C /* CheckForAppUpdatesJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckForAppUpdatesJob.swift; sourceTree = ""; }; @@ -1944,8 +1941,8 @@ FD336F5F2CAA28CF00C0B51B /* MockSwarmPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSwarmPoller.swift; sourceTree = ""; }; FD336F6B2CAA29C200C0B51B /* CommunityPollerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityPollerSpec.swift; sourceTree = ""; }; FD336F6E2CAA37CB00C0B51B /* MockCommunityPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCommunityPoller.swift; sourceTree = ""; }; - FD3559452CC1FF140088F2A9 /* _020_AddMissingWhisperFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _020_AddMissingWhisperFlag.swift; sourceTree = ""; }; - FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; sourceTree = ""; }; + FD3559452CC1FF140088F2A9 /* _034_AddMissingWhisperFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _034_AddMissingWhisperFlag.swift; sourceTree = ""; }; + FD368A6729DE8F9B000DBF1E /* _026_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _026_AddFTSIfNeeded.swift; sourceTree = ""; }; FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Utilities.swift"; sourceTree = ""; }; FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSnodeAPICache.swift; sourceTree = ""; }; FD3765E12AD8F53B00DC1489 /* CommonSSKMockExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonSSKMockExtensions.swift; sourceTree = ""; }; @@ -1966,7 +1963,7 @@ FD37E9DA28A244E9003AE748 /* ThemeMessagePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeMessagePreviewView.swift; sourceTree = ""; }; FD37E9DC28A384EB003AE748 /* PrimaryColorSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryColorSelectionView.swift; sourceTree = ""; }; FD37E9F528A5F106003AE748 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; - FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_ThemePreferences.swift; sourceTree = ""; }; + FD37E9F828A5F14A003AE748 /* _016_ThemePreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _016_ThemePreferences.swift; sourceTree = ""; }; FD37E9FE28A5F2CD003AE748 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; FD37EA0028A60473003AE748 /* UIKit+Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIKit+Theme.swift"; sourceTree = ""; }; FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpViewModel.swift; sourceTree = ""; }; @@ -1974,8 +1971,8 @@ FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableViewController.swift; sourceTree = ""; }; FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableViewModel.swift; sourceTree = ""; }; FD37EA0A28AB12E2003AE748 /* SessionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCell.swift; sourceTree = ""; }; - FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_FixDeletedMessageReadState.swift; sourceTree = ""; }; - FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_FixHiddenModAdminSupport.swift; sourceTree = ""; }; + FD37EA0C28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _013_FixDeletedMessageReadState.swift; sourceTree = ""; }; + FD37EA0E28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _014_FixHiddenModAdminSupport.swift; sourceTree = ""; }; FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitySpec.swift; sourceTree = ""; }; FD37EA1628AC5605003AE748 /* NotificationContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentViewModel.swift; sourceTree = ""; }; FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundViewModel.swift; sourceTree = ""; }; @@ -1983,6 +1980,7 @@ FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = ""; }; FD3937072E4AD3F800571F17 /* NoopDependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopDependency.swift; sourceTree = ""; }; FD39370B2E4D7BBE00571F17 /* DocumentPickerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPickerHandler.swift; sourceTree = ""; }; + FD39353528F7C3390084DADA /* _010_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _010_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = ""; }; FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = ""; }; FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookupSpec.swift; sourceTree = ""; }; FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModel.swift; sourceTree = ""; }; @@ -1998,7 +1996,7 @@ FD428B182B4B576F006D0888 /* AppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; FD428B1A2B4B6098006D0888 /* Notifications+Lifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notifications+Lifecycle.swift"; sourceTree = ""; }; FD428B1E2B4B758B006D0888 /* AppReadiness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReadiness.swift; sourceTree = ""; }; - FD428B222B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _017_RebuildFTSIfNeeded_2_4_5.swift; sourceTree = ""; }; + FD428B222B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _031_RebuildFTSIfNeeded_2_4_5.swift; sourceTree = ""; }; FD42ECCD2E287CD1002D03EA /* ThemeColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeColor.swift; sourceTree = ""; }; FD42ECCF2E289257002D03EA /* ThemeLinearGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeLinearGradient.swift; sourceTree = ""; }; FD42ECD12E3071DC002D03EA /* ThemeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeText.swift; sourceTree = ""; }; @@ -2017,7 +2015,7 @@ FD4B200D283492210034334B /* InsetLockableTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetLockableTableView.swift; sourceTree = ""; }; FD4BB22A2D63F20600D0DC3D /* MigrationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationHelper.swift; sourceTree = ""; }; FD4C4E9B2B02E2A300C72199 /* DisplayPictureError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayPictureError.swift; sourceTree = ""; }; - FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _021_ReworkRecipientState.swift; sourceTree = ""; }; + FD4C53AE2CC1D61E003B10F4 /* _035_ReworkRecipientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _035_ReworkRecipientState.swift; sourceTree = ""; }; FD52090228B4680F006098F6 /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewModel.swift; sourceTree = ""; }; FD52090628B49738006098F6 /* ConfirmationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationModal.swift; sourceTree = ""; }; @@ -2040,22 +2038,22 @@ FD5CE3442A3C5D96001A6DE3 /* DecryptExportedKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecryptExportedKey.swift; sourceTree = ""; }; FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = ""; }; FD61FCF82D308CC5005752DE /* GroupMemberSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberSpec.swift; sourceTree = ""; }; - FD61FCFA2D34A5DE005752DE /* _007_SplitSnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_SplitSnodeReceivedMessageInfo.swift; sourceTree = ""; }; + FD61FCFA2D34A5DE005752DE /* _023_SplitSnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _023_SplitSnodeReceivedMessageInfo.swift; sourceTree = ""; }; FD6531892AA025C500DFEEAA /* TestDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDependencies.swift; sourceTree = ""; }; FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = ""; }; FD6673FE2D77F9BE00041530 /* ScreenLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLock.swift; sourceTree = ""; }; FD66CB272BF3449B00268FAB /* SessionNetworkingKit.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SessionNetworkingKit.xctestplan; sourceTree = ""; }; FD66CB2B2BF344C600268FAB /* SessionMessageKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SessionMessageKit.xctestplan; sourceTree = ""; }; FD6A38F02C2A66B100762359 /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = ""; }; - FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; + FD6A7A6C2818C61500035AC1 /* _005_SNK_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_SNK_SetupStandardJobs.swift; sourceTree = ""; }; FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopSessionCallManager.swift; sourceTree = ""; }; - FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; + FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _021_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = ""; }; FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; - FD70F25B2DC1F176003729B7 /* _026_MessageDeduplicationTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _026_MessageDeduplicationTable.swift; sourceTree = ""; }; + FD70F25B2DC1F176003729B7 /* _040_MessageDeduplicationTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _040_MessageDeduplicationTable.swift; sourceTree = ""; }; FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSettingsViewModel.swift; sourceTree = ""; }; FD7115EF28C5D7DE00B47552 /* SessionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHeaderView.swift; sourceTree = ""; }; - FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _010_AddThreadIdToFTS.swift; sourceTree = ""; }; + FD7115F128C6CB3900B47552 /* _019_AddThreadIdToFTS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _019_AddThreadIdToFTS.swift; sourceTree = ""; }; FD7115F328C71EB200B47552 /* ThreadDisappearingMessagesSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDisappearingMessagesSettingsViewModel.swift; sourceTree = ""; }; FD7115F728C8151C00B47552 /* DisposableBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposableBarButtonItem.swift; sourceTree = ""; }; FD7115F928C8153400B47552 /* UIBarButtonItem+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+Combine.swift"; sourceTree = ""; }; @@ -2100,18 +2098,18 @@ FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModelSpec.swift; sourceTree = ""; }; FD7728952849E7E90018502F /* String+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Utilities.swift"; sourceTree = ""; }; FD7728972849E8110018502F /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = ""; }; - FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _014_GenerateInitialUserConfigDumps.swift; sourceTree = ""; }; + FD778B6329B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _028_GenerateInitialUserConfigDumps.swift; sourceTree = ""; }; FD78E9F12DDA9E9B00D55B50 /* MockImageDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImageDataManager.swift; sourceTree = ""; }; FD78E9F32DDABA4200D55B50 /* AttachmentUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploader.swift; sourceTree = ""; }; FD78E9F52DDD43AB00D55B50 /* Mutation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutation.swift; sourceTree = ""; }; - FD78E9F72DDD742100D55B50 /* _027_MoveSettingsToLibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _027_MoveSettingsToLibSession.swift; sourceTree = ""; }; + FD78E9F72DDD742100D55B50 /* _042_MoveSettingsToLibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _042_MoveSettingsToLibSession.swift; sourceTree = ""; }; FD78E9FC2DDD97F000D55B50 /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = ""; }; FD78EA012DDEBC2C00D55B50 /* DebounceTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebounceTaskManager.swift; sourceTree = ""; }; FD78EA032DDEC3C000D55B50 /* MultiTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiTaskManager.swift; sourceTree = ""; }; FD78EA052DDEC8F100D55B50 /* AsyncSequence+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncSequence+Utilities.swift"; sourceTree = ""; }; FD78EA092DDFE45900D55B50 /* Interaction+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Interaction+UI.swift"; sourceTree = ""; }; FD78EA0C2DDFEDDF00D55B50 /* LibSession+Local.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Local.swift"; sourceTree = ""; }; - FD7F74562BAA9D31006DDFD8 /* _006_DropSnodeCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_DropSnodeCache.swift; sourceTree = ""; }; + FD7F74562BAA9D31006DDFD8 /* _022_DropSnodeCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _022_DropSnodeCache.swift; sourceTree = ""; }; FD7F745A2BAAA35E006DDFD8 /* LibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSession.swift; sourceTree = ""; }; FD7F745E2BAAA3B4006DDFD8 /* TypeConversion+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TypeConversion+Utilities.swift"; sourceTree = ""; }; FD7F74692BAB8A6D006DDFD8 /* LibSession+Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Networking.swift"; sourceTree = ""; }; @@ -2137,7 +2135,7 @@ FD860CB52D66913B00BBE29C /* ThemePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePreviewView.swift; sourceTree = ""; }; FD860CB72D66BC9500BBE29C /* AppIconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconViewModel.swift; sourceTree = ""; }; FD860CB92D66BF2300BBE29C /* AppIconGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconGridView.swift; sourceTree = ""; }; - FD860CBB2D6E7A9400BBE29C /* _024_FixBustedInteractionVariant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _024_FixBustedInteractionVariant.swift; sourceTree = ""; }; + FD860CBB2D6E7A9400BBE29C /* _038_FixBustedInteractionVariant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _038_FixBustedInteractionVariant.swift; sourceTree = ""; }; FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeveloperSettingsViewModel+Testing.swift"; sourceTree = ""; }; FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSettingsViewModel.swift; sourceTree = ""; }; @@ -2149,12 +2147,12 @@ FD8A5B242DC05B16004C689B /* Number+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Number+Utilities.swift"; sourceTree = ""; }; FD8A5B282DC060DD004C689B /* Double+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Utilities.swift"; sourceTree = ""; }; FD8A5B2F2DC18D5E004C689B /* GeneralCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralCacheSpec.swift; sourceTree = ""; }; - FD8A5B312DC191AB004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _025_DropLegacyClosedGroupKeyPairTable.swift; sourceTree = ""; }; - FD8A5B332DC1A726004C689B /* _008_ResetUserConfigLastHashes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _008_ResetUserConfigLastHashes.swift; sourceTree = ""; }; + FD8A5B312DC191AB004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _039_DropLegacyClosedGroupKeyPairTable.swift; sourceTree = ""; }; + FD8A5B332DC1A726004C689B /* _024_ResetUserConfigLastHashes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _024_ResetUserConfigLastHashes.swift; sourceTree = ""; }; FD8ECF7A29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+SessionMessagingKit.swift"; sourceTree = ""; }; - FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _013_SessionUtilChanges.swift; sourceTree = ""; }; + FD8ECF7C2934293A00C0D1BB /* _027_SessionUtilChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _027_SessionUtilChanges.swift; sourceTree = ""; }; FD8ECF7E2934298100C0D1BB /* ConfigDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigDump.swift; sourceTree = ""; }; - FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; + FD9004132818AD0B00ABAAF6 /* _002_SUK_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SUK_SetupStandardJobs.swift; sourceTree = ""; }; FD9401CE2ABD04AC003A4834 /* TRANSLATIONS.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = TRANSLATIONS.md; sourceTree = ""; }; FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSendJobSpec.swift; sourceTree = ""; }; FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARC4RandomNumberGenerator.swift; sourceTree = ""; }; @@ -2197,7 +2195,7 @@ FDB3DA8C2E24881200148F8D /* ImageLoading+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImageLoading+Convenience.swift"; sourceTree = ""; }; FDB4BBC62838B91E00B7C95D /* LinkPreviewError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewError.swift; sourceTree = ""; }; FDB5DAC02A9443A5002C8721 /* MessageSender+Groups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageSender+Groups.swift"; sourceTree = ""; }; - FDB5DAC62A9447E7002C8721 /* _022_GroupsRebuildChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _022_GroupsRebuildChanges.swift; sourceTree = ""; }; + FDB5DAC62A9447E7002C8721 /* _036_GroupsRebuildChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _036_GroupsRebuildChanges.swift; sourceTree = ""; }; FDB5DAD32A9483F3002C8721 /* GroupUpdateInviteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateInviteMessage.swift; sourceTree = ""; }; FDB5DAD92A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateInfoChangeMessage.swift; sourceTree = ""; }; FDB5DADB2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateMemberChangeMessage.swift; sourceTree = ""; }; @@ -2213,7 +2211,7 @@ FDB7400A28EB99A70094D718 /* TimeInterval+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Utilities.swift"; sourceTree = ""; }; FDB7400C28EBEC240094D718 /* DateHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateHeaderCell.swift; sourceTree = ""; }; FDBA8A832D59796F007C19C0 /* FailedGroupInvitesAndPromotionsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedGroupInvitesAndPromotionsJob.swift; sourceTree = ""; }; - FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_AddJobPriority.swift; sourceTree = ""; }; + FDBB25E22988B13800F1508E /* _012_AddJobPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _012_AddJobPriority.swift; sourceTree = ""; }; FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = ""; }; FDBEE52D2B6A18B900C143A0 /* UserDefaultsConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsConfig.swift; sourceTree = ""; }; FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeUnit.swift; sourceTree = ""; }; @@ -2275,12 +2273,12 @@ FDD383702AFDD0E1001367F2 /* BencodeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeResponse.swift; sourceTree = ""; }; FDD383722AFDD6D7001367F2 /* BencodeResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeResponseSpec.swift; sourceTree = ""; }; FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessResult.swift; sourceTree = ""; }; - FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _019_ScheduleAppUpdateCheckJob.swift; sourceTree = ""; }; + FDDD554D2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _033_ScheduleAppUpdateCheckJob.swift; sourceTree = ""; }; FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequest+Utilities.swift"; sourceTree = ""; }; FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerSpec.swift; sourceTree = ""; }; FDE125222A837E4E002DA685 /* MainAppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAppContext.swift; sourceTree = ""; }; FDE33BBB2D5C124300E56F42 /* DispatchTimeInterval+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchTimeInterval+Utilities.swift"; sourceTree = ""; }; - FDE33BBD2D5C3AE800E56F42 /* _023_GroupsExpiredFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _023_GroupsExpiredFlag.swift; sourceTree = ""; }; + FDE33BBD2D5C3AE800E56F42 /* _037_GroupsExpiredFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _037_GroupsExpiredFlag.swift; sourceTree = ""; }; FDE519F62AB7CDC700450C53 /* Result+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Utilities.swift"; sourceTree = ""; }; FDE5218D2E03A06700061B8E /* AttachmentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentManager.swift; sourceTree = ""; }; FDE521932E050B0800061B8E /* DismissCallbackAVPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissCallbackAVPlayerViewController.swift; sourceTree = ""; }; @@ -2333,7 +2331,7 @@ FDE754F52C9BB0AF002A2623 /* NotificationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationPresenter.swift; sourceTree = ""; }; FDE754FD2C9BB0D0002A2623 /* Threading+SMK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Threading+SMK.swift"; sourceTree = ""; }; FDE754FF2C9BB0FA002A2623 /* SessionEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionEnvironment.swift; sourceTree = ""; }; - FDE755012C9BB122002A2623 /* _011_AddPendingReadReceipts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _011_AddPendingReadReceipts.swift; sourceTree = ""; }; + FDE755012C9BB122002A2623 /* _025_AddPendingReadReceipts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _025_AddPendingReadReceipts.swift; sourceTree = ""; }; FDE755032C9BB4ED002A2623 /* BencodeDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BencodeDecoder.swift; sourceTree = ""; }; FDE755042C9BB4ED002A2623 /* Bencode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bencode.swift; sourceTree = ""; }; FDE755132C9BC169002A2623 /* UIAlertAction+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertAction+Utilities.swift"; sourceTree = ""; }; @@ -2360,7 +2358,7 @@ FDF01FAC2A9ECC4200CAF969 /* SingletonConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingletonConfig.swift; sourceTree = ""; }; FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreview.swift; sourceTree = ""; }; FDF0B73F280402C4004C14C5 /* Job.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Job.swift; sourceTree = ""; }; - FDF0B7412804EA4F004C14C5 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; + FDF0B7412804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_SMK_SetupStandardJobs.swift; sourceTree = ""; }; FDF0B7432804EF1B004C14C5 /* JobRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunner.swift; sourceTree = ""; }; FDF0B74828060D13004C14C5 /* QuotedReplyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedReplyModel.swift; sourceTree = ""; }; FDF0B74A28061F7A004C14C5 /* InteractionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionAttachment.swift; sourceTree = ""; }; @@ -2375,7 +2373,7 @@ FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = ""; }; FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableRecord+Utilities.swift"; sourceTree = ""; }; FDF2F0212DAE1AEF00491E8A /* MessageReceiver+LegacyClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+LegacyClosedGroups.swift"; sourceTree = ""; }; - FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_RemoveLegacyYDB.swift; sourceTree = ""; }; + FDF40CDD2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _011_RemoveLegacyYDB.swift; sourceTree = ""; }; FDF71EA22B072C2800A8D6B5 /* LibSessionMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionMessage.swift; sourceTree = ""; }; FDF71EA42B07363500A8D6B5 /* MessageReceiver+LibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+LibSession.swift"; sourceTree = ""; }; FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+OpenGroup.swift"; sourceTree = ""; }; @@ -2424,7 +2422,7 @@ FDFDE125282D05380098B17F /* MediaInteractiveDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInteractiveDismiss.swift; sourceTree = ""; }; FDFDE127282D05530098B17F /* MediaPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPresentationContext.swift; sourceTree = ""; }; FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaZoomAnimationController.swift; sourceTree = ""; }; - FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _016_MakeBrokenProfileTimestampsNullable.swift; sourceTree = ""; }; + FDFE75B02ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _030_MakeBrokenProfileTimestampsNullable.swift; sourceTree = ""; }; FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEncoder+Utilities.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2981,7 +2979,6 @@ B8A582AB258C64E800AFD84C /* Database */ = { isa = PBXGroup; children = ( - FD17D7C827F546CE00122BE0 /* Migrations */, FD17D7CB27F546F500122BE0 /* Models */, FD17D7B427F51E6700122BE0 /* Types */, FD17D7BB27F51F5C00122BE0 /* Utilities */, @@ -3677,7 +3674,6 @@ FD2272842C33E28D004D8A6C /* SnodeAPI */, FDF8488F29405C13007DCAE5 /* Types */, C3C2A5CD255385F300C340D1 /* Utilities */, - C3C2A5B9255385ED00C340D1 /* Configuration.swift */, ); path = SessionNetworkingKit; sourceTree = ""; @@ -4013,35 +4009,50 @@ FD17D79427F3E03300122BE0 /* Migrations */ = { isa = PBXGroup; children = ( - FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */, - FDF0B7412804EA4F004C14C5 /* _002_SetupStandardJobs.swift */, - FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */, - FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */, - FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */, - FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */, - 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */, - FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */, - 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */, - FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */, - FDE755012C9BB122002A2623 /* _011_AddPendingReadReceipts.swift */, - FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */, - FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */, - FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */, - FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */, - FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */, - FD428B222B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift */, - 7B5233C5290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift */, - FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */, - FD3559452CC1FF140088F2A9 /* _020_AddMissingWhisperFlag.swift */, - FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */, - FDB5DAC62A9447E7002C8721 /* _022_GroupsRebuildChanges.swift */, - FDE33BBD2D5C3AE800E56F42 /* _023_GroupsExpiredFlag.swift */, - FD860CBB2D6E7A9400BBE29C /* _024_FixBustedInteractionVariant.swift */, - FD8A5B312DC191AB004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift */, - FD70F25B2DC1F176003729B7 /* _026_MessageDeduplicationTable.swift */, - FD78E9F72DDD742100D55B50 /* _027_MoveSettingsToLibSession.swift */, - FD05594D2E012D1A00DC48CE /* _028_RenameAttachments.swift */, - 94CD95C02E0CBF1C0097754D /* _029_AddProMessageFlag.swift */, + FD17D7C927F546D900122BE0 /* _001_SUK_InitialSetupMigration.swift */, + FD9004132818AD0B00ABAAF6 /* _002_SUK_SetupStandardJobs.swift */, + FD17D7E627F6A16700122BE0 /* _003_SUK_YDBToGRDBMigration.swift */, + FD17D79F27F40CC800122BE0 /* _004_SNK_InitialSetupMigration.swift */, + FD6A7A6C2818C61500035AC1 /* _005_SNK_SetupStandardJobs.swift */, + FD17D79527F3E04600122BE0 /* _006_SMK_InitialSetupMigration.swift */, + FDF0B7412804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift */, + FD17D7A327F40F8100122BE0 /* _008_SNK_YDBToGRDBMigration.swift */, + FD17D79827F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift */, + FD39353528F7C3390084DADA /* _010_FlagMessageHashAsDeletedOrInvalid.swift */, + FDF40CDD2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift */, + FDBB25E22988B13800F1508E /* _012_AddJobPriority.swift */, + FD37EA0C28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift */, + FD37EA0E28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift */, + 7B81682728B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift */, + FD37E9F828A5F14A003AE748 /* _016_ThemePreferences.swift */, + FD09B7E4288670BB00ED0B66 /* _017_EmojiReacts.swift */, + 7BAA7B6528D2DE4700AE1489 /* _018_OpenGroupPermission.swift */, + FD7115F128C6CB3900B47552 /* _019_AddThreadIdToFTS.swift */, + 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */, + FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */, + FD7F74562BAA9D31006DDFD8 /* _022_DropSnodeCache.swift */, + FD61FCFA2D34A5DE005752DE /* _023_SplitSnodeReceivedMessageInfo.swift */, + FD8A5B332DC1A726004C689B /* _024_ResetUserConfigLastHashes.swift */, + FDE755012C9BB122002A2623 /* _025_AddPendingReadReceipts.swift */, + FD368A6729DE8F9B000DBF1E /* _026_AddFTSIfNeeded.swift */, + FD8ECF7C2934293A00C0D1BB /* _027_SessionUtilChanges.swift */, + FD778B6329B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift */, + FD1D732D2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift */, + FDFE75B02ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift */, + FD428B222B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift */, + 7B5233C5290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift */, + FDDD554D2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift */, + FD3559452CC1FF140088F2A9 /* _034_AddMissingWhisperFlag.swift */, + FD4C53AE2CC1D61E003B10F4 /* _035_ReworkRecipientState.swift */, + FDB5DAC62A9447E7002C8721 /* _036_GroupsRebuildChanges.swift */, + FDE33BBD2D5C3AE800E56F42 /* _037_GroupsExpiredFlag.swift */, + FD860CBB2D6E7A9400BBE29C /* _038_FixBustedInteractionVariant.swift */, + FD8A5B312DC191AB004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift */, + FD70F25B2DC1F176003729B7 /* _040_MessageDeduplicationTable.swift */, + 947D7FE22D5181F400E8E413 /* _041_RenameTableSettingToKeyValueStore.swift */, + FD78E9F72DDD742100D55B50 /* _042_MoveSettingsToLibSession.swift */, + FD05594D2E012D1A00DC48CE /* _043_RenameAttachments.swift */, + 94CD95C02E0CBF1C0097754D /* _044_AddProMessageFlag.swift */, ); path = Migrations; sourceTree = ""; @@ -4049,27 +4060,11 @@ FD17D79D27F40CAA00122BE0 /* Database */ = { isa = PBXGroup; children = ( - FD17D79E27F40CC000122BE0 /* Migrations */, FD17D7A827F41BE300122BE0 /* Models */, ); path = Database; sourceTree = ""; }; - FD17D79E27F40CC000122BE0 /* Migrations */ = { - isa = PBXGroup; - children = ( - FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */, - FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */, - FD17D7A327F40F8100122BE0 /* _003_YDBToGRDBMigration.swift */, - FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */, - FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */, - FD7F74562BAA9D31006DDFD8 /* _006_DropSnodeCache.swift */, - FD61FCFA2D34A5DE005752DE /* _007_SplitSnodeReceivedMessageInfo.swift */, - FD8A5B332DC1A726004C689B /* _008_ResetUserConfigLastHashes.swift */, - ); - path = Migrations; - sourceTree = ""; - }; FD17D7A827F41BE300122BE0 /* Models */ = { isa = PBXGroup; children = ( @@ -4084,7 +4079,6 @@ FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */, FD17D7B727F51ECA00122BE0 /* Migration.swift */, FD4BB22A2D63F20600D0DC3D /* MigrationHelper.swift */, - FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */, FD7162DA281B6C440060647B /* TypedTableAlias.swift */, FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */, FD1A553D2E14BE0E003761E4 /* PagedData.swift */, @@ -4095,7 +4089,6 @@ FD17D7BB27F51F5C00122BE0 /* Utilities */ = { isa = PBXGroup; children = ( - FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */, FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */, FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */, FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */, @@ -4106,19 +4099,6 @@ path = Utilities; sourceTree = ""; }; - FD17D7C827F546CE00122BE0 /* Migrations */ = { - isa = PBXGroup; - children = ( - FD17D7C927F546D900122BE0 /* _001_InitialSetupMigration.swift */, - FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */, - FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */, - FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */, - 945D9C572D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift */, - 947D7FE22D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift */, - ); - path = Migrations; - sourceTree = ""; - }; FD17D7CB27F546F500122BE0 /* Models */ = { isa = PBXGroup; children = ( @@ -4261,7 +4241,6 @@ FD37E9F728A5F143003AE748 /* Migrations */ = { isa = PBXGroup; children = ( - FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */, ); path = Migrations; sourceTree = ""; @@ -6031,7 +6010,6 @@ 7BAF54D427ACCF01003D12F8 /* SAEScreenLockViewController.swift in Sources */, B817AD9C26436F73009DF825 /* ThreadPickerVC.swift in Sources */, FD78EA0A2DDFE45E00D55B50 /* Interaction+UI.swift in Sources */, - FD2272DE2C34F11F004D8A6C /* _001_ThemePreferences.swift in Sources */, 7BAF54D327ACCF01003D12F8 /* ShareAppExtensionContext.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6203,14 +6181,12 @@ FDF848CE29405C5B007DCAE5 /* UpdateExpiryAllRequest.swift in Sources */, FDF848C229405C5A007DCAE5 /* OxenDaemonRPCRequest.swift in Sources */, FDF848DC29405C5B007DCAE5 /* RevokeSubaccountRequest.swift in Sources */, - FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */, FDF848D029405C5B007DCAE5 /* UpdateExpiryResponse.swift in Sources */, FDF848D329405C5B007DCAE5 /* UpdateExpiryAllResponse.swift in Sources */, FDD20C162A09E64A003898FB /* GetExpiriesRequest.swift in Sources */, FDF848BC29405C5A007DCAE5 /* SnodeRecursiveResponse.swift in Sources */, FDF848C029405C5A007DCAE5 /* ONSResolveResponse.swift in Sources */, FD2272AC2C33E337004D8A6C /* IPv4.swift in Sources */, - FD17D7A427F40F8100122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, FD2272D82C34EDE7004D8A6C /* SnodeAPIEndpoint.swift in Sources */, FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */, FDB5DAF32A96DD4F002C8721 /* PreparedRequest+Sending.swift in Sources */, @@ -6241,9 +6217,7 @@ FD2272AF2C33E337004D8A6C /* JSON.swift in Sources */, FD2272D62C34ED6A004D8A6C /* RetryWithDependencies.swift in Sources */, FDF848D229405C5B007DCAE5 /* LegacyGetMessagesRequest.swift in Sources */, - FD8A5B342DC1A732004C689B /* _008_ResetUserConfigLastHashes.swift in Sources */, FDF848E529405D6E007DCAE5 /* SnodeAPIError.swift in Sources */, - FD61FCFB2D34A5EA005752DE /* _007_SplitSnodeReceivedMessageInfo.swift in Sources */, FDF848D529405C5B007DCAE5 /* DeleteAllMessagesResponse.swift in Sources */, FD2272B22C33E337004D8A6C /* PreparedRequest.swift in Sources */, FDF848BF29405C5A007DCAE5 /* SnodeResponse.swift in Sources */, @@ -6254,18 +6228,13 @@ FD2272BA2C33E337004D8A6C /* HTTPHeader.swift in Sources */, FDF848CD29405C5B007DCAE5 /* GetNetworkTimestampResponse.swift in Sources */, FDF848DA29405C5B007DCAE5 /* GetMessagesResponse.swift in Sources */, - FD39353628F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift in Sources */, - FD7F74572BAA9D31006DDFD8 /* _006_DropSnodeCache.swift in Sources */, FDF8489429405C1B007DCAE5 /* SnodeAPI.swift in Sources */, FD2286682C37DA3B00BC06F7 /* LibSession+Networking.swift in Sources */, FD2272A92C33E337004D8A6C /* ContentProxy.swift in Sources */, FDF848C829405C5B007DCAE5 /* ONSResolveRequest.swift in Sources */, - FD6DF00B2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */, - C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */, FDF848C929405C5B007DCAE5 /* SnodeRequest.swift in Sources */, FDF848CF29405C5B007DCAE5 /* SendMessageRequest.swift in Sources */, FD2272BE2C34B710004D8A6C /* Publisher+Utilities.swift in Sources */, - FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */, FD3765E72ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift in Sources */, FD2272BC2C33E337004D8A6C /* URLResponse+Utilities.swift in Sources */, FD2272B52C33E337004D8A6C /* BatchResponse.swift in Sources */, @@ -6286,7 +6255,6 @@ files = ( FD2272C82C34EB0A004D8A6C /* Job.swift in Sources */, FD2272C72C34EAF5004D8A6C /* ColumnExpressible.swift in Sources */, - 945D9C582D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift in Sources */, FDB3486E2BE8457F00B716C2 /* BackgroundTaskManager.swift in Sources */, 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */, FD428B192B4B576F006D0888 /* AppContext.swift in Sources */, @@ -6299,7 +6267,6 @@ FD42ECD62E3308B5002D03EA /* ObservableKey+SessionUtilitiesKit.swift in Sources */, FDE754D22C9BAF53002A2623 /* JobDependencies.swift in Sources */, FDE755242C9BC1D1002A2623 /* Publisher+Utilities.swift in Sources */, - FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */, 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */, FD00CDCB2D5317A7006B96D3 /* Scheduler+Utilities.swift in Sources */, FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */, @@ -6325,7 +6292,6 @@ FD5931A72A8DA5DA0040147D /* SQLInterpolation+Utilities.swift in Sources */, FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */, FD3765EA2ADE37B400DC1489 /* Authentication.swift in Sources */, - FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */, FDE755202C9BC1A6002A2623 /* CacheConfig.swift in Sources */, FD1A553E2E14BE11003761E4 /* PagedData.swift in Sources */, FD4BB22C2D63FA8600D0DC3D /* (null) in Sources */, @@ -6344,7 +6310,6 @@ FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */, FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */, FDB3DA8B2E24834000148F8D /* AVURLAsset+Utilities.swift in Sources */, - FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */, FD848B9328420164000E298B /* UnicodeScalar+Utilities.swift in Sources */, FDE754CE2C9BAF37002A2623 /* ImageFormat.swift in Sources */, FDB11A542DCD7A7F00BEF49F /* Task+Utilities.swift in Sources */, @@ -6354,11 +6319,9 @@ FDE33BBC2D5C124900E56F42 /* DispatchTimeInterval+Utilities.swift in Sources */, FD7115FA28C8153400B47552 /* UIBarButtonItem+Combine.swift in Sources */, FD705A92278D051200F16121 /* ReusableView.swift in Sources */, - FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */, FDE754DD2C9BAF8A002A2623 /* Mnemonic.swift in Sources */, FD52CB652E13B6E900A4DA70 /* ObservationBuilder.swift in Sources */, FDBEE52E2B6A18B900C143A0 /* UserDefaultsConfig.swift in Sources */, - 947D7FE32D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift in Sources */, FD78EA042DDEC3C500D55B50 /* MultiTaskManager.swift in Sources */, FD78EA062DDEC8F600D55B50 /* AsyncSequence+Utilities.swift in Sources */, FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */, @@ -6396,7 +6359,6 @@ FDE754D42C9BAF6B002A2623 /* UICollectionView+ReusableView.swift in Sources */, FDE754C02C9BAEF6002A2623 /* Array+Utilities.swift in Sources */, FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */, - FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */, FDAA16762AC28A3B00DDBF77 /* UserDefaultsType.swift in Sources */, FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */, FD7F745B2BAAA35E006DDFD8 /* LibSession.swift in Sources */, @@ -6418,7 +6380,6 @@ FDF01FAD2A9ECC4200CAF969 /* SingletonConfig.swift in Sources */, FDE755182C9BC169002A2623 /* UIAlertAction+Utilities.swift in Sources */, FD7115F828C8151C00B47552 /* DisposableBarButtonItem.swift in Sources */, - FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, FDE7551B2C9BC169002A2623 /* UINavigationController+Utilities.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6428,10 +6389,11 @@ buildActionMask = 2147483647; files = ( FD8ECF7B29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift in Sources */, - 7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */, + FDD23AE62E458CAA0057E853 /* _023_SplitSnodeReceivedMessageInfo.swift in Sources */, + 7B81682828B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift in Sources */, FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */, FD2273002C352D8E004D8A6C /* LibSession+GroupKeys.swift in Sources */, - FD70F25C2DC1F184003729B7 /* _026_MessageDeduplicationTable.swift in Sources */, + FD70F25C2DC1F184003729B7 /* _040_MessageDeduplicationTable.swift in Sources */, FD2272FB2C352D8E004D8A6C /* LibSession+UserGroups.swift in Sources */, B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */, FD22726F2C32911C004D8A6C /* ProcessPendingGroupMemberRemovalsJob.swift in Sources */, @@ -6441,7 +6403,7 @@ FDC13D582A17207D007267C7 /* UnsubscribeResponse.swift in Sources */, FD09799927FFC1A300936362 /* Attachment.swift in Sources */, FD245C5F2850662200B966DD /* OWSWindowManager.m in Sources */, - FDF40CDE2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift in Sources */, + FDF40CDE2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift in Sources */, FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */, FD78E9F42DDABA4F00D55B50 /* AttachmentUploader.swift in Sources */, FDE754A32C9A8FD1002A2623 /* SwarmPoller.swift in Sources */, @@ -6455,7 +6417,7 @@ FD47E0B12AA6A05800A55E41 /* Authentication+SessionMessagingKit.swift in Sources */, FD2272832C337830004D8A6C /* GroupPoller.swift in Sources */, FD22726C2C32911C004D8A6C /* GroupLeavingJob.swift in Sources */, - FDE33BBE2D5C3AF100E56F42 /* _023_GroupsExpiredFlag.swift in Sources */, + FDE33BBE2D5C3AF100E56F42 /* _037_GroupsExpiredFlag.swift in Sources */, FDF848F729414477007DCAE5 /* CurrentUserPoller.swift in Sources */, C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */, FD2272FD2C352D8E004D8A6C /* LibSession+ConvoInfoVolatile.swift in Sources */, @@ -6470,28 +6432,28 @@ FD2273082C353109004D8A6C /* DisplayPictureManager.swift in Sources */, FDE521A22E0D23AB00061B8E /* ObservableKey+SessionMessagingKit.swift in Sources */, FD2273022C352D8E004D8A6C /* LibSession+GroupInfo.swift in Sources */, - FDDD554E2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift in Sources */, + FDDD554E2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift in Sources */, FD09798927FD1C5A00936362 /* OpenGroup.swift in Sources */, - 94CD95C12E0CBF430097754D /* _029_AddProMessageFlag.swift in Sources */, + 94CD95C12E0CBF430097754D /* _044_AddProMessageFlag.swift in Sources */, FD2272FC2C352D8E004D8A6C /* LibSession+Contacts.swift in Sources */, FD848B9628422A2A000E298B /* MessageViewModel.swift in Sources */, FD05593D2DFA3A2800DC48CE /* VoipPayloadKey.swift in Sources */, FD2272782C32911C004D8A6C /* AttachmentDownloadJob.swift in Sources */, FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */, - FD428B232B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift in Sources */, + FD428B232B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift in Sources */, FD2272742C32911C004D8A6C /* ConfigMessageReceiveJob.swift in Sources */, FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */, FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */, FD22727A2C32911C004D8A6C /* GroupInviteMemberJob.swift in Sources */, - FDB5DAC72A9447E7002C8721 /* _022_GroupsRebuildChanges.swift in Sources */, - FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */, + FDB5DAC72A9447E7002C8721 /* _036_GroupsRebuildChanges.swift in Sources */, + FD09B7E5288670BB00ED0B66 /* _017_EmojiReacts.swift in Sources */, FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */, FDD20C1A2A0A03AC003898FB /* DeleteInboxResponse.swift in Sources */, 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */, FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */, FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */, FD2272FA2C352D8E004D8A6C /* LibSession+SharedGroup.swift in Sources */, - FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */, + FD37EA0F28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift in Sources */, FD2272772C32911C004D8A6C /* AttachmentUploadJob.swift in Sources */, 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */, FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */, @@ -6501,7 +6463,7 @@ FDC4386727B4E10E00C60D73 /* Capabilities.swift in Sources */, FDC438A427BB107F00C60D73 /* UserBanRequest.swift in Sources */, FDE754F22C9BB08B002A2623 /* Crypto+SessionMessagingKit.swift in Sources */, - FD4C53AF2CC1D62E003B10F4 /* _021_ReworkRecipientState.swift in Sources */, + FD4C53AF2CC1D62E003B10F4 /* _035_ReworkRecipientState.swift in Sources */, C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */, FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */, FDB4BBC72838B91E00B7C95D /* LinkPreviewError.swift in Sources */, @@ -6511,24 +6473,30 @@ FD09798727FD1B7800936362 /* GroupMember.swift in Sources */, FD78EA0D2DDFEDE200D55B50 /* LibSession+Local.swift in Sources */, FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */, + FDD23AE12E457CDE0057E853 /* _005_SNK_SetupStandardJobs.swift in Sources */, FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */, FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */, FDE754F02C9BB08B002A2623 /* Crypto+Attachments.swift in Sources */, - FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, + FD17D79927F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift in Sources */, FDE754A12C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift in Sources */, FDF0B7512807BA56004C14C5 /* NotificationsManagerType.swift in Sources */, FD2272722C32911C004D8A6C /* FailedAttachmentDownloadsJob.swift in Sources */, FDC13D5A2A1721C5007267C7 /* LegacyNotifyRequest.swift in Sources */, + FDD23AEA2E458EB00057E853 /* _012_AddJobPriority.swift in Sources */, FD245C59285065FC00B966DD /* ControlMessage.swift in Sources */, FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */, B8DE1FB626C22FCB0079C9CE /* CallMessage.swift in Sources */, + FDD23AEC2E458F980057E853 /* _024_ResetUserConfigLastHashes.swift in Sources */, FD245C50285065C700B966DD /* VisibleMessage+Quote.swift in Sources */, + FDD23AE82E458DD40057E853 /* _002_SUK_SetupStandardJobs.swift in Sources */, FD72BDA12BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift in Sources */, FD4C4E9C2B02E2A300C72199 /* DisplayPictureError.swift in Sources */, + FDD23AE22E457CE50057E853 /* _008_SNK_YDBToGRDBMigration.swift in Sources */, FD5C7307284F103B0029977D /* MessageReceiver+MessageRequests.swift in Sources */, C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */, FD3FAB592ADF906300DC5421 /* Profile+CurrentUser.swift in Sources */, FDEF573E2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift in Sources */, + FDD23ADF2E457CAA0057E853 /* _016_ThemePreferences.swift in Sources */, FDB11A5D2DD300D300BEF49F /* SNProtoContent+Utilities.swift in Sources */, FDE755002C9BB0FA002A2623 /* SessionEnvironment.swift in Sources */, FDB5DADC2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift in Sources */, @@ -6544,11 +6512,11 @@ B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */, C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */, C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */, - FDF0B7422804EA4F004C14C5 /* _002_SetupStandardJobs.swift in Sources */, + FDF0B7422804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift in Sources */, B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */, FDF8487F29405994007DCAE5 /* HTTPHeader+OpenGroup.swift in Sources */, - FD8ECF7D2934293A00C0D1BB /* _013_SessionUtilChanges.swift in Sources */, - FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */, + FD8ECF7D2934293A00C0D1BB /* _027_SessionUtilChanges.swift in Sources */, + FD17D7A227F40F0500122BE0 /* _006_SMK_InitialSetupMigration.swift in Sources */, FD245C5D2850660F00B966DD /* OWSAudioPlayer.m in Sources */, FD2272FE2C352D8E004D8A6C /* LibSession+GroupMembers.swift in Sources */, FDF0B7582807F368004C14C5 /* MessageReceiverError.swift in Sources */, @@ -6565,7 +6533,7 @@ FDB5DAC12A9443A5002C8721 /* MessageSender+Groups.swift in Sources */, FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */, FD2272702C32911C004D8A6C /* DisappearingMessagesJob.swift in Sources */, - FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */, + FD7115F228C6CB3900B47552 /* _019_AddThreadIdToFTS.swift in Sources */, FD716E6428502DDD00C96BF4 /* CallManagerProtocol.swift in Sources */, 943C6D822B75E061004ACE64 /* Message+DisappearingMessages.swift in Sources */, FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */, @@ -6578,8 +6546,9 @@ FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */, FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */, FDB5DAE62A95D8B0002C8721 /* GroupUpdateDeleteMemberContentMessage.swift in Sources */, - 7B5233C6290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift in Sources */, + 7B5233C6290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift in Sources */, FD5C72FD284F0EC90029977D /* MessageReceiver+ExpirationTimers.swift in Sources */, + FDD23AEB2E458F4D0057E853 /* _020_AddJobUniqueHash.swift in Sources */, FDB11A502DCC6ADE00BEF49F /* ThreadUpdateInfo.swift in Sources */, B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */, FDC1BD662CFD6C4F002CDC71 /* Config.swift in Sources */, @@ -6588,11 +6557,14 @@ C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */, FDE7549B2C940108002A2623 /* MessageViewModel+DeletionActions.swift in Sources */, FD09799527FE7B8E00936362 /* Interaction.swift in Sources */, - FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */, - 7BAA7B6628D2DE4700AE1489 /* _009_OpenGroupPermission.swift in Sources */, + FD37EA0D28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift in Sources */, + FDD23AE32E457CFE0057E853 /* _010_FlagMessageHashAsDeletedOrInvalid.swift in Sources */, + 7BAA7B6628D2DE4700AE1489 /* _018_OpenGroupPermission.swift in Sources */, FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */, FD2286692C37DA5500BC06F7 /* PollerType.swift in Sources */, - FD860CBC2D6E7A9F00BBE29C /* _024_FixBustedInteractionVariant.swift in Sources */, + FD860CBC2D6E7A9F00BBE29C /* _038_FixBustedInteractionVariant.swift in Sources */, + FDD23AE72E458DBC0057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */, + FDD23AE42E458C810057E853 /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */, FD22727B2C32911C004D8A6C /* MessageSendJob.swift in Sources */, FD78E9EE2DD6D32500D55B50 /* ImageDataManager+Singleton.swift in Sources */, FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */, @@ -6605,39 +6577,41 @@ FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */, FDC13D492A16EC20007267C7 /* Service.swift in Sources */, FDBA8A842D597975007C19C0 /* FailedGroupInvitesAndPromotionsJob.swift in Sources */, - FD778B6429B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift in Sources */, + FD778B6429B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift in Sources */, FDC13D562A171FE4007267C7 /* UnsubscribeRequest.swift in Sources */, FD1A55432E179AED003761E4 /* ObservableKeyEvent+Utilities.swift in Sources */, C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */, + FDD23AE92E458E020057E853 /* _003_SUK_YDBToGRDBMigration.swift in Sources */, FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */, FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */, FD22727F2C32911C004D8A6C /* GetExpirationJob.swift in Sources */, FD2272792C32911C004D8A6C /* DisplayPictureDownloadJob.swift in Sources */, FD981BD92DC9A69600564172 /* NotificationUserInfoKey.swift in Sources */, C352A2FF25574B6300338F3E /* (null) in Sources */, + FDD23AE52E458C940057E853 /* _022_DropSnodeCache.swift in Sources */, FD16AB612A1DD9B60083D849 /* ProfilePictureView+Convenience.swift in Sources */, B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */, FD2272762C32911C004D8A6C /* ExpirationUpdateJob.swift in Sources */, FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */, - FD368A6829DE8F9C000DBF1E /* _012_AddFTSIfNeeded.swift in Sources */, + FD368A6829DE8F9C000DBF1E /* _026_AddFTSIfNeeded.swift in Sources */, FD5C7301284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift in Sources */, C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */, FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */, - FDFE75B12ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift in Sources */, + FDFE75B12ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift in Sources */, FD09799B27FFC82D00936362 /* Quote.swift in Sources */, FD2273012C352D8E004D8A6C /* LibSession+Shared.swift in Sources */, C3C2A74425539EB700C340D1 /* Message.swift in Sources */, FD245C682850666300B966DD /* Message+Destination.swift in Sources */, FDF8488029405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift in Sources */, - FD8A5B322DC191B4004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift in Sources */, + FD8A5B322DC191B4004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift in Sources */, FD245C632850664600B966DD /* Configuration.swift in Sources */, FD981BC62DC3310B00564172 /* ExtensionHelper.swift in Sources */, - FD78E9FA2DDD74D200D55B50 /* _027_MoveSettingsToLibSession.swift in Sources */, + FD78E9FA2DDD74D200D55B50 /* _042_MoveSettingsToLibSession.swift in Sources */, FD2272FF2C352D8E004D8A6C /* LibSession+UserProfile.swift in Sources */, FD5C7305284F0FF30029977D /* MessageReceiver+VisibleMessages.swift in Sources */, FDB5DAE82A95D96C002C8721 /* MessageReceiver+Groups.swift in Sources */, 94CD95BD2E09083C0097754D /* LibSession+Pro.swift in Sources */, - FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */, + FD1D732E2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift in Sources */, FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */, FDAA167F2AC5290000DDBF77 /* Preferences+NotificationPreviewType.swift in Sources */, FD09797027FA6FF300936362 /* Profile.swift in Sources */, @@ -6652,22 +6626,24 @@ FDB5DADA2A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift in Sources */, FDF71EA32B072C2800A8D6B5 /* LibSessionMessage.swift in Sources */, FDAA167D2AC528A200DDBF77 /* Preferences+Sound.swift in Sources */, + FDD23AED2E4590A10057E853 /* _041_RenameTableSettingToKeyValueStore.swift in Sources */, FDE754FE2C9BB0D0002A2623 /* Threading+SMK.swift in Sources */, FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */, FDF2F0222DAE1AF500491E8A /* MessageReceiver+LegacyClosedGroups.swift in Sources */, FD02CC122C367762009AB976 /* Request+PushNotificationAPI.swift in Sources */, - FD05594E2E012D2700DC48CE /* _028_RenameAttachments.swift in Sources */, + FD05594E2E012D2700DC48CE /* _043_RenameAttachments.swift in Sources */, FD22726E2C32911C004D8A6C /* FailedMessageSendsJob.swift in Sources */, FDB5DAD42A9483F3002C8721 /* GroupUpdateInviteMessage.swift in Sources */, FDB5DAE22A95D8A0002C8721 /* GroupUpdateInviteResponseMessage.swift in Sources */, FDB11A522DCC6B0000BEF49F /* OpenGroupUrlInfo.swift in Sources */, - FD3559462CC1FF200088F2A9 /* _020_AddMissingWhisperFlag.swift in Sources */, + FD3559462CC1FF200088F2A9 /* _034_AddMissingWhisperFlag.swift in Sources */, FD5C72F9284F0E880029977D /* MessageReceiver+TypingIndicators.swift in Sources */, + FDD23AE02E457CD40057E853 /* _004_SNK_InitialSetupMigration.swift in Sources */, FD5C7303284F0FA50029977D /* MessageReceiver+Calls.swift in Sources */, FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */, FDC438AA27BB12BB00C60D73 /* UserModeratorRequest.swift in Sources */, FDC4385D27B4C18900C60D73 /* Room.swift in Sources */, - FDE755022C9BB122002A2623 /* _011_AddPendingReadReceipts.swift in Sources */, + FDE755022C9BB122002A2623 /* _025_AddPendingReadReceipts.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6682,7 +6658,6 @@ FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */, B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */, 7B4EF25A2934743000CB351D /* SessionTableViewTitleView.swift in Sources */, - FD2272D92C34EED6004D8A6C /* _001_ThemePreferences.swift in Sources */, 942256A12C23F90700C0FDBF /* CustomTopTabBar.swift in Sources */, FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */, 4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */, @@ -6963,12 +6938,14 @@ FD65318D2AA025C500DFEEAA /* TestDependencies.swift in Sources */, FD49E2492B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, FD99D0922D10F5EE005D2E15 /* ThreadSafeSpec.swift in Sources */, + FDD23AF02E459EDD0057E853 /* _020_AddJobUniqueHash.swift in Sources */, FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */, FD9DD2732A72516D00ECB68E /* TestExtensions.swift in Sources */, FD3FAB6E2AF1B28C00DC5421 /* MockFileManager.swift in Sources */, FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */, FDB11A592DD17D0600BEF49F /* MockLogger.swift in Sources */, FD8A5B302DC18D61004C689B /* GeneralCacheSpec.swift in Sources */, + FDD23AEE2E459E470057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */, FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FD0150402CA2433D005B08A1 /* BencodeDecoderSpec.swift in Sources */, FD0150412CA2433D005B08A1 /* BencodeEncoderSpec.swift in Sources */, @@ -6986,6 +6963,7 @@ FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */, FD0969FB2A6A00B100C5C365 /* Mocked.swift in Sources */, FD0150292CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, + FDD23AEF2E459EC90057E853 /* _012_AddJobPriority.swift in Sources */, FD481A942CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 1dd66f07f8..942992dc90 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -70,7 +70,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD self.loadingViewController = LoadingViewController() AppSetup.setupEnvironment( - additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], appSpecificBlock: { [dependencies] in Log.setup(with: Logger(primaryPrefix: "Session", using: dependencies)) Log.info(.cat, "Setting up environment.") @@ -222,7 +221,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Dispatch async so things can continue to be progressed if a migration does need to run DispatchQueue.global(qos: .userInitiated).async { [weak self, dependencies] in AppSetup.runPostSetupMigrations( - additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], migrationProgressChanged: { progress, minEstimatedTotalTime in self?.loadingViewController?.updateProgress( progress: progress, @@ -606,7 +604,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // The re-run the migration (should succeed since there is no data) AppSetup.runPostSetupMigrations( - additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], migrationProgressChanged: { [weak self] progress, minEstimatedTotalTime in self?.loadingViewController?.updateProgress( progress: progress, diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 2f861719a4..5260915ddc 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -2,58 +2,53 @@ import Foundation import GRDB import SessionUtilitiesKit -public enum SNMessagingKit: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .messagingKit, - migrations: [ - [ - _001_InitialSetupMigration.self, - _002_SetupStandardJobs.self - ], // Initial DB Creation - [ - _003_YDBToGRDBMigration.self - ], // YDB to GRDB Migration - [ - _004_RemoveLegacyYDB.self - ], // Legacy DB removal - [ - _005_FixDeletedMessageReadState.self, - _006_FixHiddenModAdminSupport.self, - _007_HomeQueryOptimisationIndexes.self - ], // Add job priorities - [ - _008_EmojiReacts.self, - _009_OpenGroupPermission.self, - _010_AddThreadIdToFTS.self - ], // Fix thread FTS - [ - _011_AddPendingReadReceipts.self, - _012_AddFTSIfNeeded.self, - _013_SessionUtilChanges.self, - _014_GenerateInitialUserConfigDumps.self, - _015_BlockCommunityMessageRequests.self, - _016_MakeBrokenProfileTimestampsNullable.self, - _017_RebuildFTSIfNeeded_2_4_5.self, - _018_DisappearingMessagesConfiguration.self, - _019_ScheduleAppUpdateCheckJob.self, - _020_AddMissingWhisperFlag.self, - _021_ReworkRecipientState.self, - _022_GroupsRebuildChanges.self, - _023_GroupsExpiredFlag.self, - _024_FixBustedInteractionVariant.self, - _025_DropLegacyClosedGroupKeyPairTable.self, - _026_MessageDeduplicationTable.self - ], - [], // Renamed `Setting` to `KeyValueStore` - [ - _027_MoveSettingsToLibSession.self, - _028_RenameAttachments.self, - _029_AddProMessageFlag.self - ] - ] - ) - } +public enum SNMessagingKit { // Just to make the external API nice + public static let migrations: [Migration.Type] = [ + _001_SUK_InitialSetupMigration.self, + _002_SUK_SetupStandardJobs.self, + _003_SUK_YDBToGRDBMigration.self, + _004_SNK_InitialSetupMigration.self, + _005_SNK_SetupStandardJobs.self, + _006_SMK_InitialSetupMigration.self, + _007_SMK_SetupStandardJobs.self, + _008_SNK_YDBToGRDBMigration.self, + _009_SMK_YDBToGRDBMigration.self, + _010_FlagMessageHashAsDeletedOrInvalid.self, + _011_RemoveLegacyYDB.self, + _012_AddJobPriority.self, + _013_FixDeletedMessageReadState.self, + _014_FixHiddenModAdminSupport.self, + _015_HomeQueryOptimisationIndexes.self, + _016_ThemePreferences.self, + _017_EmojiReacts.self, + _018_OpenGroupPermission.self, + _019_AddThreadIdToFTS.self, + _020_AddJobUniqueHash.self, + _021_AddSnodeReveivedMessageInfoPrimaryKey.self, + _022_DropSnodeCache.self, + _023_SplitSnodeReceivedMessageInfo.self, + _024_ResetUserConfigLastHashes.self, + _025_AddPendingReadReceipts.self, + _026_AddFTSIfNeeded.self, + _027_SessionUtilChanges.self, + _028_GenerateInitialUserConfigDumps.self, + _029_BlockCommunityMessageRequests.self, + _030_MakeBrokenProfileTimestampsNullable.self, + _031_RebuildFTSIfNeeded_2_4_5.self, + _032_DisappearingMessagesConfiguration.self, + _033_ScheduleAppUpdateCheckJob.self, + _034_AddMissingWhisperFlag.self, + _035_ReworkRecipientState.self, + _036_GroupsRebuildChanges.self, + _037_GroupsExpiredFlag.self, + _038_FixBustedInteractionVariant.self, + _039_DropLegacyClosedGroupKeyPairTable.self, + _040_MessageDeduplicationTable.self, + _041_RenameTableSettingToKeyValueStore.self, + _042_MoveSettingsToLibSession.self, + _043_RenameAttachments.self, + _044_AddProMessageFlag.self + ] public static func configure(using dependencies: Dependencies) { // Configure the job executors diff --git a/SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionMessagingKit/Database/Migrations/_001_SUK_InitialSetupMigration.swift similarity index 94% rename from SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift rename to SessionMessagingKit/Database/Migrations/_001_SUK_InitialSetupMigration.swift index 038c6de166..d91ffaca7d 100644 --- a/SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_001_SUK_InitialSetupMigration.swift @@ -4,10 +4,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _001_InitialSetupMigration: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "initialSetup" +enum _001_SUK_InitialSetupMigration: Migration { + static let identifier: String = "utilitiesKit.initialSetup" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [ Identity.self, Job.self, JobDependencies.self diff --git a/SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionMessagingKit/Database/Migrations/_002_SUK_SetupStandardJobs.swift similarity index 89% rename from SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift rename to SessionMessagingKit/Database/Migrations/_002_SUK_SetupStandardJobs.swift index ca787c2c2c..8f27b313d5 100644 --- a/SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionMessagingKit/Database/Migrations/_002_SUK_SetupStandardJobs.swift @@ -2,12 +2,12 @@ import Foundation import GRDB +import SessionUtilitiesKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` -enum _002_SetupStandardJobs: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "SetupStandardJobs" +enum _002_SUK_SetupStandardJobs: Migration { + static let identifier: String = "utilitiesKit.SetupStandardJobs" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionNetworkingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_SUK_YDBToGRDBMigration.swift similarity index 70% rename from SessionNetworkingKit/Database/Migrations/_003_YDBToGRDBMigration.swift rename to SessionMessagingKit/Database/Migrations/_003_SUK_YDBToGRDBMigration.swift index 382874faf3..5753532d9a 100644 --- a/SessionNetworkingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_SUK_YDBToGRDBMigration.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _003_YDBToGRDBMigration: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "YDBToGRDBMigration" +enum _003_SUK_YDBToGRDBMigration: Migration { + static let identifier: String = "utilitiesKit.YDBToGRDBMigration" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionNetworkingKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionMessagingKit/Database/Migrations/_004_SNK_InitialSetupMigration.swift similarity index 90% rename from SessionNetworkingKit/Database/Migrations/_001_InitialSetupMigration.swift rename to SessionMessagingKit/Database/Migrations/_004_SNK_InitialSetupMigration.swift index 9473c323f8..9852a0a11f 100644 --- a/SessionNetworkingKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_004_SNK_InitialSetupMigration.swift @@ -6,9 +6,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _001_InitialSetupMigration: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "initialSetup" +enum _004_SNK_InitialSetupMigration: Migration { + static let identifier: String = "snodeKit.initialSetup" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionNetworkingKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionMessagingKit/Database/Migrations/_005_SNK_SetupStandardJobs.swift similarity index 91% rename from SessionNetworkingKit/Database/Migrations/_002_SetupStandardJobs.swift rename to SessionMessagingKit/Database/Migrations/_005_SNK_SetupStandardJobs.swift index 845a42fe16..2241868cc8 100644 --- a/SessionNetworkingKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionMessagingKit/Database/Migrations/_005_SNK_SetupStandardJobs.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` -enum _002_SetupStandardJobs: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "SetupStandardJobs" +enum _005_SNK_SetupStandardJobs: Migration { + static let identifier: String = "snodeKit.SetupStandardJobs" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionMessagingKit/Database/Migrations/_006_SMK_InitialSetupMigration.swift similarity index 97% rename from SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift rename to SessionMessagingKit/Database/Migrations/_006_SMK_InitialSetupMigration.swift index 2823930730..6c9e861ca9 100644 --- a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_006_SMK_InitialSetupMigration.swift @@ -6,9 +6,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _001_InitialSetupMigration: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "initialSetup" +enum _006_SMK_InitialSetupMigration: Migration { + static let identifier: String = "messagingKit.initialSetup" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [ Contact.self, Profile.self, SessionThread.self, DisappearingMessagesConfiguration.self, @@ -59,7 +58,7 @@ enum _001_InitialSetupMigration: Migration { /// Create a full-text search table synchronized with the Profile table try db.create(virtualTable: "profile_fts", using: FTS5()) { t in t.synchronize(withTable: "profile") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("nickname") t.column("name") @@ -106,7 +105,7 @@ enum _001_InitialSetupMigration: Migration { /// Create a full-text search table synchronized with the ClosedGroup table try db.create(virtualTable: "closedGroup_fts", using: FTS5()) { t in t.synchronize(withTable: "closedGroup") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("name") } @@ -157,7 +156,7 @@ enum _001_InitialSetupMigration: Migration { /// Create a full-text search table synchronized with the OpenGroup table try db.create(virtualTable: "openGroup_fts", using: FTS5()) { t in t.synchronize(withTable: "openGroup") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("name") } @@ -268,7 +267,7 @@ enum _001_InitialSetupMigration: Migration { /// Create a full-text search table synchronized with the Interaction table try db.create(virtualTable: "interaction_fts", using: FTS5()) { t in t.synchronize(withTable: "interaction") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("body") } diff --git a/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionMessagingKit/Database/Migrations/_007_SMK_SetupStandardJobs.swift similarity index 93% rename from SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift rename to SessionMessagingKit/Database/Migrations/_007_SMK_SetupStandardJobs.swift index b9b3e31588..f7057035e0 100644 --- a/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionMessagingKit/Database/Migrations/_007_SMK_SetupStandardJobs.swift @@ -7,9 +7,8 @@ import SessionNetworkingKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` -enum _002_SetupStandardJobs: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "SetupStandardJobs" +enum _007_SMK_SetupStandardJobs: Migration { + static let identifier: String = "messagingKit.SetupStandardJobs" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_008_SNK_YDBToGRDBMigration.swift similarity index 69% rename from SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift rename to SessionMessagingKit/Database/Migrations/_008_SNK_YDBToGRDBMigration.swift index 565fd3e3f2..ac66dd7c0b 100644 --- a/SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_008_SNK_YDBToGRDBMigration.swift @@ -2,10 +2,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _003_YDBToGRDBMigration: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "YDBToGRDBMigration" +enum _008_SNK_YDBToGRDBMigration: Migration { + static let identifier: String = "snodeKit.YDBToGRDBMigration" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_009_SMK_YDBToGRDBMigration.swift similarity index 79% rename from SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift rename to SessionMessagingKit/Database/Migrations/_009_SMK_YDBToGRDBMigration.swift index 13ecc5df76..7851f48d74 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_009_SMK_YDBToGRDBMigration.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _003_YDBToGRDBMigration: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "YDBToGRDBMigration" +enum _009_SMK_YDBToGRDBMigration: Migration { + static let identifier: String = "messagingKit.YDBToGRDBMigration" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionNetworkingKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift b/SessionMessagingKit/Database/Migrations/_010_FlagMessageHashAsDeletedOrInvalid.swift similarity index 81% rename from SessionNetworkingKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift rename to SessionMessagingKit/Database/Migrations/_010_FlagMessageHashAsDeletedOrInvalid.swift index 989df981c8..ff71d5ffbe 100644 --- a/SessionNetworkingKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift +++ b/SessionMessagingKit/Database/Migrations/_010_FlagMessageHashAsDeletedOrInvalid.swift @@ -7,9 +7,8 @@ import SessionUtilitiesKit /// This migration adds a flag to the `SnodeReceivedMessageInfo` so that when deleting interactions we can /// ignore their hashes when subsequently trying to fetch new messages (which results in the storage server returning /// messages from the beginning of time) -enum _004_FlagMessageHashAsDeletedOrInvalid: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "FlagMessageHashAsDeletedOrInvalid" +enum _010_FlagMessageHashAsDeletedOrInvalid: Migration { + static let identifier: String = "snodeKit.FlagMessageHashAsDeletedOrInvalid" static let minExpectedRunDuration: TimeInterval = 0.2 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift b/SessionMessagingKit/Database/Migrations/_011_RemoveLegacyYDB.swift similarity index 78% rename from SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift rename to SessionMessagingKit/Database/Migrations/_011_RemoveLegacyYDB.swift index c5c4c4a4d8..ef8588451f 100644 --- a/SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift +++ b/SessionMessagingKit/Database/Migrations/_011_RemoveLegacyYDB.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit import SessionNetworkingKit /// This migration used to remove the legacy YapDatabase files (the old logic has been removed and is no longer supported so it now does nothing) -enum _004_RemoveLegacyYDB: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "RemoveLegacyYDB" +enum _011_RemoveLegacyYDB: Migration { + static let identifier: String = "messagingKit.RemoveLegacyYDB" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionUtilitiesKit/Database/Migrations/_004_AddJobPriority.swift b/SessionMessagingKit/Database/Migrations/_012_AddJobPriority.swift similarity index 90% rename from SessionUtilitiesKit/Database/Migrations/_004_AddJobPriority.swift rename to SessionMessagingKit/Database/Migrations/_012_AddJobPriority.swift index 852033b582..93a2c68752 100644 --- a/SessionUtilitiesKit/Database/Migrations/_004_AddJobPriority.swift +++ b/SessionMessagingKit/Database/Migrations/_012_AddJobPriority.swift @@ -2,10 +2,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _004_AddJobPriority: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "AddJobPriority" +enum _012_AddJobPriority: Migration { + static let identifier: String = "utilitiesKit.AddJobPriority" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift b/SessionMessagingKit/Database/Migrations/_013_FixDeletedMessageReadState.swift similarity index 83% rename from SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift rename to SessionMessagingKit/Database/Migrations/_013_FixDeletedMessageReadState.swift index efe33c321d..a49d63d0ca 100644 --- a/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift +++ b/SessionMessagingKit/Database/Migrations/_013_FixDeletedMessageReadState.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration fixes a bug where certain message variants could incorrectly be counted as unread messages -enum _005_FixDeletedMessageReadState: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "FixDeletedMessageReadState" +enum _013_FixDeletedMessageReadState: Migration { + static let identifier: String = "messagingKit.FixDeletedMessageReadState" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift b/SessionMessagingKit/Database/Migrations/_014_FixHiddenModAdminSupport.swift similarity index 85% rename from SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift rename to SessionMessagingKit/Database/Migrations/_014_FixHiddenModAdminSupport.swift index 006b04c283..5247ae2d79 100644 --- a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift +++ b/SessionMessagingKit/Database/Migrations/_014_FixHiddenModAdminSupport.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration fixes an issue where hidden mods/admins weren't getting recognised as mods/admins, it reset's the `info_updates` /// for open groups so they will fully re-fetch their mod/admin lists -enum _006_FixHiddenModAdminSupport: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "FixHiddenModAdminSupport" +enum _014_FixHiddenModAdminSupport: Migration { + static let identifier: String = "messagingKit.FixHiddenModAdminSupport" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift b/SessionMessagingKit/Database/Migrations/_015_HomeQueryOptimisationIndexes.swift similarity index 81% rename from SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift rename to SessionMessagingKit/Database/Migrations/_015_HomeQueryOptimisationIndexes.swift index bf8ded493e..3a0a617928 100644 --- a/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift +++ b/SessionMessagingKit/Database/Migrations/_015_HomeQueryOptimisationIndexes.swift @@ -7,9 +7,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds an index to the interaction table in order to improve the performance of retrieving the number of unread interactions -enum _007_HomeQueryOptimisationIndexes: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "HomeQueryOptimisationIndexes" +enum _015_HomeQueryOptimisationIndexes: Migration { + static let identifier: String = "messagingKit.HomeQueryOptimisationIndexes" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/Session/Database/Migrations/_001_ThemePreferences.swift b/SessionMessagingKit/Database/Migrations/_016_ThemePreferences.swift similarity index 78% rename from Session/Database/Migrations/_001_ThemePreferences.swift rename to SessionMessagingKit/Database/Migrations/_016_ThemePreferences.swift index 8bb9987f95..f19020e3ed 100644 --- a/Session/Database/Migrations/_001_ThemePreferences.swift +++ b/SessionMessagingKit/Database/Migrations/_016_ThemePreferences.swift @@ -13,9 +13,8 @@ import SessionUtilitiesKit /// **Note:** This migration used to live within `SessionUIKit` but we wanted to isolate it and remove dependencies from it so we /// needed to extract this migration into the `Session` and `SessionShareExtension` targets (since both need theming they both /// need to provide this migration as an option during setup) -enum _001_ThemePreferences: Migration { - static let target: TargetMigrations.Identifier = ._deprecatedUIKit - static let identifier: String = "ThemePreferences" +enum _016_ThemePreferences: Migration { + static let identifier: String = "uiKit.ThemePreferences" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -86,25 +85,3 @@ private extension Theme.PrimaryColor { } } } - -enum DeprecatedUIKitMigrationTarget: MigratableTarget { - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: ._deprecatedUIKit, - migrations: [ - // Want to ensure the initial DB stuff has been completed before doing any - // SNUIKit migrations - [], // Initial DB Creation - [], // YDB to GRDB Migration - [], // Legacy DB removal - [ - _001_ThemePreferences.self - ], // Add job priorities - [], // Fix thread FTS - [], - [], // Renamed `Setting` to `KeyValueStore` - [] - ] - ) - } -} diff --git a/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift b/SessionMessagingKit/Database/Migrations/_017_EmojiReacts.swift similarity index 91% rename from SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift rename to SessionMessagingKit/Database/Migrations/_017_EmojiReacts.swift index bcd9c2f84b..c102846bad 100644 --- a/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift +++ b/SessionMessagingKit/Database/Migrations/_017_EmojiReacts.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds the new types needed for Emoji Reacts -enum _008_EmojiReacts: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "EmojiReacts" +enum _017_EmojiReacts: Migration { + static let identifier: String = "messagingKit.EmojiReacts" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [Reaction.self] diff --git a/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift b/SessionMessagingKit/Database/Migrations/_018_OpenGroupPermission.swift similarity index 83% rename from SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift rename to SessionMessagingKit/Database/Migrations/_018_OpenGroupPermission.swift index b8e7c47efb..bf51074058 100644 --- a/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift +++ b/SessionMessagingKit/Database/Migrations/_018_OpenGroupPermission.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _009_OpenGroupPermission: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "OpenGroupPermission" +enum _018_OpenGroupPermission: Migration { + static let identifier: String = "messagingKit.OpenGroupPermission" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_010_AddThreadIdToFTS.swift b/SessionMessagingKit/Database/Migrations/_019_AddThreadIdToFTS.swift similarity index 82% rename from SessionMessagingKit/Database/Migrations/_010_AddThreadIdToFTS.swift rename to SessionMessagingKit/Database/Migrations/_019_AddThreadIdToFTS.swift index 9c2aea1207..92dfecc4fd 100644 --- a/SessionMessagingKit/Database/Migrations/_010_AddThreadIdToFTS.swift +++ b/SessionMessagingKit/Database/Migrations/_019_AddThreadIdToFTS.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration recreates the interaction FTS table and adds the threadId so we can do a performant in-conversation /// searh (currently it's much slower than the global search) -enum _010_AddThreadIdToFTS: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddThreadIdToFTS" +enum _019_AddThreadIdToFTS: Migration { + static let identifier: String = "messagingKit.AddThreadIdToFTS" static let minExpectedRunDuration: TimeInterval = 3 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -22,7 +21,7 @@ enum _010_AddThreadIdToFTS: Migration { try db.create(virtualTable: "interaction_fts", using: FTS5()) { t in t.synchronize(withTable: "interaction") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("body") t.column("threadId") diff --git a/SessionUtilitiesKit/Database/Migrations/_005_AddJobUniqueHash.swift b/SessionMessagingKit/Database/Migrations/_020_AddJobUniqueHash.swift similarity index 76% rename from SessionUtilitiesKit/Database/Migrations/_005_AddJobUniqueHash.swift rename to SessionMessagingKit/Database/Migrations/_020_AddJobUniqueHash.swift index e4a36701f5..b9bebf7e81 100644 --- a/SessionUtilitiesKit/Database/Migrations/_005_AddJobUniqueHash.swift +++ b/SessionMessagingKit/Database/Migrations/_020_AddJobUniqueHash.swift @@ -2,10 +2,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _005_AddJobUniqueHash: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "AddJobUniqueHash" +enum _020_AddJobUniqueHash: Migration { + static let identifier: String = "utilitiesKit.AddJobUniqueHash" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionNetworkingKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift b/SessionMessagingKit/Database/Migrations/_021_AddSnodeReveivedMessageInfoPrimaryKey.swift similarity index 91% rename from SessionNetworkingKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift rename to SessionMessagingKit/Database/Migrations/_021_AddSnodeReveivedMessageInfoPrimaryKey.swift index 6565fc40d1..55e215871e 100644 --- a/SessionNetworkingKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift +++ b/SessionMessagingKit/Database/Migrations/_021_AddSnodeReveivedMessageInfoPrimaryKey.swift @@ -2,12 +2,12 @@ import Foundation import GRDB +import SessionNetworkingKit import SessionUtilitiesKit /// This migration adds a primary key to `SnodeReceivedMessageInfo` based on the key and hash to speed up lookup -enum _005_AddSnodeReveivedMessageInfoPrimaryKey: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "AddSnodeReveivedMessageInfoPrimaryKey" +enum _021_AddSnodeReveivedMessageInfoPrimaryKey: Migration { + static let identifier: String = "snodeKit.AddSnodeReveivedMessageInfoPrimaryKey" static let minExpectedRunDuration: TimeInterval = 0.2 static let createdTables: [(TableRecord & FetchableRecord).Type] = [SnodeReceivedMessageInfo.self] diff --git a/SessionNetworkingKit/Database/Migrations/_006_DropSnodeCache.swift b/SessionMessagingKit/Database/Migrations/_022_DropSnodeCache.swift similarity index 86% rename from SessionNetworkingKit/Database/Migrations/_006_DropSnodeCache.swift rename to SessionMessagingKit/Database/Migrations/_022_DropSnodeCache.swift index 6cc16ea4ef..af5ceaaa5d 100644 --- a/SessionNetworkingKit/Database/Migrations/_006_DropSnodeCache.swift +++ b/SessionMessagingKit/Database/Migrations/_022_DropSnodeCache.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration drops the current `SnodePool` and `SnodeSet` and their associated jobs as they are handled by `libSession` now -enum _006_DropSnodeCache: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "DropSnodeCache" +enum _022_DropSnodeCache: Migration { + static let identifier: String = "snodeKit.DropSnodeCache" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionNetworkingKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift b/SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift similarity index 96% rename from SessionNetworkingKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift rename to SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift index 779eecee5e..e77ead364d 100644 --- a/SessionNetworkingKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift +++ b/SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift @@ -2,12 +2,12 @@ import Foundation import GRDB +import SessionNetworkingKit import SessionUtilitiesKit /// This migration splits the old `key` structure used for `SnodeReceivedMessageInfo` into separate columns for more efficient querying -enum _007_SplitSnodeReceivedMessageInfo: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "SplitSnodeReceivedMessageInfo" +enum _023_SplitSnodeReceivedMessageInfo: Migration { + static let identifier: String = "snodeKit.SplitSnodeReceivedMessageInfo" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [SnodeReceivedMessageInfo.self] diff --git a/SessionNetworkingKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift b/SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift similarity index 84% rename from SessionNetworkingKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift rename to SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift index 1eb3e6d265..2fad3edb4e 100644 --- a/SessionNetworkingKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift +++ b/SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift @@ -2,13 +2,13 @@ import Foundation import GRDB +import SessionNetworkingKit import SessionUtilitiesKit /// This migration resets the `lastHash` value for all user config namespaces to force the app to fetch the latest config /// messages in case there are multi-part config message we had previously seen and failed to merge -enum _008_ResetUserConfigLastHashes: Migration { - static let target: TargetMigrations.Identifier = .networkingKit - static let identifier: String = "ResetUserConfigLastHashes" +enum _024_ResetUserConfigLastHashes: Migration { + static let identifier: String = "snodeKit.ResetUserConfigLastHashes" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift b/SessionMessagingKit/Database/Migrations/_025_AddPendingReadReceipts.swift similarity index 89% rename from SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift rename to SessionMessagingKit/Database/Migrations/_025_AddPendingReadReceipts.swift index 5f51432095..0b0d5fec63 100644 --- a/SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift +++ b/SessionMessagingKit/Database/Migrations/_025_AddPendingReadReceipts.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration adds a table to track pending read receipts (it's possible to receive a read receipt message before getting the original /// message due to how one-to-one conversations work, by storing pending read receipts we should be able to prevent this case) -enum _011_AddPendingReadReceipts: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddPendingReadReceipts" +enum _025_AddPendingReadReceipts: Migration { + static let identifier: String = "messagingKit.AddPendingReadReceipts" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [PendingReadReceipt.self] diff --git a/SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift b/SessionMessagingKit/Database/Migrations/_026_AddFTSIfNeeded.swift similarity index 80% rename from SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift rename to SessionMessagingKit/Database/Migrations/_026_AddFTSIfNeeded.swift index a030deed3f..b655432c2c 100644 --- a/SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift +++ b/SessionMessagingKit/Database/Migrations/_026_AddFTSIfNeeded.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds the FTS table back for internal test users whose FTS table was removed unintentionally -enum _012_AddFTSIfNeeded: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddFTSIfNeeded" +enum _026_AddFTSIfNeeded: Migration { + static let identifier: String = "messagingKit.AddFTSIfNeeded" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -17,7 +16,7 @@ enum _012_AddFTSIfNeeded: Migration { if try db.tableExists("interaction_fts") == false { try db.create(virtualTable: "interaction_fts", using: FTS5()) { t in t.synchronize(withTable: "interaction") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("body") t.column("threadId") diff --git a/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift b/SessionMessagingKit/Database/Migrations/_027_SessionUtilChanges.swift similarity index 98% rename from SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift rename to SessionMessagingKit/Database/Migrations/_027_SessionUtilChanges.swift index cb67ad5bf5..57e76b93a9 100644 --- a/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_027_SessionUtilChanges.swift @@ -9,9 +9,8 @@ import SessionUtil import SessionUtilitiesKit /// This migration makes the neccessary changes to support the updated user config syncing system -enum _013_SessionUtilChanges: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "SessionUtilChanges" +enum _027_SessionUtilChanges: Migration { + static let identifier: String = "messagingKit.SessionUtilChanges" static let minExpectedRunDuration: TimeInterval = 0.4 static let createdTables: [(TableRecord & FetchableRecord).Type] = [ConfigDump.self] @@ -229,7 +228,7 @@ enum _013_SessionUtilChanges: Migration { } } -private extension _013_SessionUtilChanges { +private extension _027_SessionUtilChanges { static func generateLegacyClosedGroupKeyPairHash(threadId: String, publicKey: Data, secretKey: Data) -> String { return Data(Insecure.MD5 .hash(data: threadId.bytes + publicKey.bytes + secretKey.bytes) diff --git a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift b/SessionMessagingKit/Database/Migrations/_028_GenerateInitialUserConfigDumps.swift similarity index 98% rename from SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift rename to SessionMessagingKit/Database/Migrations/_028_GenerateInitialUserConfigDumps.swift index c81a813e12..a53031dd46 100644 --- a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift +++ b/SessionMessagingKit/Database/Migrations/_028_GenerateInitialUserConfigDumps.swift @@ -6,9 +6,8 @@ import SessionUtil import SessionUtilitiesKit /// This migration goes through the current state of the database and generates config dumps for the user config types -enum _014_GenerateInitialUserConfigDumps: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "GenerateInitialUserConfigDumps" +enum _028_GenerateInitialUserConfigDumps: Migration { + static let identifier: String = "messagingKit.GenerateInitialUserConfigDumps" static let minExpectedRunDuration: TimeInterval = 4.0 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift b/SessionMessagingKit/Database/Migrations/_029_BlockCommunityMessageRequests.swift similarity index 94% rename from SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift rename to SessionMessagingKit/Database/Migrations/_029_BlockCommunityMessageRequests.swift index dd58e13355..22cb579ef3 100644 --- a/SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift +++ b/SessionMessagingKit/Database/Migrations/_029_BlockCommunityMessageRequests.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds a flag indicating whether a profile has indicated it is blocking community message requests -enum _015_BlockCommunityMessageRequests: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "BlockCommunityMessageRequests" +enum _029_BlockCommunityMessageRequests: Migration { + static let identifier: String = "messagingKit.BlockCommunityMessageRequests" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_016_MakeBrokenProfileTimestampsNullable.swift b/SessionMessagingKit/Database/Migrations/_030_MakeBrokenProfileTimestampsNullable.swift similarity index 89% rename from SessionMessagingKit/Database/Migrations/_016_MakeBrokenProfileTimestampsNullable.swift rename to SessionMessagingKit/Database/Migrations/_030_MakeBrokenProfileTimestampsNullable.swift index 82816602ca..dbbeb35044 100644 --- a/SessionMessagingKit/Database/Migrations/_016_MakeBrokenProfileTimestampsNullable.swift +++ b/SessionMessagingKit/Database/Migrations/_030_MakeBrokenProfileTimestampsNullable.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration updates the tiemstamps added to the `Profile` in earlier migrations to be nullable (having it not null /// results in migration issues when a user jumps between multiple versions) -enum _016_MakeBrokenProfileTimestampsNullable: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "MakeBrokenProfileTimestampsNullable" +enum _030_MakeBrokenProfileTimestampsNullable: Migration { + static let identifier: String = "messagingKit.MakeBrokenProfileTimestampsNullable" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_017_RebuildFTSIfNeeded_2_4_5.swift b/SessionMessagingKit/Database/Migrations/_031_RebuildFTSIfNeeded_2_4_5.swift similarity index 85% rename from SessionMessagingKit/Database/Migrations/_017_RebuildFTSIfNeeded_2_4_5.swift rename to SessionMessagingKit/Database/Migrations/_031_RebuildFTSIfNeeded_2_4_5.swift index c9c9240fde..9660270f21 100644 --- a/SessionMessagingKit/Database/Migrations/_017_RebuildFTSIfNeeded_2_4_5.swift +++ b/SessionMessagingKit/Database/Migrations/_031_RebuildFTSIfNeeded_2_4_5.swift @@ -7,9 +7,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds the FTS table back if either the tables or any of the triggers no longer exist -enum _017_RebuildFTSIfNeeded_2_4_5: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "RebuildFTSIfNeeded_2_4_5" +enum _031_RebuildFTSIfNeeded_2_4_5: Migration { + static let identifier: String = "messagingKit.RebuildFTSIfNeeded_2_4_5" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -30,7 +29,7 @@ enum _017_RebuildFTSIfNeeded_2_4_5: Migration { try db.create(virtualTable: "interaction_fts", using: FTS5()) { t in t.synchronize(withTable: "interaction") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("body") t.column("threadId") @@ -44,7 +43,7 @@ enum _017_RebuildFTSIfNeeded_2_4_5: Migration { try db.create(virtualTable: "profile_fts", using: FTS5()) { t in t.synchronize(withTable: "profile") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("nickname") t.column("name") @@ -58,7 +57,7 @@ enum _017_RebuildFTSIfNeeded_2_4_5: Migration { try db.create(virtualTable: "closedGroup_fts", using: FTS5()) { t in t.synchronize(withTable: "closedGroup") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("name") } @@ -71,7 +70,7 @@ enum _017_RebuildFTSIfNeeded_2_4_5: Migration { try db.create(virtualTable: "openGroup_fts", using: FTS5()) { t in t.synchronize(withTable: "openGroup") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("name") } diff --git a/SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift b/SessionMessagingKit/Database/Migrations/_032_DisappearingMessagesConfiguration.swift similarity index 97% rename from SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift rename to SessionMessagingKit/Database/Migrations/_032_DisappearingMessagesConfiguration.swift index 809c426e56..4bf07b018f 100644 --- a/SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift +++ b/SessionMessagingKit/Database/Migrations/_032_DisappearingMessagesConfiguration.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _018_DisappearingMessagesConfiguration: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "DisappearingMessagesWithTypes" +enum _032_DisappearingMessagesConfiguration: Migration { + static let identifier: String = "messagingKit.DisappearingMessagesWithTypes" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_019_ScheduleAppUpdateCheckJob.swift b/SessionMessagingKit/Database/Migrations/_033_ScheduleAppUpdateCheckJob.swift similarity index 84% rename from SessionMessagingKit/Database/Migrations/_019_ScheduleAppUpdateCheckJob.swift rename to SessionMessagingKit/Database/Migrations/_033_ScheduleAppUpdateCheckJob.swift index 9f5bd4c724..f0df877278 100644 --- a/SessionMessagingKit/Database/Migrations/_019_ScheduleAppUpdateCheckJob.swift +++ b/SessionMessagingKit/Database/Migrations/_033_ScheduleAppUpdateCheckJob.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _019_ScheduleAppUpdateCheckJob: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "ScheduleAppUpdateCheckJob" +enum _033_ScheduleAppUpdateCheckJob: Migration { + static let identifier: String = "messagingKit.ScheduleAppUpdateCheckJob" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_020_AddMissingWhisperFlag.swift b/SessionMessagingKit/Database/Migrations/_034_AddMissingWhisperFlag.swift similarity index 81% rename from SessionMessagingKit/Database/Migrations/_020_AddMissingWhisperFlag.swift rename to SessionMessagingKit/Database/Migrations/_034_AddMissingWhisperFlag.swift index 90dbfc4fbd..ecd158b8b1 100644 --- a/SessionMessagingKit/Database/Migrations/_020_AddMissingWhisperFlag.swift +++ b/SessionMessagingKit/Database/Migrations/_034_AddMissingWhisperFlag.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _020_AddMissingWhisperFlag: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddMissingWhisperFlag" +enum _034_AddMissingWhisperFlag: Migration { + static let identifier: String = "messagingKit.AddMissingWhisperFlag" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_021_ReworkRecipientState.swift b/SessionMessagingKit/Database/Migrations/_035_ReworkRecipientState.swift similarity index 97% rename from SessionMessagingKit/Database/Migrations/_021_ReworkRecipientState.swift rename to SessionMessagingKit/Database/Migrations/_035_ReworkRecipientState.swift index a47d202666..f0ebe35f20 100644 --- a/SessionMessagingKit/Database/Migrations/_021_ReworkRecipientState.swift +++ b/SessionMessagingKit/Database/Migrations/_035_ReworkRecipientState.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _021_ReworkRecipientState: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "ReworkRecipientState" +enum _035_ReworkRecipientState: Migration { + static let identifier: String = "messagingKit.ReworkRecipientState" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -180,7 +179,7 @@ enum _021_ReworkRecipientState: Migration { } } -private extension _021_ReworkRecipientState { +private extension _035_ReworkRecipientState { enum LegacyState: Int { case sending case failed diff --git a/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift b/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift similarity index 97% rename from SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift rename to SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift index e6902e104c..87081585e3 100644 --- a/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift @@ -8,9 +8,8 @@ import GRDB import SessionNetworkingKit import SessionUtilitiesKit -enum _022_GroupsRebuildChanges: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "GroupsRebuildChanges" +enum _036_GroupsRebuildChanges: Migration { + static let identifier: String = "messagingKit.GroupsRebuildChanges" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] @@ -212,7 +211,7 @@ enum _022_GroupsRebuildChanges: Migration { } } -private extension _022_GroupsRebuildChanges { +private extension _036_GroupsRebuildChanges { static func generateFilename(format: ImageFormat = .jpeg, using dependencies: Dependencies) -> String { return dependencies[singleton: .crypto] .generate(.uuid()) diff --git a/SessionMessagingKit/Database/Migrations/_023_GroupsExpiredFlag.swift b/SessionMessagingKit/Database/Migrations/_037_GroupsExpiredFlag.swift similarity index 76% rename from SessionMessagingKit/Database/Migrations/_023_GroupsExpiredFlag.swift rename to SessionMessagingKit/Database/Migrations/_037_GroupsExpiredFlag.swift index 2bffc37639..294efd4846 100644 --- a/SessionMessagingKit/Database/Migrations/_023_GroupsExpiredFlag.swift +++ b/SessionMessagingKit/Database/Migrations/_037_GroupsExpiredFlag.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _023_GroupsExpiredFlag: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "GroupsExpiredFlag" +enum _037_GroupsExpiredFlag: Migration { + static let identifier: String = "messagingKit.GroupsExpiredFlag" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_024_FixBustedInteractionVariant.swift b/SessionMessagingKit/Database/Migrations/_038_FixBustedInteractionVariant.swift similarity index 83% rename from SessionMessagingKit/Database/Migrations/_024_FixBustedInteractionVariant.swift rename to SessionMessagingKit/Database/Migrations/_038_FixBustedInteractionVariant.swift index 9b65965362..5929ad6c87 100644 --- a/SessionMessagingKit/Database/Migrations/_024_FixBustedInteractionVariant.swift +++ b/SessionMessagingKit/Database/Migrations/_038_FixBustedInteractionVariant.swift @@ -7,9 +7,8 @@ import SessionUtilitiesKit /// There was a bug with internal releases of the Groups Rebuild feature where we incorrectly assigned an `Interaction.Variant` /// value of `3` to deleted message artifacts when it should have been `2`, this migration updates any interactions with a value of `2` /// to be `3` -enum _024_FixBustedInteractionVariant: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "FixBustedInteractionVariant" +enum _038_FixBustedInteractionVariant: Migration { + static let identifier: String = "messagingKit.FixBustedInteractionVariant" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_025_DropLegacyClosedGroupKeyPairTable.swift b/SessionMessagingKit/Database/Migrations/_039_DropLegacyClosedGroupKeyPairTable.swift similarity index 74% rename from SessionMessagingKit/Database/Migrations/_025_DropLegacyClosedGroupKeyPairTable.swift rename to SessionMessagingKit/Database/Migrations/_039_DropLegacyClosedGroupKeyPairTable.swift index afc0dd376d..20111dade4 100644 --- a/SessionMessagingKit/Database/Migrations/_025_DropLegacyClosedGroupKeyPairTable.swift +++ b/SessionMessagingKit/Database/Migrations/_039_DropLegacyClosedGroupKeyPairTable.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// Legacy closed groups are no longer supported so we can drop the `closedGroupKeyPair` table from /// the database -enum _025_DropLegacyClosedGroupKeyPairTable: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "DropLegacyClosedGroupKeyPairTable" +enum _039_DropLegacyClosedGroupKeyPairTable: Migration { + static let identifier: String = "messagingKit.DropLegacyClosedGroupKeyPairTable" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift b/SessionMessagingKit/Database/Migrations/_040_MessageDeduplicationTable.swift similarity index 98% rename from SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift rename to SessionMessagingKit/Database/Migrations/_040_MessageDeduplicationTable.swift index 5addd66869..5431858f19 100644 --- a/SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift +++ b/SessionMessagingKit/Database/Migrations/_040_MessageDeduplicationTable.swift @@ -8,9 +8,8 @@ import SessionNetworkingKit /// The different platforms use different approaches for message deduplication but in the future we want to shift the database logic into /// `libSession` so it makes sense to try to define a longer-term deduplication approach we we can use in `libSession`, additonally /// the PN extension will need to replicate this deduplication data so having a single source-of-truth for the data will make things easier -enum _026_MessageDeduplicationTable: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "MessageDeduplicationTable" +enum _040_MessageDeduplicationTable: Migration { + static let identifier: String = "messagingKit.MessageDeduplicationTable" static let minExpectedRunDuration: TimeInterval = 5 static var createdTables: [(FetchableRecord & TableRecord).Type] = [ MessageDeduplication.self @@ -343,7 +342,7 @@ enum _026_MessageDeduplicationTable: Migration { } } -internal extension _026_MessageDeduplicationTable { +internal extension _040_MessageDeduplicationTable { static func legacyDedupeIdentifier( variant: Interaction.Variant, timestampMs: Int64 @@ -372,7 +371,7 @@ internal extension _026_MessageDeduplicationTable { } } -internal extension _026_MessageDeduplicationTable { +internal extension _040_MessageDeduplicationTable { enum ControlMessageProcessRecordVariant: Int { case readReceipt = 1 case typingIndicator = 2 diff --git a/SessionUtilitiesKit/Database/Migrations/_006_RenameTableSettingToKeyValueStore.swift b/SessionMessagingKit/Database/Migrations/_041_RenameTableSettingToKeyValueStore.swift similarity index 68% rename from SessionUtilitiesKit/Database/Migrations/_006_RenameTableSettingToKeyValueStore.swift rename to SessionMessagingKit/Database/Migrations/_041_RenameTableSettingToKeyValueStore.swift index ffad28e128..48a366aecf 100644 --- a/SessionUtilitiesKit/Database/Migrations/_006_RenameTableSettingToKeyValueStore.swift +++ b/SessionMessagingKit/Database/Migrations/_041_RenameTableSettingToKeyValueStore.swift @@ -2,10 +2,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _006_RenameTableSettingToKeyValueStore: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "RenameTableSettingToKeyValueStore" // stringlint:disable +enum _041_RenameTableSettingToKeyValueStore: Migration { + static let identifier: String = "utilitiesKit.RenameTableSettingToKeyValueStore" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [ KeyValueStore.self ] diff --git a/SessionMessagingKit/Database/Migrations/_027_MoveSettingsToLibSession.swift b/SessionMessagingKit/Database/Migrations/_042_MoveSettingsToLibSession.swift similarity index 97% rename from SessionMessagingKit/Database/Migrations/_027_MoveSettingsToLibSession.swift rename to SessionMessagingKit/Database/Migrations/_042_MoveSettingsToLibSession.swift index 3f6353e72c..5e63bd1bff 100644 --- a/SessionMessagingKit/Database/Migrations/_027_MoveSettingsToLibSession.swift +++ b/SessionMessagingKit/Database/Migrations/_042_MoveSettingsToLibSession.swift @@ -6,9 +6,8 @@ import SessionUIKit import SessionUtilitiesKit /// This migration extracts an old settings from the database and saves them into libSession -enum _027_MoveSettingsToLibSession: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "MoveSettingsToLibSession" +enum _042_MoveSettingsToLibSession: Migration { + static let identifier: String = "messagingKit.MoveSettingsToLibSession" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift b/SessionMessagingKit/Database/Migrations/_043_RenameAttachments.swift similarity index 99% rename from SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift rename to SessionMessagingKit/Database/Migrations/_043_RenameAttachments.swift index a4e8969695..ff94b962c2 100644 --- a/SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift +++ b/SessionMessagingKit/Database/Migrations/_043_RenameAttachments.swift @@ -8,9 +8,8 @@ import SessionUtilitiesKit /// This migration renames all attachments to use a hash of the download url for the filename instead of a random UUID (means we can /// generate the filename just from the URL and don't need to store the filename) -enum _028_RenameAttachments: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "RenameAttachments" +enum _043_RenameAttachments: Migration { + static let identifier: String = "messagingKit.RenameAttachments" static let minExpectedRunDuration: TimeInterval = 3 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_029_AddProMessageFlag.swift b/SessionMessagingKit/Database/Migrations/_044_AddProMessageFlag.swift similarity index 76% rename from SessionMessagingKit/Database/Migrations/_029_AddProMessageFlag.swift rename to SessionMessagingKit/Database/Migrations/_044_AddProMessageFlag.swift index 0d2751199e..7f51d1ffc2 100644 --- a/SessionMessagingKit/Database/Migrations/_029_AddProMessageFlag.swift +++ b/SessionMessagingKit/Database/Migrations/_044_AddProMessageFlag.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _029_AddProMessageFlag: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddProMessageFlag" +enum _044_AddProMessageFlag: Migration { + static let identifier: String = "messagingKit.AddProMessageFlag" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Models/MessageDeduplication.swift b/SessionMessagingKit/Database/Models/MessageDeduplication.swift index 6a73d52936..e8b5a8f531 100644 --- a/SessionMessagingKit/Database/Models/MessageDeduplication.swift +++ b/SessionMessagingKit/Database/Models/MessageDeduplication.swift @@ -209,7 +209,7 @@ public extension MessageDeduplication { _ processedMessage: ProcessedMessage, using dependencies: Dependencies ) throws { - typealias Variant = _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant + typealias Variant = _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant try ensureMessageIsNotADuplicate( threadId: processedMessage.threadId, uniqueIdentifier: processedMessage.uniqueIdentifier, @@ -402,12 +402,12 @@ private extension MessageDeduplication { _ db: ObservingDatabase, threadId: String, legacyIdentifier: String?, - legacyVariant: _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant?, + legacyVariant: _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant?, timestampMs: Int64?, serverExpirationTimestamp: TimeInterval?, using dependencies: Dependencies ) throws { - typealias Variant = _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant + typealias Variant = _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant guard let legacyIdentifier: String = legacyIdentifier, let legacyVariant: Variant = legacyVariant, @@ -463,7 +463,7 @@ private extension MessageDeduplication { } @available(*, deprecated, message: "⚠️ Remove this code once once enough time has passed since it's release (at least 1 month)") - static func getLegacyVariant(for variant: Message.Variant?) -> _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant? { + static func getLegacyVariant(for variant: Message.Variant?) -> _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant? { guard let variant: Message.Variant = variant else { return nil } switch variant { @@ -494,7 +494,7 @@ private extension MessageDeduplication { case .standard(_, _, _, let messageInfo, _): guard let timestampMs: UInt64 = messageInfo.message.sentTimestampMs, - let variant: _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant = getLegacyVariant(for: Message.Variant(from: messageInfo.message)) + let variant: _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant = getLegacyVariant(for: Message.Variant(from: messageInfo.message)) else { return nil } return "LegacyRecord-\(variant.rawValue)-\(timestampMs)" // stringlint:ignore diff --git a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift index bd36080603..22fb802b43 100644 --- a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift +++ b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift @@ -18,9 +18,7 @@ class MessageDeduplicationSpec: AsyncSpec { @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies ) @TestState(singleton: .extensionHelper, in: dependencies) var mockExtensionHelper: MockExtensionHelper! = MockExtensionHelper( diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index 85d0809b20..130facfd6c 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -27,10 +27,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift b/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift index 15012b5318..e549e2c221 100644 --- a/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift @@ -35,10 +35,7 @@ class MessageSendJobSpec: QuickSpec { ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try SessionThread.upsert( diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index a226aab1df..79692802c7 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -20,10 +20,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift index 07bda1e05f..309aa452d8 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift @@ -28,11 +28,7 @@ class LibSessionGroupInfoSpec: QuickSpec { ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self, - SNNetworkingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift index 8001ede737..43f6c045de 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift @@ -27,10 +27,7 @@ class LibSessionGroupMembersSpec: QuickSpec { ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift index f23341efdf..cb8ff17dd8 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift @@ -27,10 +27,7 @@ class LibSessionSpec: QuickSpec { ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 24635b39af..da31a7c154 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -110,10 +110,7 @@ class OpenGroupManagerSpec: QuickSpec { }() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index be86b9ed76..f731e18ecf 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -29,11 +29,7 @@ class MessageReceiverGroupsSpec: QuickSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift index 2f55e63806..69d1a6d505 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift @@ -29,10 +29,7 @@ class MessageSenderGroupsSpec: QuickSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift index 6ddb79b9bf..ee73e9fbc0 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift @@ -17,10 +17,7 @@ class MessageSenderSpec: QuickSpec { @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) diff --git a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift index 6f966ebf9c..79f98b853e 100644 --- a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift @@ -21,10 +21,7 @@ class CommunityPollerSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift index 5cd9216cab..13dd801e31 100644 --- a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift +++ b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift @@ -24,10 +24,7 @@ class ExtensionHelperSpec: AsyncSpec { @TestState(singleton: .extensionHelper, in: dependencies) var extensionHelper: ExtensionHelper! = ExtensionHelper(using: dependencies) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies ) @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( diff --git a/SessionNetworkingKit/Configuration.swift b/SessionNetworkingKit/Configuration.swift deleted file mode 100644 index 18a73d2615..0000000000 --- a/SessionNetworkingKit/Configuration.swift +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import GRDB -import SessionUtilitiesKit - -public enum SNNetworkingKit: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .networkingKit, - migrations: [ - [ - _001_InitialSetupMigration.self, - _002_SetupStandardJobs.self - ], // Initial DB Creation - [ - _003_YDBToGRDBMigration.self - ], // YDB to GRDB Migration - [ - _004_FlagMessageHashAsDeletedOrInvalid.self - ], // Legacy DB removal - [], // Add job priorities - [], // Fix thread FTS - [ - _005_AddSnodeReveivedMessageInfoPrimaryKey.self, - _006_DropSnodeCache.self, - _007_SplitSnodeReceivedMessageInfo.self, - _008_ResetUserConfigLastHashes.self - ], - [], // Renamed `Setting` to `KeyValueStore` - [] - ] - ) - } -} diff --git a/SessionShareExtension/ShareNavController.swift b/SessionShareExtension/ShareNavController.swift index 6a39606df5..b1f420316f 100644 --- a/SessionShareExtension/ShareNavController.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -45,7 +45,6 @@ final class ShareNavController: UINavigationController { dependencies.warmCache(cache: .appVersion) AppSetup.setupEnvironment( - additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], appSpecificBlock: { [dependencies] in // stringlint:ignore_start if !Log.loggerExists(withPrefix: "SessionShareExtension") { diff --git a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift index 7028b12544..beaaa7ed26 100644 --- a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift @@ -21,12 +21,7 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try SessionThread( diff --git a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift index 227c06bf86..8d2532b0b0 100644 --- a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift @@ -21,12 +21,7 @@ class ThreadNotificationSettingsViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try SessionThread( diff --git a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift index ec9d79c0a2..77514dedf8 100644 --- a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift @@ -29,12 +29,7 @@ class ThreadSettingsViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity( diff --git a/SessionTests/Database/DatabaseSpec.swift b/SessionTests/Database/DatabaseSpec.swift index 5c63cd7fae..c9b824b196 100644 --- a/SessionTests/Database/DatabaseSpec.swift +++ b/SessionTests/Database/DatabaseSpec.swift @@ -38,14 +38,7 @@ class DatabaseSpec: QuickSpec { @TestState var initialResult: Result! = nil @TestState var finalResult: Result! = nil - let allMigrations: [Storage.KeyedMigration] = SynchronousStorage.sortedMigrationInfo( - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ] - ) + let allMigrations: [Migration.Type] = SNMessagingKit.migrations let dynamicTests: [MigrationTest] = MigrationTest.extractTests(allMigrations) let allTableTypes: [(TableRecord & FetchableRecord).Type] = MigrationTest.extractDatabaseTypes(allMigrations) MigrationTest.explicitValues = [ @@ -75,12 +68,7 @@ class DatabaseSpec: QuickSpec { // MARK: -- can be created from an empty state it("can be created from an empty state") { mockStorage.perform( - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: allMigrations, async: false, onProgressUpdate: nil, onComplete: { result in initialResult = result } @@ -92,7 +80,7 @@ class DatabaseSpec: QuickSpec { // MARK: -- can still parse the database table types it("can still parse the database table types") { mockStorage.perform( - sortedMigrations: allMigrations, + migrations: allMigrations, async: false, onProgressUpdate: nil, onComplete: { result in initialResult = result } @@ -115,7 +103,7 @@ class DatabaseSpec: QuickSpec { // MARK: -- can still parse the database types setting null where possible it("can still parse the database types setting null where possible") { mockStorage.perform( - sortedMigrations: allMigrations, + migrations: allMigrations, async: false, onProgressUpdate: nil, onComplete: { result in initialResult = result } @@ -137,9 +125,9 @@ class DatabaseSpec: QuickSpec { // MARK: -- can migrate from X to Y dynamicTests.forEach { test in - it("can migrate from \(test.initialMigrationKey) to \(test.finalMigrationKey)") { + it("can migrate from \(test.initialMigrationIdentifier) to \(test.finalMigrationIdentifier)") { let initialStateResult: Result = { - if let cachedResult: Result = snapshotCache[test.initialMigrationKey] { + if let cachedResult: Result = snapshotCache[test.initialMigrationIdentifier] { return cachedResult } @@ -153,7 +141,7 @@ class DatabaseSpec: QuickSpec { // Generate dummy data (otherwise structural issues or invalid foreign keys won't error) var initialResult: Result! storage.perform( - sortedMigrations: test.initialMigrations, + migrations: test.initialMigrations, async: false, onProgressUpdate: nil, onComplete: { result in initialResult = result } @@ -163,10 +151,10 @@ class DatabaseSpec: QuickSpec { // Generate dummy data (otherwise structural issues or invalid foreign keys won't error) try MigrationTest.generateDummyData(storage, nullsWherePossible: false) - snapshotCache[test.initialMigrationKey] = .success(dbQueue) + snapshotCache[test.initialMigrationIdentifier] = .success(dbQueue) return .success(dbQueue) } catch { - snapshotCache[test.initialMigrationKey] = .failure(error) + snapshotCache[test.initialMigrationIdentifier] = .failure(error) return .failure(error) } }() @@ -175,7 +163,7 @@ class DatabaseSpec: QuickSpec { switch initialStateResult { case .success(let db): sourceDb = db case .failure(let error): - fail("Failed to prepare the initial state for '\(test.initialMigrationKey)'. Error: \(error)") + fail("Failed to prepare the initial state for '\(test.initialMigrationIdentifier)'. Error: \(error)") return } @@ -186,7 +174,7 @@ class DatabaseSpec: QuickSpec { // Peform the target migrations to ensure the migrations themselves worked correctly mockStorage.perform( - sortedMigrations: test.migrationsToTest, + migrations: test.migrationsToTest, async: false, onProgressUpdate: nil, onComplete: { result in finalResult = result } @@ -195,12 +183,68 @@ class DatabaseSpec: QuickSpec { switch finalResult { case .success: break case .failure(let error): - fail("Failed to migrate from '\(test.initialMigrationKey)' to '\(test.finalMigrationKey)'. Error: \(error)") + fail("Failed to migrate from '\(test.initialMigrationIdentifier)' to '\(test.finalMigrationIdentifier)'. Error: \(error)") case .none: - fail("Failed to migrate from '\(test.initialMigrationKey)' to '\(test.finalMigrationKey)'. Error: No result") + fail("Failed to migrate from '\(test.initialMigrationIdentifier)' to '\(test.finalMigrationIdentifier)'. Error: No result") } } } + + // MARK: -- migration order hasn't changed + it("migration order hasn't changed") { + expect(SNMessagingKit.migrations.map { $0.identifier }).to(equal([ + "utilitiesKit.initialSetup", + "utilitiesKit.SetupStandardJobs", + "utilitiesKit.YDBToGRDBMigration", + "snodeKit.initialSetup", + "snodeKit.SetupStandardJobs", + "messagingKit.initialSetup", + "messagingKit.SetupStandardJobs", + "snodeKit.YDBToGRDBMigration", + "messagingKit.YDBToGRDBMigration", + "snodeKit.FlagMessageHashAsDeletedOrInvalid", + "messagingKit.RemoveLegacyYDB", + "utilitiesKit.AddJobPriority", + "messagingKit.FixDeletedMessageReadState", + "messagingKit.FixHiddenModAdminSupport", + "messagingKit.HomeQueryOptimisationIndexes", + "uiKit.ThemePreferences", + "messagingKit.EmojiReacts", + "messagingKit.OpenGroupPermission", + "messagingKit.AddThreadIdToFTS", + "utilitiesKit.AddJobUniqueHash", + "snodeKit.AddSnodeReveivedMessageInfoPrimaryKey", + "snodeKit.DropSnodeCache", + "snodeKit.SplitSnodeReceivedMessageInfo", + "snodeKit.ResetUserConfigLastHashes", + "messagingKit.AddPendingReadReceipts", + "messagingKit.AddFTSIfNeeded", + "messagingKit.SessionUtilChanges", + "messagingKit.GenerateInitialUserConfigDumps", + "messagingKit.BlockCommunityMessageRequests", + "messagingKit.MakeBrokenProfileTimestampsNullable", + "messagingKit.RebuildFTSIfNeeded_2_4_5", + "messagingKit.DisappearingMessagesWithTypes", + "messagingKit.ScheduleAppUpdateCheckJob", + "messagingKit.AddMissingWhisperFlag", + "messagingKit.ReworkRecipientState", + "messagingKit.GroupsRebuildChanges", + "messagingKit.GroupsExpiredFlag", + "messagingKit.FixBustedInteractionVariant", + "messagingKit.DropLegacyClosedGroupKeyPairTable", + "messagingKit.MessageDeduplicationTable", + "utilitiesKit.RenameTableSettingToKeyValueStore", + "messagingKit.MoveSettingsToLibSession", + "messagingKit.RenameAttachments", + "messagingKit.AddProMessageFlag" + ])) + } + + // MARK: -- there are no duplicate migration names + it("there are no duplicate migration names") { + expect(Set(SNMessagingKit.migrations.map { $0.identifier }).sorted()) + .to(equal(SNMessagingKit.migrations.map { $0.identifier }.sorted())) + } } } } @@ -236,15 +280,15 @@ private struct TableColumn: Hashable { private class MigrationTest { static var explicitValues: [TableColumn: (any DatabaseValueConvertible)] = [:] - let initialMigrations: [Storage.KeyedMigration] - let migrationsToTest: [Storage.KeyedMigration] + let initialMigrations: [Migration.Type] + let migrationsToTest: [Migration.Type] - var initialMigrationKey: String { return (initialMigrations.last?.key ?? "an empty database") } - var finalMigrationKey: String { return (migrationsToTest.last?.key ?? "invalid") } + var initialMigrationIdentifier: String { return (initialMigrations.last?.identifier ?? "an empty database") } + var finalMigrationIdentifier: String { return (migrationsToTest.last?.identifier ?? "invalid") } private init( - initialMigrations: [Storage.KeyedMigration], - migrationsToTest: [Storage.KeyedMigration] + initialMigrations: [Migration.Type], + migrationsToTest: [Migration.Type] ) { self.initialMigrations = initialMigrations self.migrationsToTest = migrationsToTest @@ -252,7 +296,7 @@ private class MigrationTest { // MARK: - Test Data - static func extractTests(_ allMigrations: [Storage.KeyedMigration]) -> [MigrationTest] { + static func extractTests(_ allMigrations: [Migration.Type]) -> [MigrationTest] { return (0..<(allMigrations.count - 1)) .flatMap { index -> [MigrationTest] in ((index + 1).. MigrationTest in @@ -264,10 +308,10 @@ private class MigrationTest { } } - static func extractDatabaseTypes(_ allMigrations: [Storage.KeyedMigration]) -> [(TableRecord & FetchableRecord).Type] { + static func extractDatabaseTypes(_ allMigrations: [Migration.Type]) -> [(TableRecord & FetchableRecord).Type] { return Array(allMigrations .reduce(into: [:]) { result, next in - next.migration.createdTables.forEach { table in + next.createdTables.forEach { table in result[ObjectIdentifier(table).hashValue] = table } } @@ -392,69 +436,3 @@ private class MigrationTest { } } } - -enum TestAllMigrationRequirementsReversedMigratableTarget: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .session, - migrations: [ - [ - TestRequiresAllMigrationRequirementsReversedMigration.self - ] - ] - ) - } -} - -enum TestRequiresLibSessionStateMigratableTarget: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .session, - migrations: [ - [ - TestRequiresLibSessionStateMigration.self - ] - ] - ) - } -} - -enum TestRequiresSessionIdCachedMigratableTarget: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .session, - migrations: [ - [ - TestRequiresSessionIdCachedMigration.self - ] - ] - ) - } -} - -enum TestRequiresAllMigrationRequirementsReversedMigration: Migration { - static let target: TargetMigrations.Identifier = .session - static let identifier: String = "test" // stringlint:ignore - static let minExpectedRunDuration: TimeInterval = 0.1 - static let createdTables: [(TableRecord & FetchableRecord).Type] = [] - - static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws {} -} - -enum TestRequiresLibSessionStateMigration: Migration { - static let target: TargetMigrations.Identifier = .session - static let identifier: String = "test" // stringlint:ignore - static let minExpectedRunDuration: TimeInterval = 0.1 - static let createdTables: [(TableRecord & FetchableRecord).Type] = [] - - static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws {} -} - -enum TestRequiresSessionIdCachedMigration: Migration { - static let target: TargetMigrations.Identifier = .session - static let identifier: String = "test" // stringlint:ignore - static let minExpectedRunDuration: TimeInterval = 0.1 - static let createdTables: [(TableRecord & FetchableRecord).Type] = [] - - static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws {} -} diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index e351b5a8b5..f39b6b65a3 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -24,12 +24,7 @@ class OnboardingSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: SNMessagingKit.migrations, using: dependencies ) @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( diff --git a/SessionTests/Settings/NotificationContentViewModelSpec.swift b/SessionTests/Settings/NotificationContentViewModelSpec.swift index 65c099d860..2a55c6ffae 100644 --- a/SessionTests/Settings/NotificationContentViewModelSpec.swift +++ b/SessionTests/Settings/NotificationContentViewModelSpec.swift @@ -21,12 +21,7 @@ class NotificationContentViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: SNMessagingKit.migrations, using: dependencies ) @TestState var secretKey: [UInt8]! = Array(Data(hex: TestConstants.edSecretKey)) diff --git a/SessionUtilitiesKit/Configuration.swift b/SessionUtilitiesKit/Configuration.swift index 16485d4f3f..d782eaba81 100644 --- a/SessionUtilitiesKit/Configuration.swift +++ b/SessionUtilitiesKit/Configuration.swift @@ -4,42 +4,14 @@ import Foundation import UIKit.UIFont import GRDB -public enum SNUtilitiesKit: MigratableTarget { // Just to make the external API nice +public enum SNUtilitiesKit { public private(set) static var maxFileSize: UInt = 0 public private(set) static var maxValidImageDimension: Int = 0 + public static var isRunningTests: Bool { ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil // stringlint:ignore } - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .utilitiesKit, - migrations: [ - [ - // Intentionally including the '_003_YDBToGRDBMigration' in the first migration - // set to ensure the 'Identity' data is migrated before any other migrations are - // run (some need access to the users publicKey) - _001_InitialSetupMigration.self, - _002_SetupStandardJobs.self, - _003_YDBToGRDBMigration.self - ], // Initial DB Creation - [], // YDB to GRDB Migration - [], // Legacy DB removal - [ - _004_AddJobPriority.self - ], // Add job priorities - [], // Fix thread FTS - [ - _005_AddJobUniqueHash.self - ], - [ - _006_RenameTableSettingToKeyValueStore.self - ], // Renamed `Setting` to `KeyValueStore` - [] - ] - ) - } - public static func configure( networkMaxFileSize: UInt, maxValidImageDimention: Int, diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index b162e27c99..e9b4cfbc4f 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -246,8 +246,6 @@ open class Storage { // MARK: - Migrations - public typealias KeyedMigration = (key: String, identifier: TargetMigrations.Identifier, migration: Migration.Type) - public static func appliedMigrationIdentifiers(_ db: ObservingDatabase) -> Set { let migrator: DatabaseMigrator = DatabaseMigrator() @@ -255,47 +253,11 @@ open class Storage { .defaulting(to: []) } - public static func sortedMigrationInfo(migrationTargets: [MigratableTarget.Type]) -> [KeyedMigration] { - typealias MigrationInfo = (identifier: TargetMigrations.Identifier, migrations: TargetMigrations.MigrationSet) - - return migrationTargets - .map { target -> TargetMigrations in target.migrations() } - .sorted() - .reduce(into: [[MigrationInfo]]()) { result, next in - next.migrations.enumerated().forEach { index, migrationSet in - if result.count <= index { - result.append([]) - } - - result[index] = (result[index] + [(next.identifier, migrationSet)]) - } - } - .reduce(into: []) { result, next in - next.forEach { identifier, migrations in - result.append(contentsOf: migrations.map { (identifier.key(with: $0), identifier, $0) }) - } - } - } - public func perform( - migrationTargets: [MigratableTarget.Type], + migrations: [Migration.Type], async: Bool = true, onProgressUpdate: ((CGFloat, TimeInterval) -> ())?, onComplete: @escaping (Result) -> () - ) { - perform( - sortedMigrations: Storage.sortedMigrationInfo(migrationTargets: migrationTargets), - async: async, - onProgressUpdate: onProgressUpdate, - onComplete: onComplete - ) - } - - internal func perform( - sortedMigrations: [KeyedMigration], - async: Bool, - onProgressUpdate: ((CGFloat, TimeInterval) -> ())?, - onComplete: @escaping (Result) -> () ) { guard isValid, let dbWriter: DatabaseWriter = dbWriter else { let error: Error = (startupError ?? StorageError.startupFailed) @@ -306,36 +268,33 @@ open class Storage { // Setup and run any required migrations var migrator: DatabaseMigrator = DatabaseMigrator() - sortedMigrations.forEach { _, identifier, migration in - migrator.registerMigration( - self, - targetIdentifier: identifier, - migration: migration, - using: dependencies - ) + migrations.forEach { migration in + migrator.registerMigration(migration.identifier) { [dependencies] db in + let migration = migration.loggedMigrate(using: dependencies) + try migration(ObservingDatabase.create(db, using: dependencies)) + } } // Determine which migrations need to be performed and gather the relevant settings needed to // inform the app of progress/states let completedMigrations: [String] = (try? dbWriter.read { db in try migrator.completedMigrations(db) }) .defaulting(to: []) - let unperformedMigrations: [KeyedMigration] = sortedMigrations + let unperformedMigrations: [Migration.Type] = migrations .reduce(into: []) { result, next in - guard !completedMigrations.contains(next.key) else { return } + guard !completedMigrations.contains(next.identifier) else { return } result.append(next) } let migrationToDurationMap: [String: TimeInterval] = unperformedMigrations .reduce(into: [:]) { result, next in - result[next.key] = next.migration.minExpectedRunDuration + result[next.identifier] = next.minExpectedRunDuration } - let unperformedMigrationDurations: [TimeInterval] = unperformedMigrations - .map { _, _, migration in migration.minExpectedRunDuration } + let unperformedMigrationDurations: [TimeInterval] = unperformedMigrations.map { $0.minExpectedRunDuration } let totalMinExpectedDuration: TimeInterval = migrationToDurationMap.values.reduce(0, +) // Store the logic to handle migration progress and completion let progressUpdater: (String, CGFloat) -> Void = { (targetKey: String, progress: CGFloat) in - guard let migrationIndex: Int = unperformedMigrations.firstIndex(where: { key, _, _ in key == targetKey }) else { + guard let migrationIndex: Int = unperformedMigrations.firstIndex(where: { $0.identifier == targetKey }) else { return } @@ -352,8 +311,8 @@ open class Storage { let migrationCompleted: (Result) -> () = { [weak self, migrator, dbWriter, dependencies] result in // Make sure to transition the progress updater to 100% for the final migration (just // in case the migration itself didn't update to 100% itself) - if let lastMigrationKey: String = unperformedMigrations.last?.key { - MigrationExecution.current?.progressUpdater(lastMigrationKey, 1) + if let lastMigrationIdentifier: String = unperformedMigrations.last?.identifier { + MigrationExecution.current?.progressUpdater(lastMigrationIdentifier, 1) } self?.hasCompletedMigrations = true @@ -401,8 +360,8 @@ open class Storage { let migrationContext: MigrationExecution.Context = MigrationExecution.Context(progressUpdater: progressUpdater) // If we have an unperformed migration then trigger the progress updater immediately - if let firstMigrationKey: String = unperformedMigrations.first?.key { - migrationContext.progressUpdater(firstMigrationKey, 0) + if let firstMigrationIdentifier: String = unperformedMigrations.first?.identifier { + migrationContext.progressUpdater(firstMigrationIdentifier, 0) } MigrationExecution.$current.withValue(migrationContext) { diff --git a/SessionUtilitiesKit/Database/Types/Migration.swift b/SessionUtilitiesKit/Database/Types/Migration.swift index 36a9026a8f..4609edfbdd 100644 --- a/SessionUtilitiesKit/Database/Types/Migration.swift +++ b/SessionUtilitiesKit/Database/Types/Migration.swift @@ -12,7 +12,6 @@ public extension Log.Category { // MARK: - Migration public protocol Migration { - static var target: TargetMigrations.Identifier { get } static var identifier: String { get } static var minExpectedRunDuration: TimeInterval { get } static var createdTables: [(TableRecord & FetchableRecord).Type] { get } @@ -21,17 +20,12 @@ public protocol Migration { } public extension Migration { - static func loggedMigrate( - _ storage: Storage?, - targetIdentifier: TargetMigrations.Identifier, - using dependencies: Dependencies - ) -> ((_ db: ObservingDatabase) throws -> ()) { + static func loggedMigrate(using dependencies: Dependencies) -> ((_ db: ObservingDatabase) throws -> ()) { return { (db: ObservingDatabase) in - Log.info(.migration, "Starting \(targetIdentifier.key(with: self))") + Log.info(.migration, "Starting \(identifier)") /// Store the `currentlyRunningMigration` in case it's useful MigrationExecution.current?.currentlyRunningMigration = MigrationExecution.CurrentlyRunningMigration( - identifier: targetIdentifier, migration: self ) defer { MigrationExecution.current?.currentlyRunningMigration = nil } @@ -44,7 +38,7 @@ public extension Migration { MigrationExecution.current?.observedEvents.append(contentsOf: db.events) MigrationExecution.current?.postCommitActions.merge(db.postCommitActions) { old, _ in old } - Log.info(.migration, "Completed \(targetIdentifier.key(with: self))") + Log.info(.migration, "Completed \(identifier)") } } } @@ -53,10 +47,9 @@ public extension Migration { public enum MigrationExecution { public struct CurrentlyRunningMigration: ThreadSafeType { - public let identifier: TargetMigrations.Identifier public let migration: Migration.Type - public var key: String { identifier.key(with: migration) } + public var key: String { migration.identifier } } public final class Context { @@ -83,6 +76,7 @@ public enum MigrationExecution { @TaskLocal public static var current: Context? + // stringlint:ignore_contents public static func updateProgress(_ progress: CGFloat) { // In test builds ignore any migration progress updates (we run in a custom database writer anyway) guard !SNUtilitiesKit.isRunningTests else { return } diff --git a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift b/SessionUtilitiesKit/Database/Types/TargetMigrations.swift deleted file mode 100644 index 860647c618..0000000000 --- a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import GRDB - -public protocol MigratableTarget { - static func migrations() -> TargetMigrations -} - -public struct TargetMigrations: Comparable { - /// This identifier is used to determine the order each set of migrations should run in. - /// - /// All migrations within a specific set will run first, followed by all migrations for the same set index in - /// the next `Identifier` before moving on to the next `MigrationSet`. So given the migrations: - /// - /// `{a: [1], [2, 3]}, {b: [4, 5], [6]}` - /// - /// the migrations will run in the following order: - /// - /// `a1, b4, b5, a2, a3, b6` - public enum Identifier: String, CaseIterable, Comparable { - // WARNING: The string version of these cases are used as migration identifiers so - // changing them will result in the migrations running again - case session - case utilitiesKit - case networkingKit = "snodeKit" - case messagingKit - case _deprecatedUIKit = "uiKit" - case test - - public static func < (lhs: Self, rhs: Self) -> Bool { - let lhsIndex: Int = (Identifier.allCases.firstIndex(of: lhs) ?? Identifier.allCases.count) - let rhsIndex: Int = (Identifier.allCases.firstIndex(of: rhs) ?? Identifier.allCases.count) - - return (lhsIndex < rhsIndex) - } - - public func key(with migration: Migration.Type) -> String { - return "\(self.rawValue).\(migration.identifier)" - } - } - - public typealias MigrationSet = [Migration.Type] - - let identifier: Identifier - let migrations: [MigrationSet] - - // MARK: - Initialization - - public init( - identifier: Identifier, - migrations: [MigrationSet] - ) { - guard !migrations.contains(where: { migration in migration.contains(where: { $0.target != identifier }) }) else { - preconditionFailure("Attempted to register a migration with the wrong target") - } - - self.identifier = identifier - self.migrations = migrations - } - - // MARK: - Equatable - - public static func == (lhs: TargetMigrations, rhs: TargetMigrations) -> Bool { - return ( - lhs.identifier == rhs.identifier && - lhs.migrations.count == rhs.migrations.count - ) - } - - // MARK: - Comparable - - public static func < (lhs: Self, rhs: Self) -> Bool { - return (lhs.identifier < rhs.identifier) - } -} diff --git a/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift deleted file mode 100644 index 748175ca8d..0000000000 --- a/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import GRDB - -public extension DatabaseMigrator { - mutating func registerMigration( - _ storage: Storage?, - targetIdentifier: TargetMigrations.Identifier, - migration: Migration.Type, - foreignKeyChecks: ForeignKeyChecks = .deferred, - using dependencies: Dependencies - ) { - self.registerMigration( - targetIdentifier.key(with: migration), - migrate: { db in - let migration = migration.loggedMigrate(storage, targetIdentifier: targetIdentifier, using: dependencies) - try migration(ObservingDatabase.create(db, using: dependencies)) - } - ) - } -} diff --git a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift index a6c60c58b8..5101300fc7 100644 --- a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift +++ b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift @@ -15,9 +15,7 @@ class IdentitySpec: QuickSpec { @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self - ], + migrations: [_001_SUK_InitialSetupMigration.self], using: dependencies ) diff --git a/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift b/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift index 82b6fe25e2..bfd9d50009 100644 --- a/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift +++ b/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift @@ -47,14 +47,12 @@ class JobRunnerSpec: QuickSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self + migrations: [ + _001_SUK_InitialSetupMigration.self, + _012_AddJobPriority.self, + _020_AddJobUniqueHash.self ], - using: dependencies, - initialData: { db in - // Migrations add jobs which we don't want so delete them - try Job.deleteAll(db) - } + using: dependencies ) @TestState(singleton: .jobRunner, in: dependencies) var jobRunner: JobRunnerType! = JobRunner( isTestingJobRunner: true, diff --git a/SignalUtilitiesKit/Utilities/AppSetup.swift b/SignalUtilitiesKit/Utilities/AppSetup.swift index f6e76580bf..1a3f2f7558 100644 --- a/SignalUtilitiesKit/Utilities/AppSetup.swift +++ b/SignalUtilitiesKit/Utilities/AppSetup.swift @@ -10,7 +10,6 @@ import SessionUtilitiesKit public enum AppSetup { public static func setupEnvironment( requestId: String? = nil, - additionalMigrationTargets: [MigratableTarget.Type] = [], appSpecificBlock: (() -> ())? = nil, migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, migrationsCompletion: @escaping (Result) -> (), @@ -43,7 +42,6 @@ public enum AppSetup { runPostSetupMigrations( requestId: requestId, backgroundTask: backgroundTask, - additionalMigrationTargets: additionalMigrationTargets, migrationProgressChanged: migrationProgressChanged, migrationsCompletion: migrationsCompletion, using: dependencies @@ -57,7 +55,6 @@ public enum AppSetup { public static func runPostSetupMigrations( requestId: String? = nil, backgroundTask: SessionBackgroundTask? = nil, - additionalMigrationTargets: [MigratableTarget.Type] = [], migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, migrationsCompletion: @escaping (Result) -> (), using dependencies: Dependencies @@ -65,12 +62,7 @@ public enum AppSetup { var backgroundTask: SessionBackgroundTask? = (backgroundTask ?? SessionBackgroundTask(label: #function, using: dependencies)) dependencies[singleton: .storage].perform( - migrationTargets: additionalMigrationTargets - .appending(contentsOf: [ - SNUtilitiesKit.self, - SNNetworkingKit.self, - SNMessagingKit.self - ]), + migrations: SNMessagingKit.migrations, onProgressUpdate: migrationProgressChanged, onComplete: { originalResult in // Now that the migrations are complete there are a few more states which need diff --git a/_SharedTestUtilities/SynchronousStorage.swift b/_SharedTestUtilities/SynchronousStorage.swift index fef7fd8aae..59510cf436 100644 --- a/_SharedTestUtilities/SynchronousStorage.swift +++ b/_SharedTestUtilities/SynchronousStorage.swift @@ -11,8 +11,7 @@ class SynchronousStorage: Storage, DependenciesSettable, InitialSetupable { public init( customWriter: DatabaseWriter? = nil, - migrationTargets: [MigratableTarget.Type]? = nil, - migrations: [Storage.KeyedMigration]? = nil, + migrations: [Migration.Type]? = nil, using dependencies: Dependencies, initialData: ((ObservingDatabase) throws -> ())? = nil ) { @@ -21,20 +20,9 @@ class SynchronousStorage: Storage, DependenciesSettable, InitialSetupable { super.init(customWriter: customWriter, using: dependencies) - // Process any migration targets first - if let migrationTargets: [MigratableTarget.Type] = migrationTargets { + if let migrations: [Migration.Type] = migrations { perform( - migrationTargets: migrationTargets, - async: false, - onProgressUpdate: nil, - onComplete: { _ in } - ) - } - - // Then process any provided migration info - if let migrations: [Storage.KeyedMigration] = migrations { - perform( - sortedMigrations: migrations, + migrations: migrations, async: false, onProgressUpdate: nil, onComplete: { _ in } From 994be245be08e740cf65a0b7bae6706de98821aa Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 8 Aug 2025 10:12:29 +1000 Subject: [PATCH 05/59] [WIP] Initial work on integrated refactored libSession networking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Started work on integrating refactored libSession networking • Fixed a couple of issues with the libSession build script --- Scripts/build_libSession_util.sh | 73 ++- Session.xcodeproj/project.pbxproj | 4 + Session/Settings/NukeDataModal.swift | 4 +- .../Jobs/ConfigurationSyncJob.swift | 2 +- .../Open Groups/OpenGroupAPI.swift | 16 +- .../Types/Request+OpenGroupAPI.swift | 8 +- .../Notifications/PushNotificationAPI.swift | 8 +- .../Types/Request+PushNotificationAPI.swift | 6 +- .../Utilities/DisplayPictureManager.swift | 2 +- .../Models/SnodeReceivedMessageInfo.swift | 4 +- .../LibSession/LibSession+Networking.swift | 614 +++++++++++++----- .../SessionNetworkAPI/SessionNetworkAPI.swift | 4 +- .../SnodeAPI/Request+SnodeAPI.swift | 72 +- SessionNetworkingKit/SnodeAPI/SnodeAPI.swift | 51 +- SessionNetworkingKit/Types/Network.swift | 20 +- .../Types/PreparedRequest+Sending.swift | 9 +- .../Types/PreparedRequest.swift | 106 +-- SessionNetworkingKit/Types/Request.swift | 36 +- .../Types/RequestCategory.swift | 31 + .../Types/PreparedRequestSendingSpec.swift | 42 +- .../Types/PreparedRequestSpec.swift | 12 +- .../CommonSSKMockExtensions.swift | 4 + SessionTests/Onboarding/OnboardingSpec.swift | 29 +- 23 files changed, 795 insertions(+), 362 deletions(-) diff --git a/Scripts/build_libSession_util.sh b/Scripts/build_libSession_util.sh index df2d64b325..f20b3527e2 100755 --- a/Scripts/build_libSession_util.sh +++ b/Scripts/build_libSession_util.sh @@ -22,6 +22,30 @@ function finish { } trap finish EXIT ERR SIGINT SIGTERM +# Robustly removes a directory, first clearing any immutable flags (work around Xcode's indexer file locking) +remove_locked_dir() { + local dir_to_remove="$1" + if [ -d "${dir_to_remove}" ]; then + echo "- Unlocking and removing ${dir_to_remove}" + chflags -R nouchg "${dir_to_remove}" &>/dev/null || true + rm -rf "${dir_to_remove}" + fi +} + +sync_headers() { + local source_dir="$1" + echo "- Syncing headers from ${source_dir}" + remove_locked_dir "${TARGET_BUILD_DIR}/include" + remove_locked_dir "${INDEX_DIR}/include" + + # Ensure destination parent directories exist + mkdir -p "${TARGET_BUILD_DIR}/include" + mkdir -p "${INDEX_DIR}/include" + + rsync -rtc --delete --exclude='.DS_Store' "${source_dir}/" "${TARGET_BUILD_DIR}/include/" + rsync -rtc --delete --exclude='.DS_Store' "${source_dir}/" "${INDEX_DIR}/include/" +} + # Determine whether we want to build from source TARGET_ARCH_DIR="" @@ -35,11 +59,10 @@ else fi if [ "${COMPILE_LIB_SESSION}" != "YES" ]; then - echo "Restoring original headers to Xcode Indexer cache from backup..." - rm -rf "${INDEX_DIR}/include" - rsync -rt --exclude='.DS_Store' "${PRE_BUILT_FRAMEWORK_DIR}/${FRAMEWORK_DIR}/${TARGET_ARCH_DIR}/Headers/" "${INDEX_DIR}/include" - echo "Using pre-packaged SessionUtil" + sync_headers "${PRE_BUILT_FRAMEWORK_DIR}/${FRAMEWORK_DIR}/${TARGET_ARCH_DIR}/Headers/" + echo "- Revert to SPM complete." + exit 0 fi @@ -83,20 +106,22 @@ fi echo "- Checking if libSession changed..." REQUIRES_BUILD=0 -# Generate a hash to determine whether any source files have changed -SOURCE_HASH=$(find "${LIB_SESSION_SOURCE_DIR}/src" -type f -not -name '.DS_Store' -exec md5 {} + | awk '{print $NF}' | sort | md5 | awk '{print $NF}') -HEADER_HASH=$(find "${LIB_SESSION_SOURCE_DIR}/include" -type f -not -name '.DS_Store' -exec md5 {} + | awk '{print $NF}' | sort | md5 | awk '{print $NF}') -EXTERNAL_HASH=$(find "${LIB_SESSION_SOURCE_DIR}/external" -type f -not -name '.DS_Store' -exec md5 {} + | awk '{print $NF}' | sort | md5 | awk '{print $NF}') -MAKE_LISTS_HASH=$(md5 -q "${LIB_SESSION_SOURCE_DIR}/CMakeLists.txt") -STATIC_BUNDLE_HASH=$(md5 -q "${LIB_SESSION_SOURCE_DIR}/utils/static-bundle.sh") - -CURRENT_SOURCE_TREE_HASH=$( ( - echo "${SOURCE_HASH}" - echo "${HEADER_HASH}" - echo "${EXTERNAL_HASH}" - echo "${MAKE_LISTS_HASH}" - echo "${STATIC_BUNDLE_HASH}" -) | sort | md5 -q) +# Generate a hash to determine whether any source files have changed (by using git we automatically +# respect .gitignore) +CURRENT_SOURCE_TREE_HASH=$( \ + ( \ + cd "${LIB_SESSION_SOURCE_DIR}" && git ls-files --recurse-submodules \ + ) \ + | grep -vE '/(tests?|docs?|examples?)/|\.md$|/(\.DS_Store|\.gitignore)$' \ + | sort \ + | tr '\n' '\0' \ + | ( \ + cd "${LIB_SESSION_SOURCE_DIR}" && xargs -0 md5 -r \ + ) \ + | awk '{print $1}' \ + | sort \ + | md5 -q \ +) PREVIOUS_BUILT_FRAMEWORK_SLICE_DIR="" if [ -f "$LAST_BUILT_FRAMEWORK_SLICE_DIR_FILE" ]; then @@ -217,6 +242,8 @@ if [ "${REQUIRES_BUILD}" == 1 ]; then -DBUILD_TESTS=OFF \ -DBUILD_STATIC_DEPS=ON \ -DENABLE_VISIBILITY=ON \ + -DLOKINET_FULL=OFF \ + -DLOKINET_DAEMON=OFF \ -DSUBMODULE_CHECK=$submodule_check \ -DCMAKE_BUILD_TYPE=$build_type \ -DLOCAL_MIRROR=https://oxen.rocks/deps @@ -318,15 +345,11 @@ fi echo "- Replacing build dir files" -# Remove the current files (might be "newer") -rm -rf "${TARGET_BUILD_DIR}/libsession-util.a" -rm -rf "${TARGET_BUILD_DIR}/include" -rm -rf "${INDEX_DIR}/include" - # Rsync the compiled ones (maintaining timestamps) +rm -rf "${TARGET_BUILD_DIR}/libsession-util.a" rsync -rt "${COMPILE_DIR}/libsession-util.a" "${TARGET_BUILD_DIR}/libsession-util.a" -rsync -rt --exclude='.DS_Store' "${COMPILE_DIR}/Headers/" "${TARGET_BUILD_DIR}/include" -rsync -rt --exclude='.DS_Store' "${COMPILE_DIR}/Headers/" "${INDEX_DIR}/include" +sync_headers "${COMPILE_DIR}/Headers/" +echo "- Sync complete." # Output to XCode just so the output is good echo "LibSession is Ready" diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 1fc3020a15..246d109eef 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -1008,6 +1008,7 @@ FDD23AEE2E459E470057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C927F546D900122BE0 /* _001_SUK_InitialSetupMigration.swift */; }; FDD23AEF2E459EC90057E853 /* _012_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _012_AddJobPriority.swift */; }; FDD23AF02E459EDD0057E853 /* _020_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */; }; + FDD23ADE2E44501E0057E853 /* RequestCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD23ADD2E44501B0057E853 /* RequestCategory.swift */; }; FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; }; FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; }; @@ -2268,6 +2269,7 @@ FDD20C152A09E64A003898FB /* GetExpiriesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetExpiriesRequest.swift; sourceTree = ""; }; FDD20C172A09E7D3003898FB /* GetExpiriesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetExpiriesResponse.swift; sourceTree = ""; }; FDD20C192A0A03AC003898FB /* DeleteInboxResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteInboxResponse.swift; sourceTree = ""; }; + FDD23ADD2E44501B0057E853 /* RequestCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestCategory.swift; sourceTree = ""; }; FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Utilities.swift"; sourceTree = ""; }; FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = ""; }; FDD383702AFDD0E1001367F2 /* BencodeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeResponse.swift; sourceTree = ""; }; @@ -5026,6 +5028,7 @@ FDB5DAF22A96DD4F002C8721 /* PreparedRequest+Sending.swift */, FD22729D2C33E336004D8A6C /* ProxiedContentDownloader.swift */, FD2272A32C33E337004D8A6C /* Request.swift */, + FDD23ADD2E44501B0057E853 /* RequestCategory.swift */, FD2272A52C33E337004D8A6C /* ResponseInfo.swift */, FD2272A02C33E336004D8A6C /* SwarmDrainBehaviour.swift */, FD2272962C33E335004D8A6C /* UpdatableTimestamp.swift */, @@ -6173,6 +6176,7 @@ files = ( FD2272B12C33E337004D8A6C /* ProxiedContentDownloader.swift in Sources */, FDF848C329405C5A007DCAE5 /* DeleteMessagesRequest.swift in Sources */, + FDD23ADE2E44501E0057E853 /* RequestCategory.swift in Sources */, FDF8489129405C13007DCAE5 /* SnodeAPINamespace.swift in Sources */, FD2272AB2C33E337004D8A6C /* ValidatableResponse.swift in Sources */, FD2272B72C33E337004D8A6C /* Request.swift in Sources */, diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index d4162bb02c..316a158c0c 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -202,7 +202,7 @@ final class NukeDataModal: Modal { switch authMethod.info { case .community(let server, _, _, _, _): return try OpenGroupAPI.preparedClearInbox( - requestAndPathBuildTimeout: Network.defaultTimeout, + overallTimeout: Network.defaultTimeout, authMethod: authMethod, using: dependencies ) @@ -221,7 +221,7 @@ final class NukeDataModal: Modal { try SnodeAPI .preparedDeleteAllMessages( namespace: .all, - requestAndPathBuildTimeout: Network.defaultTimeout, + overallTimeout: Network.defaultTimeout, authMethod: authMethod, using: dependencies ) diff --git a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift index 136b736815..bdfbfdd262 100644 --- a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift +++ b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift @@ -132,7 +132,7 @@ public enum ConfigurationSyncJob: JobExecutor { requireAllBatchResponses: (additionalTransientData?.requireAllBatchResponses == true), swarmPublicKey: swarmPublicKey, snodeRetrievalRetryCount: 0, // This job has it's own retry mechanism - requestAndPathBuildTimeout: Network.defaultTimeout, + overallTimeout: Network.defaultTimeout, using: dependencies ).send(using: dependencies) } diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 9f80cbe964..d52c44a010 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -806,11 +806,11 @@ public enum OpenGroupAPI { x25519PublicKey: publicKey, fileName: fileName ), - body: data + body: data, + requestTimeout: Network.fileUploadTimeout ), responseType: FileUploadResponse.self, additionalSignatureData: AdditionalSigningData(authMethod), - requestTimeout: Network.fileUploadTimeout, using: dependencies ) .signed(with: OpenGroupAPI.signRequest, using: dependencies) @@ -844,11 +844,11 @@ public enum OpenGroupAPI { return try Network.PreparedRequest( request: Request( endpoint: .roomFileIndividual(roomToken, fileId), - authMethod: authMethod + authMethod: authMethod, + requestTimeout: Network.fileDownloadTimeout ), responseType: Data.self, additionalSignatureData: AdditionalSigningData(authMethod), - requestTimeout: Network.fileDownloadTimeout, using: dependencies ) .signed(with: OpenGroupAPI.signRequest, using: dependencies) @@ -898,7 +898,7 @@ public enum OpenGroupAPI { /// Remove all message requests from inbox, this methrod will return the number of messages deleted public static func preparedClearInbox( requestTimeout: TimeInterval = Network.defaultTimeout, - requestAndPathBuildTimeout: TimeInterval? = nil, + overallTimeout: TimeInterval? = nil, authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest { @@ -906,12 +906,12 @@ public enum OpenGroupAPI { request: Request( method: .delete, endpoint: .inbox, - authMethod: authMethod + authMethod: authMethod, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout ), responseType: DeleteInboxResponse.self, additionalSignatureData: AdditionalSigningData(authMethod), - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) .signed(with: OpenGroupAPI.signRequest, using: dependencies) diff --git a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift index 5c8d72187f..05a7fda686 100644 --- a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift @@ -14,7 +14,9 @@ public extension Request where Endpoint == OpenGroupAPI.Endpoint { queryParameters: [HTTPQueryParam: String] = [:], headers: [HTTPHeader: String] = [:], body: T? = nil, - authMethod: AuthenticationMethod + authMethod: AuthenticationMethod, + requestTimeout: TimeInterval = Network.defaultTimeout, + overallTimeout: TimeInterval? = nil ) throws { guard case .community(let server, let publicKey, _, _, _) = authMethod.info else { throw CryptoError.signatureGenerationFailed @@ -29,7 +31,9 @@ public extension Request where Endpoint == OpenGroupAPI.Endpoint { headers: headers, x25519PublicKey: publicKey ), - body: body + body: body, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout ) } } diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index 2b4201f5a1..e559754c80 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -186,10 +186,10 @@ public enum PushNotificationAPI { timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) // Seconds ) } - ) + ), + retryCount: PushNotificationAPI.maxRetryCount ), responseType: SubscribeResponse.self, - retryCount: PushNotificationAPI.maxRetryCount, using: dependencies ) .handleEvents( @@ -233,10 +233,10 @@ public enum PushNotificationAPI { timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) // Seconds ) } - ) + ), + retryCount: PushNotificationAPI.maxRetryCount ), responseType: UnsubscribeResponse.self, - retryCount: PushNotificationAPI.maxRetryCount, using: dependencies ) .handleEvents( diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift index c63988f5d1..1cb0fb6465 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift @@ -12,7 +12,8 @@ public extension Request where Endpoint == PushNotificationAPI.Endpoint { endpoint: Endpoint, queryParameters: [HTTPQueryParam: String] = [:], headers: [HTTPHeader: String] = [:], - body: T? = nil + body: T? = nil, + retryCount: Int = 0 ) throws { self = try Request( endpoint: endpoint, @@ -23,7 +24,8 @@ public extension Request where Endpoint == PushNotificationAPI.Endpoint { headers: headers, x25519PublicKey: PushNotificationAPI.serverPublicKey ), - body: body + body: body, + retryCount: retryCount ) } } diff --git a/SessionMessagingKit/Utilities/DisplayPictureManager.swift b/SessionMessagingKit/Utilities/DisplayPictureManager.swift index 6663fcf0f8..fec7bb37ac 100644 --- a/SessionMessagingKit/Utilities/DisplayPictureManager.swift +++ b/SessionMessagingKit/Utilities/DisplayPictureManager.swift @@ -281,7 +281,7 @@ public class DisplayPictureManager { guard let preparedUpload: Network.PreparedRequest = try? Network.preparedUpload( data: encryptedData, - requestAndPathBuildTimeout: Network.fileUploadTimeout, + overallTimeout: Network.fileUploadTimeout, using: dependencies ) else { diff --git a/SessionNetworkingKit/Database/Models/SnodeReceivedMessageInfo.swift b/SessionNetworkingKit/Database/Models/SnodeReceivedMessageInfo.swift index a54cbc083f..8561a35b7b 100644 --- a/SessionNetworkingKit/Database/Models/SnodeReceivedMessageInfo.swift +++ b/SessionNetworkingKit/Database/Models/SnodeReceivedMessageInfo.swift @@ -60,7 +60,7 @@ public extension SnodeReceivedMessageInfo { expirationDateMs: Int64? ) { self.swarmPublicKey = swarmPublicKey - self.snodeAddress = snode.address + self.snodeAddress = snode.omqAddress self.namespace = namespace.rawValue self.hash = hash self.expirationDateMs = (expirationDateMs ?? 0) @@ -85,7 +85,7 @@ public extension SnodeReceivedMessageInfo { .filter(SnodeReceivedMessageInfo.Columns.wasDeletedOrInvalid == false) .filter( SnodeReceivedMessageInfo.Columns.swarmPublicKey == swarmPublicKey && - SnodeReceivedMessageInfo.Columns.snodeAddress == snode.address && + SnodeReceivedMessageInfo.Columns.snodeAddress == snode.omqAddress && SnodeReceivedMessageInfo.Columns.namespace == namespace.rawValue ) .filter(SnodeReceivedMessageInfo.Columns.expirationDateMs > currentOffsetTimestampMs) diff --git a/SessionNetworkingKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift index c445d3c433..020fc0119e 100644 --- a/SessionNetworkingKit/LibSession/LibSession+Networking.swift +++ b/SessionNetworkingKit/LibSession/LibSession+Networking.swift @@ -48,7 +48,7 @@ class LibSessionNetwork: NetworkType { typealias Output = Result, Error> return dependencies - .mutate(cache: .libSessionNetwork) { $0.getOrCreateNetwork() } + .mutate(cache: .libSessionNetwork) { $0.getOrCreateNetwork_v2() } .tryMapCallbackContext(type: Output.self) { ctx, network in let sessionId: SessionId = try SessionId(from: swarmPublicKey) @@ -56,7 +56,7 @@ class LibSessionNetwork: NetworkType { throw LibSessionError.invalidCConversion } - network_get_swarm(network, cSwarmPublicKey, { swarmPtr, swarmSize, ctx in + session_network_get_swarm(network, cSwarmPublicKey, { swarmPtr, swarmSize, ctx in guard swarmSize > 0, let cSwarm: UnsafeMutablePointer = swarmPtr @@ -110,28 +110,28 @@ class LibSessionNetwork: NetworkType { } func send( - _ body: Data?, - to destination: Network.Destination, + endpoint: (any EndpointType), + destination: Network.Destination, + body: Data?, + category: Network.RequestCategory, requestTimeout: TimeInterval, - requestAndPathBuildTimeout: TimeInterval? + overallTimeout: TimeInterval? ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { +// func send( +// _ body: Data?, +// to destination: Network.Destination, +// requestTimeout: TimeInterval, +// requestAndPathBuildTimeout: TimeInterval? +// ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { switch destination { - case .server, .serverUpload, .serverDownload, .cached: + case .snode, .server, .serverUpload, .serverDownload, .cached: return sendRequest( - to: destination, + endpoint: endpoint, + destination: destination, body: body, + category: category, requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout - ) - - case .snode: - guard body != nil else { return Fail(error: NetworkError.invalidPreparedRequest).eraseToAnyPublisher() } - - return sendRequest( - to: destination, - body: body, - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout + overallTimeout: overallTimeout ) case .randomSnode(let swarmPublicKey, let retryCount): @@ -143,10 +143,12 @@ class LibSessionNetwork: NetworkType { return getSwarm(for: swarmPublicKey) .tryFlatMapWithRandomSnode(retry: retryCount, using: dependencies) { [weak self] snode in try self.validOrThrow().sendRequest( - to: .snode(snode, swarmPublicKey: swarmPublicKey), + endpoint: endpoint, + destination: .snode(snode, swarmPublicKey: swarmPublicKey), body: body, + category: category, requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout + overallTimeout: overallTimeout ) } @@ -168,10 +170,12 @@ class LibSessionNetwork: NetworkType { else { throw NetworkError.invalidPreparedRequest } return try self.validOrThrow().sendRequest( - to: .snode(snode, swarmPublicKey: swarmPublicKey), + endpoint: endpoint, + destination: .snode(snode, swarmPublicKey: swarmPublicKey), body: updatedBody, + category: category, requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout + overallTimeout: overallTimeout ) .map { info, response -> (ResponseInfoType, Data?) in ( @@ -229,124 +233,113 @@ class LibSessionNetwork: NetworkType { // MARK: - Internal Functions private func sendRequest( - to destination: Network.Destination, + endpoint: (any EndpointType), + destination: Network.Destination, body: T?, + category: Network.RequestCategory, requestTimeout: TimeInterval, - requestAndPathBuildTimeout: TimeInterval? + overallTimeout: TimeInterval? ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { typealias Output = (success: Bool, timeout: Bool, statusCode: Int, headers: [String: String], data: Data?) return dependencies - .mutate(cache: .libSessionNetwork) { $0.getOrCreateNetwork() } + .mutate(cache: .libSessionNetwork) { $0.getOrCreateNetwork_v2() } .tryMapCallbackContext(type: Output.self) { ctx, network in - // Prepare the parameters - let cPayloadBytes: [UInt8] + /// If it's a cached request then just return the cached result immediately + if case .cached(let success, let timeout, let statusCode, let headers, let data) = destination { + return CallbackWrapper.run(ctx, (success, timeout, statusCode, headers, data)) + } - switch body { - case .none: cPayloadBytes = [] - case let data as Data: cPayloadBytes = Array(data) - case let bytes as [UInt8]: cPayloadBytes = bytes - default: - guard let encodedBody: Data = try? JSONEncoder().encode(body) else { - throw SnodeAPIError.invalidPayload - } - - cPayloadBytes = Array(encodedBody) + /// Define the callback to avoid dupolication + typealias ResponseCallback = session_network_response_t + let cCallback: ResponseCallback = { success, timeout, statusCode, cHeaders, cHeadersLen, dataPtr, dataLen, ctx in + let headers: [String: String] = CallbackWrapper.headers(cHeaders, cHeadersLen) + let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } + CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) } + let request: Request = Request( + endpoint: endpoint, + body: body, + category: category, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout + ) - // Trigger the request switch destination { - // These types should be processed and converted to a 'snode' destination before - // they get here - case .randomSnode, .randomSnodeLatestNetworkTimeTarget: - throw NetworkError.invalidPreparedRequest - - case .snode(let snode, let swarmPublicKey): - let cSwarmPublicKey: [CChar]? = try swarmPublicKey.map { - _ = try SessionId(from: $0) - - // Quick way to drop '05' prefix if present - return $0.suffix(64).cString(using: .utf8) - }.flatMap { $0 } - - network_send_onion_request_to_snode_destination( - network, - snode.cSnode, - cPayloadBytes, - cPayloadBytes.count, - cSwarmPublicKey, - Int64(floor(requestTimeout * 1000)), - Int64(floor((requestAndPathBuildTimeout ?? 0) * 1000)), - { success, timeout, statusCode, cHeaders, cHeaderVals, headerLen, dataPtr, dataLen, ctx in - let headers: [String: String] = CallbackWrapper - .headers(cHeaders, cHeaderVals, headerLen) - let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } - CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) - }, - ctx - ) - - case .server: - try destination.withUnsafePointer { cServerDestination in - network_send_onion_request_to_server_destination( - network, - cServerDestination, - cPayloadBytes, - cPayloadBytes.count, - Int64(floor(requestTimeout * 1000)), - Int64(floor((requestAndPathBuildTimeout ?? 0) * 1000)), - { success, timeout, statusCode, cHeaders, cHeaderVals, headerLen, dataPtr, dataLen, ctx in - let headers: [String: String] = CallbackWrapper - .headers(cHeaders, cHeaderVals, headerLen) - let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } - CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) - }, - ctx - ) + case .snode(let snode, _): + try LibSessionNetwork.withSnodeRequestParams(request, snode) { paramsPtr in +// var mutableParams = params + session_network_send_request(network, paramsPtr, cCallback, ctx) } - - case .serverUpload(_, let fileName): - guard !cPayloadBytes.isEmpty else { throw NetworkError.invalidPreparedRequest } - try destination.withUnsafePointer { cServerDestination in - network_upload_to_server( - network, - cServerDestination, - cPayloadBytes, - cPayloadBytes.count, - fileName?.cString(using: .utf8), - Int64(floor(requestTimeout * 1000)), - Int64(floor((requestAndPathBuildTimeout ?? 0) * 1000)), - { success, timeout, statusCode, cHeaders, cHeaderVals, headerLen, dataPtr, dataLen, ctx in - let headers: [String: String] = CallbackWrapper - .headers(cHeaders, cHeaderVals, headerLen) - let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } - CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) - }, - ctx - ) - } - - case .serverDownload: - try destination.withUnsafePointer { cServerDestination in - network_download_from_server( - network, - cServerDestination, - Int64(floor(requestTimeout * 1000)), - Int64(floor((requestAndPathBuildTimeout ?? 0) * 1000)), - { success, timeout, statusCode, cHeaders, cHeaderVals, headerLen, dataPtr, dataLen, ctx in - let headers: [String: String] = CallbackWrapper - .headers(cHeaders, cHeaderVals, headerLen) - let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } - CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) - }, - ctx - ) + case .server(let info), .serverUpload(let info, _), .serverDownload(let info): + try LibSessionNetwork.withServerRequestParams(request, info) { paramsPtr in + session_network_send_request(network, paramsPtr, cCallback, ctx) } - - case .cached(let success, let timeout, let statusCode, let headers, let data): - CallbackWrapper.run(ctx, (success, timeout, statusCode, headers, data)) + + /// Some destinations are for convenience and redirect to "proper" destination types so if one of them gets here + /// then it is invalid + default: throw NetworkError.invalidPreparedRequest } + +// /// Prepare the values +// var cSnode: network_service_node? = { +// switch destination { +// case .snode(let snode, _): return snode.cSnode +// default: return nil +// } +// }() +// var serverInfo: Network.Destination.ServerInfo? = { +// switch destination { +// case .server(let info), .serverUpload(let info, _), .serverDownload(let info): +// return info +// +// default: return nil +// } +// }() +// let cBodyBytes: [UInt8] = try { +// switch body { +// case .none: return [] +// case let data as Data: return Array(data) +// case let bytes as [UInt8]: return bytes +// default: +// guard let encodedBody: Data = try? JSONEncoder().encode(body) else { +// throw SnodeAPIError.invalidPayload +// } +// +// return Array(encodedBody) +// } +// }() +// +// /// Construct and send the params +// return try endpoint.path.withCString { cEndpoint in +// try serverInfo.withUnsafePointer { cServerDest in +// // TODO: `fileName?.cString(using: .utf8),` for server upload +// var params = session_request_params() +//// var params = session_request_params( +//// snode_dest: &cSnode, +//// server_dest: cServerDest, +//// endpoint: cEndpoint, +//// body: &cBodyBytes, +//// body_size: cBodyBytes.count, +//// category: category.libSessionValue, +//// request_timeout_ms: Int64(floor(requestTimeout * 1000)), +//// overall_timeout_ms: Int64(floor((overallTimeout ?? 0) * 1000)), +//// request_id: nil +//// ) +// +// session_network_send_request( +// network, +// ¶ms, +// { success, timeout, statusCode, cHeaders, dataPtr, dataLen, ctx in +// let headers: [String: String] = CallbackWrapper.headers(cHeaders) +// let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } +// CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) +// }, +// ctx +// ) +// } +// } } .tryMap { [dependencies] success, timeout, statusCode, headers, data -> (any ResponseInfoType, Data?) in try LibSessionNetwork.throwErrorIfNeeded(success, timeout, statusCode, headers, data, using: dependencies) @@ -503,45 +496,71 @@ private extension NetworkStatus { extension LibSession { public struct Snode: Codable, Hashable, CustomStringConvertible { - public let ip: String - public let quicPort: UInt16 public let ed25519PubkeyHex: String + public let ip: String + public let httpsPort: UInt16 + public let omqPort: UInt16 + public let version: String + public let swarmId: UInt64 - public var address: String { "\(ip):\(quicPort)" } - public var description: String { address } + public var httpsAddress: String { "\(ip):\(httpsPort)" } + public var omqAddress: String { "\(ip):\(omqPort)" } + public var description: String { omqAddress } public var cSnode: network_service_node { var result: network_service_node = network_service_node() - result.ipString = ip - result.set(\.quic_port, to: quicPort) result.set(\.ed25519_pubkey_hex, to: ed25519PubkeyHex) + result.ipString = ip + result.set(\.https_port, to: httpsPort) + result.set(\.omq_port, to: omqPort) + result.versionString = version + result.set(\.swarm_id, to: swarmId) return result } init(_ cSnode: network_service_node) { - ip = cSnode.ipString - quicPort = cSnode.get(\.quic_port) ed25519PubkeyHex = cSnode.get(\.ed25519_pubkey_hex) + ip = cSnode.ipString + httpsPort = cSnode.get(\.https_port) + omqPort = cSnode.get(\.omq_port) + version = cSnode.versionString + swarmId = cSnode.get(\.swarm_id) } - internal init(ip: String, quicPort: UInt16, ed25519PubkeyHex: String) { - self.ip = ip - self.quicPort = quicPort + internal init( + ed25519PubkeyHex: String, + ip: String, + httpsPort: UInt16, + quicPort: UInt16, + version: String, + swarmId: UInt64 + ) { self.ed25519PubkeyHex = ed25519PubkeyHex + self.ip = ip + self.httpsPort = httpsPort + self.omqPort = quicPort + self.version = version + self.swarmId = swarmId } public func hash(into hasher: inout Hasher) { - ip.hash(into: &hasher) - quicPort.hash(into: &hasher) ed25519PubkeyHex.hash(into: &hasher) + ip.hash(into: &hasher) + httpsPort.hash(into: &hasher) + omqPort.hash(into: &hasher) + version.hash(into: &hasher) + swarmId.hash(into: &hasher) } public static func == (lhs: Snode, rhs: Snode) -> Bool { return ( + lhs.ed25519PubkeyHex == rhs.ed25519PubkeyHex && lhs.ip == rhs.ip && - lhs.quicPort == rhs.quicPort && - lhs.ed25519PubkeyHex == rhs.ed25519PubkeyHex + lhs.httpsPort == rhs.httpsPort && + lhs.omqPort == rhs.omqPort && + lhs.version == rhs.version && + lhs.swarmId == rhs.swarmId ) } } @@ -562,26 +581,124 @@ extension network_service_node: @retroactive CAccessible, @retroactive CMutable self.ip = (ipParts[0], ipParts[1], ipParts[2], ipParts[3]) } } + + var versionString: String { + get { "\(version.0).\(version.1).\(version.2)" } + set { + let versionParts: [UInt16] = newValue + .components(separatedBy: ".") + .compactMap { UInt16($0) } + + guard versionParts.count == 3 else { return } + + self.version = (versionParts[0], versionParts[1], versionParts[2]) + } + } } // MARK: - Convenience -private extension Network.Destination { - func withUnsafePointer(_ body: (network_server_destination) throws -> Result) throws -> Result { - let method: HTTPMethod - let url: URL - let headers: [HTTPHeader: String]? - let x25519PublicKey: String +private extension LibSessionNetwork { + struct Request { + let endpoint: (any EndpointType) + let body: T? + let category: Network.RequestCategory + let requestTimeout: TimeInterval + let overallTimeout: TimeInterval? + } + + static func withSnodeRequestParams( + _ request: Request, + _ node: LibSession.Snode, + _ callback: (UnsafePointer) -> Result + ) throws -> Result { + var cSnode = node.cSnode - switch self { - case .snode, .randomSnode, .randomSnodeLatestNetworkTimeTarget, .cached: throw NetworkError.invalidPreparedRequest - case .server(let info), .serverUpload(let info, _), .serverDownload(let info): - method = info.method - url = try info.url - headers = info.headers - x25519PublicKey = String(info.x25519PublicKey - .suffix(64)) // Quick way to drop '05' prefix if present + return try withBodyPointer(request.body) { cBodyPtr, bodySize in + withUnsafePointer(to: &cSnode) { cSnodePtr in + request.endpoint.path.withCString { cEndpoint in + let params: session_request_params = session_request_params( + snode_dest: cSnodePtr, + server_dest: nil, + endpoint: cEndpoint, + body: cBodyPtr, + body_size: bodySize, + category: request.category.libSessionValue, + request_timeout_ms: UInt64(Int64(floor(request.requestTimeout * 1000))), + overall_timeout_ms: UInt64(floor((request.overallTimeout ?? 0) * 1000)), + request_id: nil + ) + + return withUnsafePointer(to: params) { paramsPtr in + callback(paramsPtr) + } + } + } + } + } + + static func withServerRequestParams( + _ request: Request, + _ info: Network.Destination.ServerInfo, + _ callback: (UnsafePointer) -> Result + ) throws -> Result { + + return try withBodyPointer(request.body) { cBodyPtr, bodySize in + try info.withServerInfoPointer { cServerDestinationPtr in + request.endpoint.path.withCString { cEndpoint in + let params: session_request_params = session_request_params( + snode_dest: nil, + server_dest: cServerDestinationPtr, + endpoint: cEndpoint, + body: cBodyPtr, + body_size: bodySize, + category: request.category.libSessionValue, + request_timeout_ms: UInt64(floor(request.requestTimeout * 1000)), + overall_timeout_ms: UInt64(floor((request.overallTimeout ?? 0) * 1000)), + request_id: nil + ) + + return withUnsafePointer(to: params) { paramsPtr in + callback(paramsPtr) + } + } + } + } + } + + private static func withBodyPointer( + _ body: T?, + _ closure: (UnsafePointer?, Int) throws -> Result + ) throws -> Result { + let maybeBodyData: Data? + + switch body { + case .none: maybeBodyData = nil + case let data as Data: maybeBodyData = data + case let bytes as [UInt8]: maybeBodyData = Data(bytes) + default: + guard let encodedBody: Data = try? JSONEncoder().encode(body) else { + throw SnodeAPIError.invalidPayload + } + + maybeBodyData = encodedBody + } + + guard let bodyData: Data = maybeBodyData, !bodyData.isEmpty else { + return try closure(nil, 0) + } + + return try bodyData.withUnsafeBytes { (rawPtr: UnsafeRawBufferPointer) in + let ptr: UnsafePointer? = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) + return try closure(ptr, bodyData.count) } + } +} + +private extension Network.Destination.ServerInfo { + func withServerInfoPointer(_ body: (UnsafePointer) -> Result) throws -> Result { + let url: URL = try self.url + let x25519PublicKey: String = String(x25519PublicKey.suffix(64)) // Quick way to drop '05' prefix if present guard let host: String = url.host else { throw NetworkError.invalidURL } guard x25519PublicKey.count == 64 || x25519PublicKey.count == 66 else { @@ -592,9 +709,7 @@ private extension Network.Destination { let endpoint: String = url.path .appending(url.query.map { value in "?\(value)" } ?? "") let port: UInt16 = UInt16(url.port ?? (targetScheme == "https" ? 443 : 80)) - let headerKeys: [String] = (headers?.map { $0.key } ?? []) - let headerValues: [String] = (headers?.map { $0.value } ?? []) - let headersSize = headerKeys.count + let headersArray: [String] = headers.flatMap { [$0.key, $0.value] } // Use scoped closure to avoid manual memory management (crazy nesting but it ends up safer) return try method.rawValue.withCString { cMethodPtr in @@ -602,21 +717,20 @@ private extension Network.Destination { try host.withCString { cHostPtr in try endpoint.withCString { cEndpointPtr in try x25519PublicKey.withCString { cX25519PubkeyPtr in - try headerKeys.withUnsafeCStrArray { headerKeysPtr in - try headerValues.withUnsafeCStrArray { headerValuesPtr in - let cServerDest = network_server_destination( - method: cMethodPtr, - protocol: cTargetSchemePtr, - host: cHostPtr, - endpoint: cEndpointPtr, - port: port, - x25519_pubkey: cX25519PubkeyPtr, - headers: headerKeysPtr.baseAddress, - header_values: headerValuesPtr.baseAddress, - headers_size: headersSize - ) - - return try body(cServerDest) + try headersArray.withUnsafeCStrArray { headersArrayPtr in + let cServerDest = network_v2_server_destination( + method: cMethodPtr, + protocol: cTargetSchemePtr, + host: cHostPtr, + endpoint: cEndpointPtr, // TODO: Ditch this + port: port, + x25519_pubkey_hex: cX25519PubkeyPtr, + headers_kv_pairs: headersArrayPtr.baseAddress, + headers_kv_pairs_len: headersArray.count + ) + + return withUnsafePointer(to: cServerDest) { ptr in + body(ptr) } } } @@ -639,6 +753,17 @@ private extension LibSessionNetwork.CallbackWrapper { return zip(headers, headerVals) .reduce(into: [:]) { result, next in result[next.0] = next.1 } } + + static func headers(_ cHeaders: UnsafePointer?>?, _ count: Int) -> [String: String] { + let headersArray: [String] = ([String](cStringArray: cHeaders, count: count) ?? []) + + return stride(from: 0, to: headersArray.count, by: 2) + .reduce(into: [:]) { result, index in + if (index + index) < headersArray.count { + result[headersArray[index]] = headersArray[index + 1] + } + } + } } // MARK: - LibSession.NetworkCache @@ -650,6 +775,7 @@ public extension LibSession { private let dependencies: Dependencies private let dependenciesPtr: UnsafeMutableRawPointer private var network: UnsafeMutablePointer? = nil + private var network_v2: UnsafeMutablePointer? = nil private let _paths: CurrentValueSubject<[[Snode]], Never> = CurrentValueSubject([]) private let _networkStatus: CurrentValueSubject = CurrentValueSubject(.unknown) private let _snodeNumber: CurrentValueSubject<[String: Int], Never> = .init([:]) @@ -671,6 +797,7 @@ public extension LibSession { // Create the network object getOrCreateNetwork().sinkUntilComplete() + getOrCreateNetwork_v2().sinkUntilComplete() // If the app has been set to 'forceOffline' then we need to explicitly set the network status // to disconnected (because it'll never be set otherwise) @@ -724,6 +851,9 @@ public extension LibSession { } public func getOrCreateNetwork() -> AnyPublisher?, Error> { + return Deferred { + Future?, Error> { promise in } + }.eraseToAnyPublisher() guard !isSuspended else { Log.warn(.network, "Attempted to access suspended network.") return Fail(error: NetworkError.suspended).eraseToAnyPublisher() @@ -808,9 +938,9 @@ public extension LibSession { } // Need to free the nodes within the path as we are the owner - cPaths.forEach { cPath in - free(UnsafeMutableRawPointer(mutating: cPath.nodes)) - } +// cPaths.forEach { cPath in +// free(UnsafeMutableRawPointer(mutating: cPath.nodes)) +// } } // Need to free the pathsPtr as we are the owner @@ -832,6 +962,133 @@ public extension LibSession { } } + public func getOrCreateNetwork_v2() -> AnyPublisher?, Error> { + guard !isSuspended else { + Log.warn(.network, "Attempted to access suspended network.") + return Fail(error: NetworkError.suspended).eraseToAnyPublisher() + } + + switch (network_v2, dependencies[feature: .forceOffline]) { + case (_, true): + return Fail(error: NetworkError.serviceUnavailable) + .delay(for: .seconds(1), scheduler: DispatchQueue.global(qos: .userInitiated)) + .eraseToAnyPublisher() + + case (.some(let existingNetwork), _): + return Just(existingNetwork) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + + case (.none, _): + guard let cCachePath: [CChar] = NetworkCache.snodeCachePath.cString(using: .utf8) else { + Log.error(.network, "Unable to create network object: \(LibSessionError.invalidCConversion)") + return Fail(error: NetworkError.invalidState).eraseToAnyPublisher() + } + + let useTestnet: Bool = (dependencies[feature: .serviceNetwork] == .testnet) + let isMainApp: Bool = dependencies[singleton: .appContext].isMainApp + var error: [CChar] = [CChar](repeating: 0, count: 256) + var network: UnsafeMutablePointer? + var config: session_network_config = session_network_config_default() + + if dependencies[feature: .serviceNetwork] == .testnet { + config.netid = SESSION_NETWORK_TESTNET + config.enforce_subnet_diversity = false // On testnet we can't do this as nodes share IPs + } + + + let result: Result = cCachePath.withUnsafeBufferPointer { cachePtr in + config.cache_dir = cachePtr.baseAddress + + guard session_network_init(&network, &config, &error) else { + Log.error(.network, "Unable to create network object: \(String(cString: error))") + return .failure(NetworkError.invalidState) + } + + return .success(()) + } + + switch result { + case .success: break + case .failure(let error): return Fail(error: error).eraseToAnyPublisher() + } + + // Store the newly created network + self.network_v2 = network + +// /// Register the callbacks in the next run loop (this needs to happen in a subsequent run loop because it mutates the +// /// `libSessionNetwork` cache and this function gets called during init so could end up with weird order-of-execution issues) +// /// +// /// **Note:** We do it this way because `DispatchQueue.async` can be optimised out if the code is already running in a +// /// queue with the same `qos`, this approach ensures the code will run in a subsequent run loop regardless +// let concurrentQueue = DispatchQueue(label: "Network.callback.registration", attributes: .concurrent) +// concurrentQueue.async(flags: .barrier) { [weak self] in +// guard +// let network: UnsafeMutablePointer = self?.network, +// let dependenciesPtr: UnsafeMutableRawPointer = self?.dependenciesPtr +// else { return } +// +// // Register for network status changes +// network_set_status_changed_callback(network, { cStatus, ctx in +// guard let ctx: UnsafeMutableRawPointer = ctx else { return } +// +// let status: NetworkStatus = NetworkStatus(status: cStatus) +// let dependencies: Dependencies = Unmanaged.fromOpaque(ctx).takeUnretainedValue() +// +// // Dispatch async so we don't hold up the libSession thread that triggered the update +// // or have a reentrancy issue with the mutable cache +// DispatchQueue.global(qos: .default).async { +// dependencies.mutate(cache: .libSessionNetwork) { $0.setNetworkStatus(status: status) } +// } +// }, dependenciesPtr) +// +// // Register for path changes +// network_set_paths_changed_callback(network, { pathsPtr, pathsLen, ctx in +// guard let ctx: UnsafeMutableRawPointer = ctx else { return } +// +// var paths: [[Snode]] = [] +// +// if let cPathsPtr: UnsafeMutablePointer = pathsPtr { +// var cPaths: [onion_request_path] = [] +// +// (0...fromOpaque(ctx).takeUnretainedValue() +// +// DispatchQueue.global(qos: .default).async { +// dependencies.mutate(cache: .libSessionNetwork) { $0.setPaths(paths: paths) } +// } +// }, dependenciesPtr) +// } + + return Just(network) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + } + public func setNetworkStatus(status: NetworkStatus) { guard status == .disconnected || !isSuspended else { Log.warn(.network, "Attempted to update network status to '\(status)' for suspended network, closing connections again.") @@ -909,6 +1166,7 @@ public extension LibSession { func suspendNetworkAccess() func resumeNetworkAccess() func getOrCreateNetwork() -> AnyPublisher?, Error> + func getOrCreateNetwork_v2() -> AnyPublisher?, Error> func setNetworkStatus(status: NetworkStatus) func setPaths(paths: [[Snode]]) func setSnodeNumber(publicKey: String, value: Int) @@ -935,6 +1193,10 @@ public extension LibSession { return Fail(error: NetworkError.invalidState) .eraseToAnyPublisher() } + public func getOrCreateNetwork_v2() -> AnyPublisher?, Error> { + return Fail(error: NetworkError.invalidState) + .eraseToAnyPublisher() + } public func setNetworkStatus(status: NetworkStatus) {} public func setPaths(paths: [[LibSession.Snode]]) {} @@ -944,3 +1206,5 @@ public extension LibSession { public func snodeCacheSize() -> Int { 0 } } } + +extension session_network_config: @retroactive CAccessible, @retroactive CMutable {} diff --git a/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift index b2be2d86ba..a6b68ac989 100644 --- a/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift +++ b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift @@ -27,10 +27,10 @@ public enum SessionNetworkAPI { server: Network.NetworkAPI.networkAPIServer, queryParameters: [:], x25519PublicKey: Network.NetworkAPI.networkAPIServerPublicKey - ) + ), + overallTimeout: Network.defaultTimeout ), responseType: Info.self, - requestAndPathBuildTimeout: Network.defaultTimeout, using: dependencies ) .signed(with: SessionNetworkAPI.signRequest, using: dependencies) diff --git a/SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift index 48914fdd90..ff99bea9d5 100644 --- a/SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift +++ b/SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift @@ -8,67 +8,89 @@ import SessionUtilitiesKit // MARK: Request - SnodeAPI public extension Request where Endpoint == SnodeAPI.Endpoint { - init( + init(//)( endpoint: SnodeAPI.Endpoint, snode: LibSession.Snode, swarmPublicKey: String? = nil, - body: B - ) throws where T == SnodeRequest { + body: T, + requestTimeout: TimeInterval = Network.defaultTimeout, + overallTimeout: TimeInterval? = nil, + retryCount: Int = 0 + ) throws {//where T == SnodeRequest { self = try Request( endpoint: endpoint, destination: .snode( snode, swarmPublicKey: swarmPublicKey ), - body: SnodeRequest( - endpoint: endpoint, - body: body - ) + body: body, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout, + retryCount: retryCount +// body: SnodeRequest( +// endpoint: endpoint, +// body: body +// ) ) } - init( + init(//)( endpoint: SnodeAPI.Endpoint, swarmPublicKey: String, - body: B, + body: T, + requestTimeout: TimeInterval = Network.defaultTimeout, + overallTimeout: TimeInterval? = nil, + retryCount: Int = 0, snodeRetrievalRetryCount: Int = SnodeAPI.maxRetryCount - ) throws where T == SnodeRequest { + ) throws {//where T == SnodeRequest { self = try Request( endpoint: endpoint, destination: .randomSnode( swarmPublicKey: swarmPublicKey, snodeRetrievalRetryCount: snodeRetrievalRetryCount ), - body: SnodeRequest( - endpoint: endpoint, - body: body - ) + body: body, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout, + retryCount: retryCount +// body: SnodeRequest( +// endpoint: endpoint, +// body: body +// ) ) } - init( + init(//)( endpoint: Endpoint, swarmPublicKey: String, requiresLatestNetworkTime: Bool, - body: B, + body: T,//B, + requestTimeout: TimeInterval = Network.defaultTimeout, + overallTimeout: TimeInterval? = nil, + retryCount: Int = 0, snodeRetrievalRetryCount: Int = SnodeAPI.maxRetryCount - ) throws where T == SnodeRequest, B: Encodable & UpdatableTimestamp { + ) throws where T: UpdatableTimestamp{//where T == SnodeRequest, B: Encodable & UpdatableTimestamp { self = try Request( endpoint: endpoint, destination: .randomSnodeLatestNetworkTimeTarget( swarmPublicKey: swarmPublicKey, snodeRetrievalRetryCount: snodeRetrievalRetryCount, bodyWithUpdatedTimestampMs: { timestampMs, dependencies in - SnodeRequest( - endpoint: endpoint, - body: body.with(timestampMs: timestampMs) - ) + body.with(timestampMs: timestampMs) +// SnodeRequest( +// endpoint: endpoint, +// body: body.with(timestampMs: timestampMs) +// ) } ), - body: SnodeRequest( - endpoint: endpoint, - body: body - ) + body: body, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout, + retryCount: retryCount +// body: SnodeRequest( +// endpoint: endpoint, +// body: body +// ) ) } } diff --git a/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift index 71bbe5bd41..df699e4a16 100644 --- a/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift +++ b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift @@ -90,7 +90,7 @@ public final class SnodeAPI { snode: LibSession.Snode? = nil, swarmPublicKey: String, requestTimeout: TimeInterval = Network.defaultTimeout, - requestAndPathBuildTimeout: TimeInterval? = nil, + overallTimeout: TimeInterval? = nil, using dependencies: Dependencies ) throws -> Network.PreparedRequest { return try SnodeAPI @@ -101,7 +101,9 @@ public final class SnodeAPI { return try Request( endpoint: .batch, swarmPublicKey: swarmPublicKey, - body: Network.BatchRequest(requestsKey: .requests, requests: requests) + body: Network.BatchRequest(requestsKey: .requests, requests: requests), + requestTimeout: requestTimeout, + overallTimeout: overallTimeout ) case .some(let snode): @@ -109,14 +111,14 @@ public final class SnodeAPI { endpoint: .batch, snode: snode, swarmPublicKey: swarmPublicKey, - body: Network.BatchRequest(requestsKey: .requests, requests: requests) + body: Network.BatchRequest(requestsKey: .requests, requests: requests), + requestTimeout: requestTimeout, + overallTimeout: overallTimeout ) } }(), responseType: Network.BatchResponse.self, requireAllBatchResponses: requireAllBatchResponses, - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) } @@ -127,7 +129,7 @@ public final class SnodeAPI { swarmPublicKey: String, snodeRetrievalRetryCount: Int, requestTimeout: TimeInterval = Network.defaultTimeout, - requestAndPathBuildTimeout: TimeInterval? = nil, + overallTimeout: TimeInterval? = nil, using dependencies: Dependencies ) throws -> Network.PreparedRequest { return try SnodeAPI @@ -136,12 +138,12 @@ public final class SnodeAPI { endpoint: .sequence, swarmPublicKey: swarmPublicKey, body: Network.BatchRequest(requestsKey: .requests, requests: requests), + requestTimeout: requestTimeout, + overallTimeout: overallTimeout, snodeRetrievalRetryCount: snodeRetrievalRetryCount ), responseType: Network.BatchResponse.self, requireAllBatchResponses: requireAllBatchResponses, - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) } @@ -325,10 +327,10 @@ public final class SnodeAPI { message: message, namespace: namespace ), + overallTimeout: Network.defaultTimeout, snodeRetrievalRetryCount: 0 // The SendMessageJob already has a retry mechanism ), responseType: SendMessagesResponse.self, - requestAndPathBuildTimeout: Network.defaultTimeout, using: dependencies ) } @@ -343,10 +345,10 @@ public final class SnodeAPI { authMethod: authMethod, timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() ), + overallTimeout: Network.defaultTimeout, snodeRetrievalRetryCount: 0 // The SendMessageJob already has a retry mechanism ), responseType: SendMessagesResponse.self, - requestAndPathBuildTimeout: Network.defaultTimeout, using: dependencies ) }() @@ -556,7 +558,7 @@ public final class SnodeAPI { public static func preparedDeleteAllMessages( namespace: SnodeAPI.Namespace, requestTimeout: TimeInterval = Network.defaultTimeout, - requestAndPathBuildTimeout: TimeInterval? = nil, + overallTimeout: TimeInterval? = nil, authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest<[String: Bool]> { @@ -571,11 +573,11 @@ public final class SnodeAPI { authMethod: authMethod, timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() ), + requestTimeout: requestTimeout, + overallTimeout: overallTimeout, snodeRetrievalRetryCount: 0 ), responseType: DeleteAllMessagesResponse.self, - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) .tryMap { info, response -> [String: Bool] in @@ -609,10 +611,10 @@ public final class SnodeAPI { namespace: namespace, authMethod: authMethod, timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() - ) + ), + retryCount: maxRetryCount, ), responseType: DeleteAllMessagesResponse.self, - retryCount: maxRetryCount, using: dependencies ) .tryMap { _, response -> [String: Bool] in @@ -632,11 +634,16 @@ public final class SnodeAPI { ) throws -> Network.PreparedRequest { return try SnodeAPI .prepareRequest( - request: Request, Endpoint>( + request: Request<[String: String], Endpoint>( endpoint: .getInfo, snode: snode, body: [:] ), +// request: Request, Endpoint>( +// endpoint: .getInfo, +// snode: snode, +// body: [:] +// ), responseType: GetNetworkTimestampResponse.self, using: dependencies ) @@ -656,18 +663,18 @@ public final class SnodeAPI { request: Request, responseType: R.Type, requireAllBatchResponses: Bool = true, - retryCount: Int = 0, - requestTimeout: TimeInterval = Network.defaultTimeout, - requestAndPathBuildTimeout: TimeInterval? = nil, +// retryCount: Int = 0, +// requestTimeout: TimeInterval = Network.defaultTimeout, +// requestAndPathBuildTimeout: TimeInterval? = nil, using dependencies: Dependencies ) throws -> Network.PreparedRequest { return try Network.PreparedRequest( request: request, responseType: responseType, requireAllBatchResponses: requireAllBatchResponses, - retryCount: retryCount, - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, +// retryCount: retryCount, +// requestTimeout: requestTimeout, +// requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) .handleEvents( diff --git a/SessionNetworkingKit/Types/Network.swift b/SessionNetworkingKit/Types/Network.swift index 5913775129..08cf2916d6 100644 --- a/SessionNetworkingKit/Types/Network.swift +++ b/SessionNetworkingKit/Types/Network.swift @@ -22,10 +22,12 @@ public protocol NetworkType { func getRandomNodes(count: Int) -> AnyPublisher, Error> func send( - _ body: Data?, - to destination: Network.Destination, + endpoint: (any EndpointType), + destination: Network.Destination, + body: Data?, + category: Network.RequestCategory, requestTimeout: TimeInterval, - requestAndPathBuildTimeout: TimeInterval? + overallTimeout: TimeInterval? ) -> AnyPublisher<(ResponseInfoType, Data?), Error> func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error> @@ -128,7 +130,7 @@ public extension Network { static func preparedUpload( data: Data, - requestAndPathBuildTimeout: TimeInterval? = nil, + overallTimeout: TimeInterval? = nil, using dependencies: Dependencies ) throws -> PreparedRequest { return try PreparedRequest( @@ -139,11 +141,11 @@ public extension Network { x25519PublicKey: FileServer.fileServerPublicKey, fileName: nil ), - body: data + body: data, + requestTimeout: Network.fileUploadTimeout, + overallTimeout: overallTimeout ), responseType: FileUploadResponse.self, - requestTimeout: Network.fileUploadTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) } @@ -159,10 +161,10 @@ public extension Network { url: url, x25519PublicKey: FileServer.fileServerPublicKey, fileName: nil - ) + ), + requestTimeout: Network.fileUploadTimeout ), responseType: Data.self, - requestTimeout: Network.fileUploadTimeout, using: dependencies ) } diff --git a/SessionNetworkingKit/Types/PreparedRequest+Sending.swift b/SessionNetworkingKit/Types/PreparedRequest+Sending.swift index a49bae9cd0..295407765f 100644 --- a/SessionNetworkingKit/Types/PreparedRequest+Sending.swift +++ b/SessionNetworkingKit/Types/PreparedRequest+Sending.swift @@ -7,7 +7,14 @@ import SessionUtilitiesKit public extension Network.PreparedRequest { func send(using dependencies: Dependencies) -> AnyPublisher<(ResponseInfoType, R), Error> { return dependencies[singleton: .network] - .send(body, to: destination, requestTimeout: requestTimeout, requestAndPathBuildTimeout: requestAndPathBuildTimeout) + .send( + endpoint: endpoint, + destination: destination, + body: body, + category: category, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout + ) .decoded(with: self, using: dependencies) .retry(retryCount, using: dependencies) .handleEvents( diff --git a/SessionNetworkingKit/Types/PreparedRequest.swift b/SessionNetworkingKit/Types/PreparedRequest.swift index 67c1697324..39a5aaae30 100644 --- a/SessionNetworkingKit/Types/PreparedRequest.swift +++ b/SessionNetworkingKit/Types/PreparedRequest.swift @@ -15,14 +15,16 @@ public extension Network { fileprivate let convertedData: R } - public let body: Data? + public let endpoint: (any EndpointType) public let destination: Destination + public let body: Data? + public let category: RequestCategory + public let requestTimeout: TimeInterval + public let overallTimeout: TimeInterval? + public let retryCount: Int public let additionalSignatureData: Any? public let originalType: Decodable.Type public let responseType: R.Type - public let retryCount: Int - public let requestTimeout: TimeInterval - public let requestAndPathBuildTimeout: TimeInterval? public let cachedResponse: CachedResponse? fileprivate let responseConverter: ((ResponseInfoType, Any) throws -> R) public let subscriptionHandler: (() -> Void)? @@ -33,7 +35,6 @@ public extension Network { // The following types are needed for `BatchRequest` handling public let method: HTTPMethod public let path: String - public let endpoint: (any EndpointType) public let endpointName: String public let headers: [HTTPHeader: String] public let batchEndpoints: [any EndpointType] @@ -51,9 +52,9 @@ public extension Network { request: Request, responseType: R.Type, requireAllBatchResponses: Bool = true, - retryCount: Int = 0, - requestTimeout: TimeInterval = Network.defaultTimeout, - requestAndPathBuildTimeout: TimeInterval? = nil, +// retryCount: Int = 0, +// requestTimeout: TimeInterval = Network.defaultTimeout, +// requestAndPathBuildTimeout: TimeInterval? = nil, using dependencies: Dependencies ) throws where R: Decodable { try self.init( @@ -61,9 +62,9 @@ public extension Network { responseType: responseType, additionalSignatureData: NoSignature.null, requireAllBatchResponses: requireAllBatchResponses, - retryCount: retryCount, - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, +// retryCount: retryCount, +// requestTimeout: requestTimeout, +// requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) } @@ -73,9 +74,9 @@ public extension Network { responseType: R.Type, additionalSignatureData: S?, requireAllBatchResponses: Bool = true, - retryCount: Int = 0, - requestTimeout: TimeInterval = Network.defaultTimeout, - requestAndPathBuildTimeout: TimeInterval? = nil, +// retryCount: Int = 0, +// requestTimeout: TimeInterval = Network.defaultTimeout, +// requestAndPathBuildTimeout: TimeInterval? = nil, using dependencies: Dependencies ) throws where R: Decodable { let batchRequests: [Network.BatchRequest.Child]? = (request.body as? BatchRequestChildRetrievable)?.requests @@ -90,14 +91,16 @@ public extension Network { } .flatMap { $0 }) - self.body = try request.bodyData(using: dependencies) + self.endpoint = request.endpoint self.destination = request.destination + self.body = try request.bodyData(using: dependencies) + self.category = request.category + self.requestTimeout = request.requestTimeout + self.overallTimeout = request.overallTimeout + self.retryCount = request.retryCount self.additionalSignatureData = additionalSignatureData self.originalType = R.self self.responseType = responseType - self.retryCount = retryCount - self.requestTimeout = requestTimeout - self.requestAndPathBuildTimeout = requestAndPathBuildTimeout self.cachedResponse = nil // When we are making a batch request we also want to call though any sub request event @@ -228,7 +231,6 @@ public extension Network { // The following data is needed in this type for handling batch requests self.method = request.destination.method - self.endpoint = request.endpoint self.endpointName = E.name self.path = request.destination.urlPathAndParamsString self.headers = request.destination.headers @@ -271,14 +273,16 @@ public extension Network { } fileprivate init( - body: Data?, + endpoint: (any EndpointType), destination: Destination, + body: Data?, + category: Network.RequestCategory, + requestTimeout: TimeInterval, + overallTimeout: TimeInterval?, + retryCount: Int, additionalSignatureData: Any?, originalType: U.Type, responseType: R.Type, - retryCount: Int, - requestTimeout: TimeInterval, - requestAndPathBuildTimeout: TimeInterval?, cachedResponse: CachedResponse?, responseConverter: @escaping (ResponseInfoType, Any) throws -> R, subscriptionHandler: (() -> Void)?, @@ -286,7 +290,6 @@ public extension Network { completionEventHandler: ((Subscribers.Completion) -> Void)?, cancelEventHandler: (() -> Void)?, method: HTTPMethod, - endpoint: (any EndpointType), endpointName: String, headers: [HTTPHeader: String], path: String, @@ -300,14 +303,16 @@ public extension Network { b64: String?, bytes: [UInt8]? ) { - self.body = body + self.endpoint = endpoint self.destination = destination + self.body = body + self.category = category + self.requestTimeout = requestTimeout + self.overallTimeout = overallTimeout + self.retryCount = retryCount self.additionalSignatureData = additionalSignatureData self.originalType = originalType self.responseType = responseType - self.retryCount = retryCount - self.requestTimeout = requestTimeout - self.requestAndPathBuildTimeout = requestAndPathBuildTimeout self.cachedResponse = cachedResponse self.responseConverter = responseConverter self.subscriptionHandler = subscriptionHandler @@ -317,7 +322,6 @@ public extension Network { // The following data is needed in this type for handling batch requests self.method = method - self.endpoint = endpoint self.endpointName = endpointName self.headers = headers self.path = path @@ -453,14 +457,16 @@ public extension Network.PreparedRequest { let signedDestination: Network.Destination = try requestSigner(self, dependencies) return Network.PreparedRequest( - body: body, + endpoint: endpoint, destination: signedDestination, + body: body, + category: category, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout, + retryCount: retryCount, additionalSignatureData: additionalSignatureData, originalType: originalType, responseType: responseType, - retryCount: retryCount, - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, cachedResponse: cachedResponse, responseConverter: responseConverter, subscriptionHandler: subscriptionHandler, @@ -468,7 +474,6 @@ public extension Network.PreparedRequest { completionEventHandler: completionEventHandler, cancelEventHandler: cancelEventHandler, method: method, - endpoint: endpoint, endpointName: endpointName, headers: signedDestination.headers, path: path, @@ -500,14 +505,16 @@ public extension Network.PreparedRequest { } return Network.PreparedRequest( - body: body, + endpoint: endpoint, destination: destination, + body: body, + category: category, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout, + retryCount: retryCount, additionalSignatureData: additionalSignatureData, originalType: originalType, responseType: O.self, - retryCount: retryCount, - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, cachedResponse: cachedResponse.map { data in (try? responseConverter(data.info, data.convertedData)) .map { convertedData in @@ -536,7 +543,6 @@ public extension Network.PreparedRequest { completionEventHandler: completionEventHandler, cancelEventHandler: cancelEventHandler, method: method, - endpoint: endpoint, endpointName: endpointName, headers: headers, path: path, @@ -612,14 +618,16 @@ public extension Network.PreparedRequest { }() return Network.PreparedRequest( - body: body, + endpoint: endpoint, destination: destination, + body: body, + category: category, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout, + retryCount: retryCount, additionalSignatureData: additionalSignatureData, originalType: originalType, responseType: responseType, - retryCount: retryCount, - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, cachedResponse: cachedResponse, responseConverter: responseConverter, subscriptionHandler: subscriptionHandler, @@ -627,7 +635,6 @@ public extension Network.PreparedRequest { completionEventHandler: completionEventHandler, cancelEventHandler: cancelEventHandler, method: method, - endpoint: endpoint, endpointName: endpointName, headers: headers, path: path, @@ -653,17 +660,19 @@ public extension Network.PreparedRequest { using dependencies: Dependencies ) throws -> Network.PreparedRequest where R: Codable { return Network.PreparedRequest( - body: nil, + endpoint: endpoint, destination: try .cached( response: cachedResponse, using: dependencies ), + body: nil, + category: .standard, + requestTimeout: 0, + overallTimeout: nil, + retryCount: 0, additionalSignatureData: nil, originalType: R.self, responseType: R.self, - retryCount: 0, - requestTimeout: 0, - requestAndPathBuildTimeout: nil, cachedResponse: Network.PreparedRequest.CachedResponse( info: Network.ResponseInfo(code: 0, headers: [:]), originalData: cachedResponse, @@ -675,7 +684,6 @@ public extension Network.PreparedRequest { completionEventHandler: nil, cancelEventHandler: nil, method: .get, - endpoint: endpoint, endpointName: E.name, headers: [:], path: "", diff --git a/SessionNetworkingKit/Types/Request.swift b/SessionNetworkingKit/Types/Request.swift index f25efccaa9..4b08a0e563 100644 --- a/SessionNetworkingKit/Types/Request.swift +++ b/SessionNetworkingKit/Types/Request.swift @@ -40,16 +40,29 @@ public struct Request { /// is custom handling for certain data types public let body: T? + public let category: Network.RequestCategory + public let requestTimeout: TimeInterval + public let overallTimeout: TimeInterval? + public let retryCount: Int + // MARK: - Initialization public init( endpoint: Endpoint, destination: Network.Destination, - body: T? = nil + body: T? = nil, + category: Network.RequestCategory = .standard, + requestTimeout: TimeInterval = Network.defaultTimeout, + overallTimeout: TimeInterval? = nil, + retryCount: Int = 0 ) throws { self.endpoint = endpoint self.destination = try destination.withGeneratedUrl(for: endpoint) self.body = body + self.category = category + self.requestTimeout = requestTimeout + self.overallTimeout = overallTimeout + self.retryCount = retryCount } // MARK: - Internal Methods @@ -80,3 +93,24 @@ public struct Request { } } } + +public extension Request where T == NoBody { + init( + endpoint: Endpoint, + destination: Network.Destination, + category: Network.RequestCategory = .standard, + requestTimeout: TimeInterval = Network.defaultTimeout, + overallTimeout: TimeInterval? = nil, + retryCount: Int = 0 + ) throws { + self = try Request( + endpoint: endpoint, + destination: destination, + body: nil, + category: category, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout, + retryCount: retryCount + ) + } +} diff --git a/SessionNetworkingKit/Types/RequestCategory.swift b/SessionNetworkingKit/Types/RequestCategory.swift index e69de29bb2..c3bf81830c 100644 --- a/SessionNetworkingKit/Types/RequestCategory.swift +++ b/SessionNetworkingKit/Types/RequestCategory.swift @@ -0,0 +1,31 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtil + +public extension Network { + enum RequestCategory { + case standard + case upload + case download + } +} + +extension Network.RequestCategory { + public var libSessionValue: SESSION_NETWORK_REQUEST_CATEGORY { + switch self { + case .standard: return SESSION_NETWORK_REQUEST_CATEGORY_STANDARD + case .upload: return SESSION_NETWORK_REQUEST_CATEGORY_UPLOAD + case .download: return SESSION_NETWORK_REQUEST_CATEGORY_DOWNLOAD + } + } + + public init(_ libSessionValue: SESSION_NETWORK_REQUEST_CATEGORY) { + switch libSessionValue { + case SESSION_NETWORK_REQUEST_CATEGORY_STANDARD: self = .standard + case SESSION_NETWORK_REQUEST_CATEGORY_UPLOAD: self = .upload + case SESSION_NETWORK_REQUEST_CATEGORY_DOWNLOAD: self = .download + default: self = .standard + } + } +} diff --git a/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift index a1ad6f438b..b62d147f7a 100644 --- a/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift +++ b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift @@ -31,8 +31,6 @@ class PreparedRequestSendingSpec: QuickSpec { return try! Network.PreparedRequest( request: request, responseType: Int.self, - retryCount: 0, - requestTimeout: 10, using: dependencies ) }() @@ -45,7 +43,16 @@ class PreparedRequestSendingSpec: QuickSpec { context("when sending") { beforeEach { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.response(with: 1)) } @@ -322,15 +329,11 @@ class PreparedRequestSendingSpec: QuickSpec { try! Network.PreparedRequest( request: subRequest1, responseType: TestType.self, - retryCount: 0, - requestTimeout: 10, using: dependencies ), try! Network.PreparedRequest( request: subRequest2, responseType: TestType.self, - retryCount: 0, - requestTimeout: 10, using: dependencies ) ] @@ -340,8 +343,6 @@ class PreparedRequestSendingSpec: QuickSpec { return try! Network.PreparedRequest( request: request, responseType: Network.BatchResponseMap.self, - retryCount: 0, - requestTimeout: 10, using: dependencies ) }() @@ -352,7 +353,16 @@ class PreparedRequestSendingSpec: QuickSpec { beforeEach { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn( MockNetwork.batchResponseData(with: [ (endpoint: TestEndpoint.endpoint1, data: TestType.mockBatchSubResponse()), @@ -403,16 +413,12 @@ class PreparedRequestSendingSpec: QuickSpec { try! Network.PreparedRequest( request: subRequest1, responseType: TestType.self, - retryCount: 0, - requestTimeout: 10, using: dependencies ) .map { _, _ in "Test" }, try! Network.PreparedRequest( request: subRequest2, responseType: TestType.self, - retryCount: 0, - requestTimeout: 10, using: dependencies ) ] @@ -422,8 +428,6 @@ class PreparedRequestSendingSpec: QuickSpec { return try! Network.PreparedRequest( request: request, responseType: Network.BatchResponseMap.self, - retryCount: 0, - requestTimeout: 10, using: dependencies ) }() @@ -472,8 +476,6 @@ class PreparedRequestSendingSpec: QuickSpec { try! Network.PreparedRequest( request: subRequest1, responseType: TestType.self, - retryCount: 0, - requestTimeout: 10, using: dependencies ) .handleEvents( @@ -482,8 +484,6 @@ class PreparedRequestSendingSpec: QuickSpec { try! Network.PreparedRequest( request: subRequest2, responseType: TestType.self, - retryCount: 0, - requestTimeout: 10, using: dependencies ) ] @@ -493,8 +493,6 @@ class PreparedRequestSendingSpec: QuickSpec { return try! Network.PreparedRequest( request: request, responseType: Network.BatchResponseMap.self, - retryCount: 0, - requestTimeout: 10, using: dependencies ) }() diff --git a/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift b/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift index 7b949b5fde..94bb2b4d73 100644 --- a/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift +++ b/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift @@ -36,12 +36,15 @@ class PreparedRequestSpec: QuickSpec { ], x25519PublicKey: "" ), - body: nil + body: nil, + category: .upload, + requestTimeout: 123, + overallTimeout: 1234, + retryCount: 3 ) preparedRequest = try! Network.PreparedRequest( request: request, responseType: TestType.self, - requestTimeout: 10, using: dependencies ) @@ -51,6 +54,10 @@ class PreparedRequestSpec: QuickSpec { "TestCustomHeader": "TestCustom", HTTPHeader.testHeader: "Test" ])) + expect(preparedRequest.category).to(equal(.upload)) + expect(preparedRequest.requestTimeout).to(equal(123)) + expect(preparedRequest.overallTimeout).to(equal(1234)) + expect(preparedRequest.retryCount).to(equal(3)) } // MARK: -- does not strip excluded subrequest headers @@ -72,7 +79,6 @@ class PreparedRequestSpec: QuickSpec { preparedRequest = try! Network.PreparedRequest( request: request, responseType: TestType.self, - requestTimeout: 10, using: dependencies ) diff --git a/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift b/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift index d99134ceba..2aac55bcf0 100644 --- a/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift +++ b/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift @@ -39,3 +39,7 @@ extension Network.Destination: Mocked { x25519PublicKey: "" ).withGeneratedUrl(for: MockEndpoint.mock) } + +extension Network.RequestCategory: Mocked { + static var mock: Network.RequestCategory = .standard +} diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index f39b6b65a3..c048ded97c 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -86,9 +86,12 @@ class OnboardingSpec: AsyncSpec { initialSetup: { network in network.when { $0.getSwarm(for: .any) }.thenReturn([ LibSession.Snode( + ed25519PubkeyHex: "1234", ip: "1.2.3.4", + httpsPort: 1233, quicPort: 1234, - ed25519PubkeyHex: "1234" + version: "2.11.0", + swarmId: 1 ) ]) @@ -108,7 +111,16 @@ class OnboardingSpec: AsyncSpec { ) network - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.mock, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.batchResponseData( with: [ ( @@ -453,17 +465,22 @@ class OnboardingSpec: AsyncSpec { await expect(mockNetwork) .toEventually(call(.exactly(times: 1), matchingParameters: .atLeast(3)) { $0.send( - Data(base64Encoded: base64EncodedDataString), - to: Network.Destination.snode( + endpoint: MockEndpoint.mock, + destination: Network.Destination.snode( LibSession.Snode( + ed25519PubkeyHex: "", ip: "1.2.3.4", + httpsPort: 1233, quicPort: 1234, - ed25519PubkeyHex: "" + version: "2.11.0", + swarmId: 1 ), swarmPublicKey: "0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b" ), + body: Data(base64Encoded: base64EncodedDataString), + category: .standard, requestTimeout: 10, - requestAndPathBuildTimeout: nil + overallTimeout: nil ) }) } From cb6b5aa4f2ea7a6b258fffcdd81fd2153b4d8e94 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 8 Aug 2025 16:16:54 +1000 Subject: [PATCH 06/59] Fixed a few issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fixed some duplicate content in Logging • Fixed broken service node batch requests • Fixed a crash which could occur when using the storage publisher functions --- Scripts/build_libSession_util.sh | 1 - Session.xcodeproj/project.pbxproj | 10 +--------- .../LibSession/LibSession+Networking.swift | 3 +-- SessionNetworkingKit/Types/PreparedRequest.swift | 8 ++++---- SessionUtilitiesKit/Database/Storage.swift | 8 +++++--- SessionUtilitiesKit/General/Logging.swift | 3 +-- 6 files changed, 12 insertions(+), 21 deletions(-) diff --git a/Scripts/build_libSession_util.sh b/Scripts/build_libSession_util.sh index f20b3527e2..4b5131ee0c 100755 --- a/Scripts/build_libSession_util.sh +++ b/Scripts/build_libSession_util.sh @@ -338,7 +338,6 @@ if [ "${REQUIRES_BUILD}" == 1 ]; then echo "- Touching timestamp file to signal update to Xcode" touch "${BUILT_LIB_FINAL_TIMESTAMP_FILE}" - cp "${BUILT_LIB_FINAL_TIMESTAMP_FILE}" "${SPM_TIMESTAMP_FILE}" echo "- Build complete" fi diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 246d109eef..3a44ca63e2 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -990,6 +990,7 @@ FDD20C162A09E64A003898FB /* GetExpiriesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C152A09E64A003898FB /* GetExpiriesRequest.swift */; }; FDD20C182A09E7D3003898FB /* GetExpiriesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C172A09E7D3003898FB /* GetExpiriesResponse.swift */; }; FDD20C1A2A0A03AC003898FB /* DeleteInboxResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C192A0A03AC003898FB /* DeleteInboxResponse.swift */; }; + FDD23ADE2E44501E0057E853 /* RequestCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD23ADD2E44501B0057E853 /* RequestCategory.swift */; }; FDD23ADF2E457CAA0057E853 /* _016_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _016_ThemePreferences.swift */; }; FDD23AE02E457CD40057E853 /* _004_SNK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _004_SNK_InitialSetupMigration.swift */; }; FDD23AE12E457CDE0057E853 /* _005_SNK_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _005_SNK_SetupStandardJobs.swift */; }; @@ -1008,7 +1009,6 @@ FDD23AEE2E459E470057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C927F546D900122BE0 /* _001_SUK_InitialSetupMigration.swift */; }; FDD23AEF2E459EC90057E853 /* _012_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _012_AddJobPriority.swift */; }; FDD23AF02E459EDD0057E853 /* _020_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */; }; - FDD23ADE2E44501E0057E853 /* RequestCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD23ADD2E44501B0057E853 /* RequestCategory.swift */; }; FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; }; FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; }; @@ -4235,18 +4235,10 @@ isa = PBXGroup; children = ( FD78EA082DDFE45000D55B50 /* Convenience */, - FD37E9F728A5F143003AE748 /* Migrations */, ); path = Database; sourceTree = ""; }; - FD37E9F728A5F143003AE748 /* Migrations */ = { - isa = PBXGroup; - children = ( - ); - path = Migrations; - sourceTree = ""; - }; FD37EA1228AB3F60003AE748 /* Database */ = { isa = PBXGroup; children = ( diff --git a/SessionNetworkingKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift index 020fc0119e..7e5d318a60 100644 --- a/SessionNetworkingKit/LibSession/LibSession+Networking.swift +++ b/SessionNetworkingKit/LibSession/LibSession+Networking.swift @@ -268,7 +268,6 @@ class LibSessionNetwork: NetworkType { switch destination { case .snode(let snode, _): try LibSessionNetwork.withSnodeRequestParams(request, snode) { paramsPtr in -// var mutableParams = params session_network_send_request(network, paramsPtr, cCallback, ctx) } @@ -990,13 +989,13 @@ public extension LibSession { var error: [CChar] = [CChar](repeating: 0, count: 256) var network: UnsafeMutablePointer? var config: session_network_config = session_network_config_default() + config.cache_refresh_using_legacy_endpoint = true if dependencies[feature: .serviceNetwork] == .testnet { config.netid = SESSION_NETWORK_TESTNET config.enforce_subnet_diversity = false // On testnet we can't do this as nodes share IPs } - let result: Result = cCachePath.withUnsafeBufferPointer { cachePtr in config.cache_dir = cachePtr.baseAddress diff --git a/SessionNetworkingKit/Types/PreparedRequest.swift b/SessionNetworkingKit/Types/PreparedRequest.swift index 39a5aaae30..c737132930 100644 --- a/SessionNetworkingKit/Types/PreparedRequest.swift +++ b/SessionNetworkingKit/Types/PreparedRequest.swift @@ -417,13 +417,13 @@ extension Network.PreparedRequest: ErasedPreparedRequest { } public func encodeForBatchRequest(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: Network.BatchRequest.Child.CodingKeys.self) + switch batchRequestVariant { case .unsupported: Log.critical("Attempted to encode unsupported request type \(endpointName) as a batch subrequest") case .sogs: - var container: KeyedEncodingContainer = encoder.container(keyedBy: Network.BatchRequest.Child.CodingKeys.self) - // Exclude request signature headers (not used for sub-requests) let excludedSubRequestHeaders: [HTTPHeader] = excludedSubRequestHeaders let batchRequestHeaders: [HTTPHeader: String] = headers @@ -440,9 +440,9 @@ extension Network.PreparedRequest: ErasedPreparedRequest { try container.encodeIfPresent(bytes, forKey: .bytes) case .storageServer: - var container: SingleValueEncodingContainer = encoder.singleValueContainer() + try container.encode(endpoint.path, forKey: .method) + try jsonKeyedBodyEncoder?(&container, .params) - try jsonBodyEncoder?(&container) } } } diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index e9b4cfbc4f..7d80293638 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -1319,7 +1319,7 @@ public protocol IdentifiableTransactionObserver: TransactionObserver { private extension Storage { actor PublisherBridge { - private weak var subject: PassthroughSubject? + private var subject: PassthroughSubject? private var isFinished: Bool = false init(subject: PassthroughSubject) { @@ -1329,7 +1329,8 @@ private extension Storage { func send(result: Result, info: CallInfo) { guard !isFinished, let subject = self.subject else { return } - isFinished = true + self.isFinished = true + self.subject = nil switch result { case .success(let value): @@ -1346,7 +1347,8 @@ private extension Storage { func cancel() { guard !isFinished else { return } - isFinished = true + self.isFinished = true + self.subject = nil } } } diff --git a/SessionUtilitiesKit/General/Logging.swift b/SessionUtilitiesKit/General/Logging.swift index 9bd2646f9e..67b529d8ec 100644 --- a/SessionUtilitiesKit/General/Logging.swift +++ b/SessionUtilitiesKit/General/Logging.swift @@ -730,8 +730,7 @@ public actor Logger: LoggerType { }() /// Clean up the message if needed (replace double periods with single, trim whitespace, truncate pubkeys) - let cleanedMessage: String = logPrefix - .appending(message) + let cleanedMessage: String = message .replacingOccurrences(of: "...", with: "|||") .replacingOccurrences(of: "..", with: ".") .replacingOccurrences(of: "|||", with: "...") From ad42281c0a96cc6484289307315d432fe1c0345c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 22 Aug 2025 16:00:55 +1000 Subject: [PATCH 07/59] Made a bunch of QoL changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added some convenience functions • Removed the database requirement to get some AuthenticationMethods --- .../Calls/Call Management/SessionCall.swift | 2 +- Session/Calls/WebRTC/WebRTCSession.swift | 60 ++++---- Session/Meta/Translations/InfoPlist.xcstrings | 52 ++++++- Session/Settings/NukeDataModal.swift | 32 ++--- .../Jobs/ExpirationUpdateJob.swift | 17 +-- .../Jobs/GetExpirationJob.swift | 5 +- .../Jobs/GroupInviteMemberJob.swift | 24 ++-- .../Jobs/GroupLeavingJob.swift | 2 +- .../Jobs/GroupPromoteMemberJob.swift | 13 +- .../Jobs/SendReadReceiptsJob.swift | 12 +- .../LibSession+GroupInfo.swift | 1 - .../LibSession+UserGroups.swift | 15 ++ .../LibSession+SessionMessagingKit.swift | 6 + .../LibSession/Types/GroupAuthData.swift | 8 ++ .../MessageReceiver+Calls.swift | 2 +- .../MessageReceiver+Groups.swift | 30 ++-- .../MessageReceiver+UnsendRequests.swift | 18 +-- .../Models/SubscribeResponse.swift | 6 +- .../Models/UnsubscribeResponse.swift | 6 +- .../MessageViewModel+DeletionActions.swift | 1 - .../Authentication+SessionMessagingKit.swift | 18 +-- .../Combine/Publisher+Utilities.swift | 12 ++ .../Database/Models/KeyValueStore.swift | 131 +++++++++++------- .../Dependency Injection/Dependencies.swift | 51 ++++++- .../LibSession/Types/ObservingDatabase.swift | 2 +- .../Types/CurrentValueAsyncStream.swift | 49 ++++--- .../Types/StreamLifecycleManager.swift | 52 +++++++ .../Utilities/Task+Utilities.swift | 15 ++ 28 files changed, 449 insertions(+), 193 deletions(-) create mode 100644 SessionMessagingKit/LibSession/Types/GroupAuthData.swift create mode 100644 SessionUtilitiesKit/Types/StreamLifecycleManager.swift diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index f08656626c..f7a9a64ec7 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -248,7 +248,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { message: message, threadId: thread.id, interactionId: interaction?.id, - authMethod: try Authentication.with(db, swarmPublicKey: thread.id, using: dependencies) + authMethod: try Authentication.with(swarmPublicKey: thread.id, using: dependencies) ) .retry(5) // Start the timeout timer for the call diff --git a/Session/Calls/WebRTC/WebRTCSession.swift b/Session/Calls/WebRTC/WebRTCSession.swift index d579d8a1a5..aa36ca7f72 100644 --- a/Session/Calls/WebRTC/WebRTCSession.swift +++ b/Session/Calls/WebRTC/WebRTCSession.swift @@ -181,15 +181,17 @@ public final class WebRTCSession: NSObject, RTCPeerConnectionDelegate { } dependencies[singleton: .storage] - .writePublisher { db -> (AuthenticationMethod, DisappearingMessagesConfiguration?) in - ( - try Authentication.with(db, swarmPublicKey: thread.id, using: dependencies), - try DisappearingMessagesConfiguration.fetchOne(db, id: thread.id) - ) + .writePublisher { db -> DisappearingMessagesConfiguration? in + try DisappearingMessagesConfiguration.fetchOne(db, id: thread.id) } .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .tryFlatMap { authMethod, disappearingMessagesConfiguration in - try MessageSender.preparedSend( + .tryFlatMap { disappearingMessagesConfiguration in + let authMethod: AuthenticationMethod = try Authentication.with( + swarmPublicKey: thread.id, + using: dependencies + ) + + return try MessageSender.preparedSend( message: CallMessage( uuid: uuid, kind: .offer, @@ -226,7 +228,7 @@ public final class WebRTCSession: NSObject, RTCPeerConnectionDelegate { let mediaConstraints: RTCMediaConstraints = mediaConstraints(false) return dependencies[singleton: .storage] - .readPublisher { [dependencies] db -> (AuthenticationMethod, DisappearingMessagesConfiguration?) in + .readPublisher { db -> DisappearingMessagesConfiguration? in /// Ensure a thread exists for the `sessionId` and that it's a `contact` thread guard SessionThread @@ -235,13 +237,15 @@ public final class WebRTCSession: NSObject, RTCPeerConnectionDelegate { .isNotEmpty(db) else { throw WebRTCSessionError.noThread } - return ( - try Authentication.with(db, swarmPublicKey: sessionId, using: dependencies), - try DisappearingMessagesConfiguration.fetchOne(db, id: sessionId) - ) + return try DisappearingMessagesConfiguration.fetchOne(db, id: sessionId) } - .flatMap { [weak self, dependencies] authMethod, disappearingMessagesConfiguration in - Future { resolver in + .tryFlatMap { [weak self, dependencies] disappearingMessagesConfiguration in + let authMethod: AuthenticationMethod = try Authentication.with( + swarmPublicKey: sessionId, + using: dependencies + ) + + return Future { resolver in self?.peerConnection?.answer(for: mediaConstraints) { [weak self] sdp, error in if let error = error { resolver(Result.failure(error)) @@ -313,7 +317,7 @@ public final class WebRTCSession: NSObject, RTCPeerConnectionDelegate { self.queuedICECandidates.removeAll() return dependencies[singleton: .storage] - .readPublisher { [dependencies] db -> (AuthenticationMethod, DisappearingMessagesConfiguration?) in + .readPublisher { db -> DisappearingMessagesConfiguration? in /// Ensure a thread exists for the `sessionId` and that it's a `contact` thread guard SessionThread @@ -322,13 +326,15 @@ public final class WebRTCSession: NSObject, RTCPeerConnectionDelegate { .isNotEmpty(db) else { throw WebRTCSessionError.noThread } - return ( - try Authentication.with(db, swarmPublicKey: contactSessionId, using: dependencies), - try DisappearingMessagesConfiguration.fetchOne(db, id: contactSessionId) - ) + return try DisappearingMessagesConfiguration.fetchOne(db, id: contactSessionId) } .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .tryFlatMap { [dependencies] authMethod, disappearingMessagesConfiguration in + .tryFlatMap { [dependencies] disappearingMessagesConfiguration in + let authMethod: AuthenticationMethod = try Authentication.with( + swarmPublicKey: contactSessionId, + using: dependencies + ) + Log.info(.calls, "Batch sending \(candidates.count) ICE candidates.") return try MessageSender @@ -368,7 +374,7 @@ public final class WebRTCSession: NSObject, RTCPeerConnectionDelegate { public func endCall(with sessionId: String) { return dependencies[singleton: .storage] - .readPublisher { [dependencies] db -> (AuthenticationMethod, DisappearingMessagesConfiguration?) in + .readPublisher { db -> DisappearingMessagesConfiguration? in /// Ensure a thread exists for the `sessionId` and that it's a `contact` thread guard SessionThread @@ -377,13 +383,15 @@ public final class WebRTCSession: NSObject, RTCPeerConnectionDelegate { .isNotEmpty(db) else { throw WebRTCSessionError.noThread } - return ( - try Authentication.with(db, swarmPublicKey: sessionId, using: dependencies), - try DisappearingMessagesConfiguration.fetchOne(db, id: sessionId) - ) + return try DisappearingMessagesConfiguration.fetchOne(db, id: sessionId) } .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .tryFlatMap { [dependencies, uuid] authMethod, disappearingMessagesConfiguration in + .tryFlatMap { [dependencies, uuid] disappearingMessagesConfiguration in + let authMethod: AuthenticationMethod = try Authentication.with( + swarmPublicKey: sessionId, + using: dependencies + ) + Log.info(.calls, "Sending end call message.") return try MessageSender diff --git a/Session/Meta/Translations/InfoPlist.xcstrings b/Session/Meta/Translations/InfoPlist.xcstrings index 8199912edc..3d97c3284c 100644 --- a/Session/Meta/Translations/InfoPlist.xcstrings +++ b/Session/Meta/Translations/InfoPlist.xcstrings @@ -1507,7 +1507,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Session, səsli və görüntülü zənglər edə bilmək üçün daxili şəbəkəyə müraciət etməlidir." + "value" : "Session, səsli və görüntülü zənglər edə bilmək üçün lokal şəbəkəyə erişməlidir." } }, "ca" : { @@ -1546,6 +1546,18 @@ "value" : "Session bezonas aliron al loka reto por fari voĉajn kaj video vokojn." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necesita acceso a la red local para realizar llamadas de voz y video." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necesita acceso a la red local para realizar llamadas de voz y video." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -1564,6 +1576,18 @@ "value" : "A(z) Session alkalmazásnak hozzáférésre van szüksége a helyi hálózathoz a hang- és videohívások indításához." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necessita dell'accesso alla rete locale per effettuare chiamate vocali e video." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session は音声・ビデオ通話を行うためにローカルネットワークへのアクセスが必要です。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -1582,6 +1606,18 @@ "value" : "Session potrzebuje dostępu do sieci lokalnej, aby wykonywać połączenia głosowe i wideo." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session precisa de acesso à rede local para efetuar chamadas de voz e vídeo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session are nevoie de acces la rețeaua locală pentru a efectua apeluri vocale și video." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -1594,6 +1630,12 @@ "value" : "Session behöver åtkomst till det lokala nätverket för att kunna ringa röst och videosamtal." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session uygulamasının sesli ve görüntülü arama yapabilmesi için yerel ağa erişmesi gerekiyor." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -1611,6 +1653,12 @@ "state" : "translated", "value" : "Session需要访问本地网络才能进行语音和视频通话。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session 需要存取本地網路以進行語音與視訊通話。" + } } } }, @@ -2117,7 +2165,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Session qoşmaları və medianı saxlamaq üçün anbara müraciət etməlidir." + "value" : "Session qoşmaları və medianı saxlamaq üçün anbara erişməlidir." } }, "bal" : { diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index 316a158c0c..3561029208 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -178,25 +178,23 @@ final class NukeDataModal: Modal { ModalActivityIndicatorViewController .present(fromViewController: presentedViewController, canCancel: false) { [weak self, dependencies] _ in dependencies[singleton: .storage] - .readPublisher { db -> (AuthenticationMethod, [AuthenticationMethod]) in - ( - try Authentication.with( - db, - swarmPublicKey: dependencies[cache: .general].sessionId.hexString, - using: dependencies - ), - try OpenGroup - .filter(OpenGroup.Columns.isActive == true) - .select(.server) - .distinct() - .asRequest(of: String.self) - .fetchSet(db) - .map { try Authentication.with(db, server: $0, using: dependencies) } - ) + .readPublisher { db -> [AuthenticationMethod] in + try OpenGroup + .filter(OpenGroup.Columns.isActive == true) + .select(.server) + .distinct() + .asRequest(of: String.self) + .fetchSet(db) + .map { try Authentication.with(db, server: $0, using: dependencies) } } .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) - .tryFlatMap { (userAuth: AuthenticationMethod, communityAuth: [AuthenticationMethod]) -> AnyPublisher<(AuthenticationMethod, [String]), Error> in - Publishers + .tryFlatMap { (communityAuth: [AuthenticationMethod]) -> AnyPublisher<(AuthenticationMethod, [String]), Error> in + let userAuth: AuthenticationMethod = try Authentication.with( + swarmPublicKey: dependencies[cache: .general].sessionId.hexString, + using: dependencies + ) + + return Publishers .MergeMany( try communityAuth.compactMap { authMethod in switch authMethod.info { diff --git a/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift b/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift index 03e602be04..fc2ac1da57 100644 --- a/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift +++ b/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift @@ -24,18 +24,19 @@ public enum ExpirationUpdateJob: JobExecutor { let details: Details = try? JSONDecoder(using: dependencies).decode(Details.self, from: detailsData) else { return failure(job, JobRunnerError.missingRequiredDetails, true) } - dependencies[singleton: .storage] - .readPublisher { db in - try SnodeAPI + AnyPublisher + .lazy { + let authMethod: AuthenticationMethod = try Authentication.with( + swarmPublicKey: dependencies[cache: .general].sessionId.hexString, + using: dependencies + ) + + return try SnodeAPI .preparedUpdateExpiry( serverHashes: details.serverHashes, updatedExpiryMs: details.expirationTimestampMs, shortenOnly: true, - authMethod: try Authentication.with( - db, - swarmPublicKey: dependencies[cache: .general].sessionId.hexString, - using: dependencies - ), + authMethod: authMethod, using: dependencies ) } diff --git a/SessionMessagingKit/Jobs/GetExpirationJob.swift b/SessionMessagingKit/Jobs/GetExpirationJob.swift index a70bfc5d0a..67a5a17f25 100644 --- a/SessionMessagingKit/Jobs/GetExpirationJob.swift +++ b/SessionMessagingKit/Jobs/GetExpirationJob.swift @@ -37,12 +37,11 @@ public enum GetExpirationJob: JobExecutor { return success(job, false) } - dependencies[singleton: .storage] - .readPublisher { db -> Network.PreparedRequest in + AnyPublisher + .lazy { try SnodeAPI.preparedGetExpiries( of: expirationInfo.map { $0.key }, authMethod: try Authentication.with( - db, swarmPublicKey: dependencies[cache: .general].sessionId.hexString, using: dependencies ), diff --git a/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift b/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift index e652f75f13..6af0899a4b 100644 --- a/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift +++ b/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift @@ -46,7 +46,7 @@ public enum GroupInviteMemberJob: JobExecutor { /// Perform the actual message sending dependencies[singleton: .storage] - .writePublisher { db -> (AuthenticationMethod, AuthenticationMethod) in + .writePublisher { db in _ = try? GroupMember .filter(GroupMember.Columns.groupId == threadId) .filter(GroupMember.Columns.profileId == details.memberSessionIdHexString) @@ -56,18 +56,18 @@ public enum GroupInviteMemberJob: JobExecutor { GroupMember.Columns.roleStatus.set(to: GroupMember.RoleStatus.sending), using: dependencies ) - - return ( - try Authentication.with(db, swarmPublicKey: threadId, using: dependencies), - try Authentication.with( - db, - swarmPublicKey: details.memberSessionIdHexString, - using: dependencies - ) - ) } - .tryFlatMap { groupAuthMethod, memberAuthMethod -> AnyPublisher<(ResponseInfoType, Message), Error> in - try MessageSender.preparedSend( + .tryFlatMap { _ -> AnyPublisher<(ResponseInfoType, Message), Error> in + let groupAuthMethod: AuthenticationMethod = try Authentication.with( + swarmPublicKey: threadId, + using: dependencies + ) + let memberAuthMethod: AuthenticationMethod = try Authentication.with( + swarmPublicKey: details.memberSessionIdHexString, + using: dependencies + ) + + return try MessageSender.preparedSend( message: try GroupUpdateInviteMessage( inviteeSessionIdHexString: details.memberSessionIdHexString, groupSessionId: SessionId(.group, hex: threadId), diff --git a/SessionMessagingKit/Jobs/GroupLeavingJob.swift b/SessionMessagingKit/Jobs/GroupLeavingJob.swift index 92448ad51b..39fcd136d4 100644 --- a/SessionMessagingKit/Jobs/GroupLeavingJob.swift +++ b/SessionMessagingKit/Jobs/GroupLeavingJob.swift @@ -69,7 +69,7 @@ public enum GroupLeavingJob: JobExecutor { switch (finalBehaviour, isAdminUser, (isAdminUser && numAdminUsers == 1)) { case (.leave, _, false): let disappearingConfig: DisappearingMessagesConfiguration? = try? DisappearingMessagesConfiguration.fetchOne(db, id: threadId) - let authMethod: AuthenticationMethod = try Authentication.with(db, swarmPublicKey: threadId, using: dependencies) + let authMethod: AuthenticationMethod = try Authentication.with(swarmPublicKey: threadId, using: dependencies) return .sendLeaveMessage(authMethod, disappearingConfig) diff --git a/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift b/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift index d46639ac78..ff466ec940 100644 --- a/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift +++ b/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift @@ -61,7 +61,7 @@ public enum GroupPromoteMemberJob: JobExecutor { /// Perform the actual message sending dependencies[singleton: .storage] - .writePublisher { db -> AuthenticationMethod in + .writePublisher { db in _ = try? GroupMember .filter(GroupMember.Columns.groupId == threadId) .filter(GroupMember.Columns.profileId == details.memberSessionIdHexString) @@ -71,11 +71,14 @@ public enum GroupPromoteMemberJob: JobExecutor { GroupMember.Columns.roleStatus.set(to: GroupMember.RoleStatus.sending), using: dependencies ) - - return try Authentication.with(db, swarmPublicKey: details.memberSessionIdHexString, using: dependencies) } - .tryFlatMap { authMethod -> AnyPublisher<(ResponseInfoType, Message), Error> in - try MessageSender.preparedSend( + .tryFlatMap { _ -> AnyPublisher<(ResponseInfoType, Message), Error> in + let authMethod: AuthenticationMethod = try Authentication.with( + swarmPublicKey: details.memberSessionIdHexString, + using: dependencies + ) + + return try MessageSender.preparedSend( message: message, to: .contact(publicKey: details.memberSessionIdHexString), namespace: .default, diff --git a/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift b/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift index 9196301bfc..b31db68374 100644 --- a/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift +++ b/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift @@ -33,10 +33,14 @@ public enum SendReadReceiptsJob: JobExecutor { return success(job, true) } - dependencies[singleton: .storage] - .readPublisher { db in try Authentication.with(db, swarmPublicKey: threadId, using: dependencies) } - .tryFlatMap { authMethod -> AnyPublisher<(ResponseInfoType, Message), Error> in - try MessageSender.preparedSend( + AnyPublisher + .lazy { () -> AnyPublisher<(ResponseInfoType, Message), Error> in + let authMethod: AuthenticationMethod = try Authentication.with( + swarmPublicKey: threadId, + using: dependencies + ) + + return try MessageSender.preparedSend( message: ReadReceipt( timestamps: details.timestampMsValues.map { UInt64($0) } ), diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift index eec8fbd631..3facf3bfd0 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift @@ -289,7 +289,6 @@ internal extension LibSessionCacheType { // send a fire-and-forget API call to delete the messages from the swarm if isAdmin && !messageHashesToDelete.isEmpty { (try? Authentication.with( - db, swarmPublicKey: groupSessionId.hexString, using: dependencies )).map { authMethod in diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift index c9724f15b1..4c60e4faae 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift @@ -1043,6 +1043,21 @@ public extension LibSession.Cache { return ugroups_group_is_destroyed(&userGroup) } + + func authData(groupSessionId: SessionId) -> GroupAuthData { + var group: ugroups_group_info = ugroups_group_info() + + guard + case .userGroups(let conf) = config(for: .userGroups, sessionId: userSessionId), + var cGroupId: [CChar] = groupSessionId.hexString.cString(using: .utf8), + user_groups_get_group(conf, &group, &cGroupId) + else { return GroupAuthData(groupIdentityPrivateKey: nil, authData: nil) } + + return GroupAuthData( + groupIdentityPrivateKey: (!group.have_secretkey ? nil : group.get(\.secretkey, nullIfEmpty: true)), + authData: (!group.have_auth_data ? nil : group.get(\.auth_data, nullIfEmpty: true)) + ) + } } // MARK: - Convenience diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 217af7d5ae..315d7dca76 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -1113,6 +1113,8 @@ public protocol LibSessionCacheType: LibSessionImmutableCacheType, MutableCacheT func groupIsDestroyed(groupSessionId: SessionId) -> Bool func groupDeleteBefore(groupSessionId: SessionId) -> TimeInterval? func groupDeleteAttachmentsBefore(groupSessionId: SessionId) -> TimeInterval? + + func authData(groupSessionId: SessionId) -> GroupAuthData } public extension LibSessionCacheType { @@ -1383,6 +1385,10 @@ private final class NoopLibSessionCache: LibSessionCacheType, NoopDependency { func groupIsDestroyed(groupSessionId: SessionId) -> Bool { return false } func groupDeleteBefore(groupSessionId: SessionId) -> TimeInterval? { return nil } func groupDeleteAttachmentsBefore(groupSessionId: SessionId) -> TimeInterval? { return nil } + + func authData(groupSessionId: SessionId) -> GroupAuthData { + return GroupAuthData(groupIdentityPrivateKey: nil, authData: nil) + } } // MARK: - Convenience diff --git a/SessionMessagingKit/LibSession/Types/GroupAuthData.swift b/SessionMessagingKit/LibSession/Types/GroupAuthData.swift new file mode 100644 index 0000000000..cb0dd8bf24 --- /dev/null +++ b/SessionMessagingKit/LibSession/Types/GroupAuthData.swift @@ -0,0 +1,8 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public struct GroupAuthData: Codable { + let groupIdentityPrivateKey: Data? + let authData: Data? +} diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift index cdd2921bcf..bf7d6c9987 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift @@ -404,7 +404,7 @@ extension MessageReceiver { message: message, disappearingMessagesConfiguration: try? DisappearingMessagesConfiguration .fetchOne(db, id: threadId), - authMethod: try Authentication.with(db, swarmPublicKey: threadId, using: dependencies), + authMethod: try Authentication.with(swarmPublicKey: threadId, using: dependencies), onEvent: MessageSender.standardEventHandling(using: dependencies), using: dependencies ) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift index e771404d9a..c0c503e171 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift @@ -746,7 +746,6 @@ extension MessageReceiver { cache.isAdmin(groupSessionId: groupSessionId) }), let authMethod: AuthenticationMethod = try? Authentication.with( - db, swarmPublicKey: groupSessionId.hexString, using: dependencies ) @@ -918,22 +917,19 @@ extension MessageReceiver { case .none: break case .some(let serverHash): db.afterCommit { - dependencies[singleton: .storage] - .readPublisher { db in - try SnodeAPI.preparedDeleteMessages( - serverHashes: [serverHash], - requireSuccessfulDeletion: false, - authMethod: try Authentication.with( - db, - swarmPublicKey: userSessionId.hexString, - using: dependencies - ), - using: dependencies - ) - } - .flatMap { $0.send(using: dependencies) } - .subscribe(on: DispatchQueue.global(qos: .background), using: dependencies) - .sinkUntilComplete() + guard let authMethod: AuthenticationMethod = try? Authentication.with(swarmPublicKey: userSessionId.hexString, using: dependencies) else { + return + } + + try? SnodeAPI.preparedDeleteMessages( + serverHashes: [serverHash], + requireSuccessfulDeletion: false, + authMethod: authMethod, + using: dependencies + ) + .send(using: dependencies) + .subscribe(on: DispatchQueue.global(qos: .background), using: dependencies) + .sinkUntilComplete() } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift index a30f849c81..1c056645bd 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import Combine import GRDB import SessionNetworkingKit import SessionUtilitiesKit @@ -68,16 +69,17 @@ extension MessageReceiver { switch threadVariant { case .legacyGroup, .group, .community: break case .contact: - dependencies[singleton: .storage] - .readPublisher { db in - try SnodeAPI.preparedDeleteMessages( + AnyPublisher + .lazy { + let authMethod: AuthenticationMethod = try Authentication.with( + swarmPublicKey: dependencies[cache: .general].sessionId.hexString, + using: dependencies + ) + + return try SnodeAPI.preparedDeleteMessages( serverHashes: Array(hashes), requireSuccessfulDeletion: false, - authMethod: try Authentication.with( - db, - swarmPublicKey: dependencies[cache: .general].sessionId.hexString, - using: dependencies - ), + authMethod: authMethod, using: dependencies ) } diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeResponse.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeResponse.swift index bff4193f7d..7e8028c620 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeResponse.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeResponse.swift @@ -4,7 +4,7 @@ import Foundation public extension PushNotificationAPI { struct SubscribeResponse: Codable { - struct SubResponse: Codable { + public struct SubResponse: Codable { /// Flag indicating the success of the registration let success: Bool? @@ -32,6 +32,10 @@ public extension PushNotificationAPI { let subResponses: [SubResponse] + public init(subResponses: [SubResponse]) { + self.subResponses = subResponses + } + public init(from decoder: Decoder) throws { guard let container: SingleValueDecodingContainer = try? decoder.singleValueContainer(), diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeResponse.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeResponse.swift index c89aa19f3a..118cd7bd87 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeResponse.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeResponse.swift @@ -4,7 +4,7 @@ import Foundation public extension PushNotificationAPI { struct UnsubscribeResponse: Codable { - struct SubResponse: Codable { + public struct SubResponse: Codable { /// Flag indicating the success of the registration let success: Bool? @@ -32,6 +32,10 @@ public extension PushNotificationAPI { let subResponses: [SubResponse] + public init(subResponses: [SubResponse]) { + self.subResponses = subResponses + } + public init(from decoder: Decoder) throws { guard let container: SingleValueDecodingContainer = try? decoder.singleValueContainer(), diff --git a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift index 89d21fdc30..b1f70daec5 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift @@ -436,7 +436,6 @@ public extension MessageViewModel.DeletionBehaviours { serverHashes: Array(serverHashes), requireSuccessfulDeletion: false, authMethod: try Authentication.with( - db, swarmPublicKey: threadData.currentUserSessionId, using: dependencies ), diff --git a/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift b/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift index 745e1e4418..61cbe319a9 100644 --- a/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift +++ b/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift @@ -114,11 +114,6 @@ public extension Authentication { // MARK: - Convenience -fileprivate struct GroupAuthData: Codable, FetchableRecord { - let groupIdentityPrivateKey: Data? - let authData: Data? -} - public extension Authentication { static func with( _ db: ObservingDatabase, @@ -162,12 +157,11 @@ public extension Authentication { return Authentication.community(info: info, forceBlinded: forceBlinded) - default: return try Authentication.with(db, swarmPublicKey: threadId, using: dependencies) + default: return try Authentication.with(swarmPublicKey: threadId, using: dependencies) } } static func with( - _ db: ObservingDatabase, swarmPublicKey: String, using dependencies: Dependencies ) throws -> AuthenticationMethod { @@ -186,13 +180,11 @@ public extension Authentication { ) case .some(let sessionId) where sessionId.prefix == .group: - let authData: GroupAuthData? = try? ClosedGroup - .filter(id: swarmPublicKey) - .select(.authData, .groupIdentityPrivateKey) - .asRequest(of: GroupAuthData.self) - .fetchOne(db) + let authData: GroupAuthData = dependencies.mutate(cache: .libSession) { libSession in + libSession.authData(groupSessionId: SessionId(.group, hex: swarmPublicKey)) + } - switch (authData?.groupIdentityPrivateKey, authData?.authData) { + switch (authData.groupIdentityPrivateKey, authData.authData) { case (.some(let privateKey), _): return Authentication.groupAdmin( groupSessionId: sessionId, diff --git a/SessionUtilitiesKit/Combine/Publisher+Utilities.swift b/SessionUtilitiesKit/Combine/Publisher+Utilities.swift index ca7cea95c0..e988982c7e 100644 --- a/SessionUtilitiesKit/Combine/Publisher+Utilities.swift +++ b/SessionUtilitiesKit/Combine/Publisher+Utilities.swift @@ -204,3 +204,15 @@ extension AnyPublisher: @retroactive ExpressibleByArrayLiteral where Output: Ran self = Just(Output(elements)).setFailureType(to: Failure.self).eraseToAnyPublisher() } } + +public extension AnyPublisher where Failure == Error { + static func lazy(_ closure: @escaping () throws -> Output) -> Self { + return Deferred { + Future { promise in + do { promise(.success(try closure())) } + catch { promise(.failure(error)) } + } + } + .eraseToAnyPublisher() + } +} diff --git a/SessionUtilitiesKit/Database/Models/KeyValueStore.swift b/SessionUtilitiesKit/Database/Models/KeyValueStore.swift index d7bac86112..e86cfd7784 100644 --- a/SessionUtilitiesKit/Database/Models/KeyValueStore.swift +++ b/SessionUtilitiesKit/Database/Models/KeyValueStore.swift @@ -70,77 +70,54 @@ extension KeyValueStore { // MARK: - Keys public extension KeyValueStore { - struct BoolKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { + protocol Key: RawRepresentable, ExpressibleByStringLiteral, Hashable { + var rawValue: String { get } + init(_ rawValue: String) + } + + struct BoolKey: Key { public let rawValue: String - public init(_ rawValue: String) { self.rawValue = rawValue } - public init?(rawValue: String) { self.rawValue = rawValue } - public init(stringLiteral value: String) { self.init(value) } - public init(unicodeScalarLiteral value: String) { self.init(value) } - public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } } - struct DateKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { + struct DateKey: Key { public let rawValue: String - public init(_ rawValue: String) { self.rawValue = rawValue } - public init?(rawValue: String) { self.rawValue = rawValue } - public init(stringLiteral value: String) { self.init(value) } - public init(unicodeScalarLiteral value: String) { self.init(value) } - public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } } - struct DoubleKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { + struct DoubleKey: Key { public let rawValue: String - public init(_ rawValue: String) { self.rawValue = rawValue } - public init?(rawValue: String) { self.rawValue = rawValue } - public init(stringLiteral value: String) { self.init(value) } - public init(unicodeScalarLiteral value: String) { self.init(value) } - public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } } - struct IntKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { + struct IntKey: Key { public let rawValue: String - public init(_ rawValue: String) { self.rawValue = rawValue } - public init?(rawValue: String) { self.rawValue = rawValue } - public init(stringLiteral value: String) { self.init(value) } - public init(unicodeScalarLiteral value: String) { self.init(value) } - public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } } - struct Int64Key: RawRepresentable, ExpressibleByStringLiteral, Hashable { + struct Int64Key: Key { public let rawValue: String - public init(_ rawValue: String) { self.rawValue = rawValue } - public init?(rawValue: String) { self.rawValue = rawValue } - public init(stringLiteral value: String) { self.init(value) } - public init(unicodeScalarLiteral value: String) { self.init(value) } - public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } } - struct StringKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { + struct StringKey: Key { public let rawValue: String - public init(_ rawValue: String) { self.rawValue = rawValue } - public init?(rawValue: String) { self.rawValue = rawValue } - public init(stringLiteral value: String) { self.init(value) } - public init(unicodeScalarLiteral value: String) { self.init(value) } - public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } } - struct EnumKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { + struct EnumKey: Key { public let rawValue: String - public init(_ rawValue: String) { self.rawValue = rawValue } - public init?(rawValue: String) { self.rawValue = rawValue } - public init(stringLiteral value: String) { self.init(value) } - public init(unicodeScalarLiteral value: String) { self.init(value) } - public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } } } +public extension KeyValueStore.Key { + init?(rawValue: String) { self.init(rawValue) } + init(stringLiteral value: String) { self.init(value) } + init(unicodeScalarLiteral value: String) { self.init(value) } + init(extendedGraphemeClusterLiteral value: String) { self.init(value) } +} + // MARK: - GRDB Interactions public extension ObservingDatabase { @@ -170,30 +147,45 @@ public extension ObservingDatabase { // Default to false if it doesn't exist (self[key.rawValue]?.unsafeValue(as: Bool.self) ?? false) } - set { self[key.rawValue] = KeyValueStore(key: key.rawValue, value: newValue) } + set { + self[key.rawValue] = KeyValueStore(key: key.rawValue, value: newValue) + self.addEvent(newValue, forKey: .keyValue(key)) + } } subscript(key: KeyValueStore.DoubleKey) -> Double? { get { self[key.rawValue]?.value(as: Double.self) } - set { self[key.rawValue] = KeyValueStore(key: key.rawValue, value: newValue) } + set { + self[key.rawValue] = KeyValueStore(key: key.rawValue, value: newValue) + self.addEvent(newValue, forKey: .keyValue(key)) + } } subscript(key: KeyValueStore.IntKey) -> Int? { get { self[key.rawValue]?.value(as: Int.self) } - set { self[key.rawValue] = KeyValueStore(key: key.rawValue, value: newValue) } + set { + self[key.rawValue] = KeyValueStore(key: key.rawValue, value: newValue) + self.addEvent(newValue, forKey: .keyValue(key)) + } } subscript(key: KeyValueStore.Int64Key) -> Int64? { get { self[key.rawValue]?.value(as: Int64.self) } - set { self[key.rawValue] = KeyValueStore(key: key.rawValue, value: newValue) } + set { + self[key.rawValue] = KeyValueStore(key: key.rawValue, value: newValue) + self.addEvent(newValue, forKey: .keyValue(key)) + } } subscript(key: KeyValueStore.StringKey) -> String? { get { self[key.rawValue]?.value(as: String.self) } - set { self[key.rawValue] = KeyValueStore(key: key.rawValue, value: newValue) } + set { + self[key.rawValue] = KeyValueStore(key: key.rawValue, value: newValue) + self.addEvent(newValue, forKey: .keyValue(key)) + } } - subscript(key: KeyValueStore.EnumKey) -> T? where T.RawValue == Int { + subscript(key: KeyValueStore.EnumKey) -> T? where T.RawValue == Int, T: Hashable { get { guard let rawValue: Int = self[key.rawValue]?.value(as: Int.self) else { return nil @@ -201,10 +193,13 @@ public extension ObservingDatabase { return T(rawValue: rawValue) } - set { self[key.rawValue] = KeyValueStore(key: key.rawValue, value: newValue?.rawValue) } + set { + self[key.rawValue] = KeyValueStore(key: key.rawValue, value: newValue?.rawValue) + self.addEvent(newValue, forKey: .keyValue(key)) + } } - subscript(key: KeyValueStore.EnumKey) -> T? where T.RawValue == String { + subscript(key: KeyValueStore.EnumKey) -> T? where T.RawValue == String, T: Hashable { get { guard let rawValue: String = self[key.rawValue]?.value(as: String.self) else { return nil @@ -212,7 +207,10 @@ public extension ObservingDatabase { return T(rawValue: rawValue) } - set { self[key.rawValue] = KeyValueStore(key: key.rawValue, value: newValue?.rawValue) } + set { + self[key.rawValue] = KeyValueStore(key: key.rawValue, value: newValue?.rawValue) + self.addEvent(newValue, forKey: .keyValue(key)) + } } /// Value will be stored as a timestamp in seconds since 1970 @@ -227,48 +225,56 @@ public extension ObservingDatabase { key: key.rawValue, value: newValue.map { $0.timeIntervalSince1970 } ) + self.addEvent(newValue, forKey: .keyValue(key)) } } func setting(key: KeyValueStore.BoolKey, to newValue: Bool) -> KeyValueStore? { let result: KeyValueStore? = KeyValueStore(key: key.rawValue, value: newValue) self[key.rawValue] = result + self.addEvent(newValue, forKey: .keyValue(key)) return result } func setting(key: KeyValueStore.DoubleKey, to newValue: Double?) -> KeyValueStore? { let result: KeyValueStore? = KeyValueStore(key: key.rawValue, value: newValue) self[key.rawValue] = result + self.addEvent(newValue, forKey: .keyValue(key)) return result } func setting(key: KeyValueStore.IntKey, to newValue: Int?) -> KeyValueStore? { let result: KeyValueStore? = KeyValueStore(key: key.rawValue, value: newValue) self[key.rawValue] = result + self.addEvent(newValue, forKey: .keyValue(key)) return result } func setting(key: KeyValueStore.Int64Key, to newValue: Int64?) -> KeyValueStore? { let result: KeyValueStore? = KeyValueStore(key: key.rawValue, value: newValue) self[key.rawValue] = result + self.addEvent(newValue, forKey: .keyValue(key)) return result } func setting(key: KeyValueStore.StringKey, to newValue: String?) -> KeyValueStore? { let result: KeyValueStore? = KeyValueStore(key: key.rawValue, value: newValue) self[key.rawValue] = result + self.addEvent(newValue, forKey: .keyValue(key)) return result } - func setting(key: KeyValueStore.EnumKey, to newValue: T?) -> KeyValueStore? where T.RawValue == Int { + func setting(key: KeyValueStore.EnumKey, to newValue: T?) -> KeyValueStore? where T.RawValue == Int, T: Hashable { let result: KeyValueStore? = KeyValueStore(key: key.rawValue, value: newValue?.rawValue) self[key.rawValue] = result + self.addEvent(newValue, forKey: .keyValue(key)) return result } - func setting(key: KeyValueStore.EnumKey, to newValue: T?) -> KeyValueStore? where T.RawValue == String { + func setting(key: KeyValueStore.EnumKey, to newValue: T?) -> KeyValueStore? where T.RawValue == String, T: Hashable { let result: KeyValueStore? = KeyValueStore(key: key.rawValue, value: newValue?.rawValue) self[key.rawValue] = result + self.addEvent(newValue, forKey: .keyValue(key)) return result } @@ -276,6 +282,25 @@ public extension ObservingDatabase { func setting(key: KeyValueStore.DateKey, to newValue: Date?) -> KeyValueStore? { let result: KeyValueStore? = KeyValueStore(key: key.rawValue, value: newValue.map { $0.timeIntervalSince1970 }) self[key.rawValue] = result + self.addEvent(newValue, forKey: .keyValue(key)) return result } } + +// MARK: - ObservationManager + +public extension ObservableKey { + fileprivate static func keyValue(_ key: String) -> ObservableKey { ObservableKey(key, .keyValue) } + + static func keyValue(_ key: KeyValueStore.BoolKey) -> ObservableKey { keyValue(key.rawValue) } + static func keyValue(_ key: KeyValueStore.DateKey) -> ObservableKey { keyValue(key.rawValue) } + static func keyValue(_ key: KeyValueStore.DoubleKey) -> ObservableKey { keyValue(key.rawValue) } + static func keyValue(_ key: KeyValueStore.IntKey) -> ObservableKey { keyValue(key.rawValue) } + static func keyValue(_ key: KeyValueStore.Int64Key) -> ObservableKey { keyValue(key.rawValue) } + static func keyValue(_ key: KeyValueStore.StringKey) -> ObservableKey { keyValue(key.rawValue) } + static func keyValue(_ key: KeyValueStore.EnumKey) -> ObservableKey { keyValue(key.rawValue) } +} + +public extension GenericObservableKey { + static let keyValue: GenericObservableKey = "keyValue" +} diff --git a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift index 8103d8bfe0..4419366ba0 100644 --- a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift +++ b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift @@ -125,6 +125,10 @@ public class Dependencies { // MARK: - Instance management + public func warmSingleton(singleton: SingletonConfig) { + _ = getOrCreate(singleton) + } + public func warmCache(cache: CacheConfig) { _ = getOrCreate(cache) } @@ -227,7 +231,6 @@ public extension Dependencies { removeValue(feature.identifier, of: .feature) /// Notify observers - dependecyChangeContinuation.yield((key, nil)) notifyAsync(events: [ ObservedEvent(key: .feature(feature), value: nil), ObservedEvent(key: .featureGroup(feature), value: nil) @@ -448,3 +451,49 @@ private extension Dependencies.DependencyStorage { } } } + +// MARK: - Async/Await + +public extension Dependencies { + private func stream(key: DependencyStorage.Key, initialValueRetriever: () -> T?) -> AsyncStream { + return AsyncStream { continuation in + if let initialValue: T = initialValueRetriever() { + continuation.yield(initialValue) + } + + let observationTask = Task { [weak self] in + guard let self else { return continuation.finish() } + + for await (changedKey, changedValue) in self.dependecyChangeStream { + guard changedKey == key else { continue } + + if let newInstance = changedValue?.value(as: T.self) { + continuation.yield(newInstance) + } + } + } + + continuation.onTermination = { @Sendable _ in + observationTask.cancel() + } + } + } + + func stream(singleton: SingletonConfig) -> AsyncStream { + let key = DependencyStorage.Key.Variant.singleton.key(singleton.identifier) + + return stream(key: key, initialValueRetriever: { [weak self] in self?[singleton: singleton] }) + } + + func stream(cache: CacheConfig) -> AsyncStream { + let key = DependencyStorage.Key.Variant.cache.key(cache.identifier) + + return stream(key: key, initialValueRetriever: { [weak self] in self?[cache: cache] }) + } + + func stream(feature: FeatureConfig) -> AsyncStream { + let key = DependencyStorage.Key.Variant.feature.key(feature.identifier) + + return stream(key: key, initialValueRetriever: { [weak self] in self?[feature: feature] }) + } +} diff --git a/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift b/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift index c060be2aa0..8a3cd844d1 100644 --- a/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift +++ b/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift @@ -47,7 +47,7 @@ public extension ObservingDatabase { addEvent(ObservedEvent(key: key, value: nil)) } - func addEvent(_ value: AnyHashable?, forKey key: ObservableKey) { + func addEvent(_ value: T?, forKey key: ObservableKey) { addEvent(ObservedEvent(key: key, value: value)) } } diff --git a/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift b/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift index 26c6b9245e..cd4e8b7a10 100644 --- a/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift +++ b/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift @@ -3,33 +3,50 @@ import Foundation public actor CurrentValueAsyncStream { - private var _currentValue: Element - private let continuation: AsyncStream.Continuation - public let stream: AsyncStream - - public var currentValue: Element { _currentValue } + private let lifecycleManager: StreamLifecycleManager = StreamLifecycleManager() + + /// This is the most recently emitted value + public private(set) var currentValue: Element + + /// Every time `stream` is accessed it will create a **new** stream + /// + /// **Note:** This is non-isolated so it can be exposed via protocols without `async`, this is safe because `AsyncStream` is + /// thread-safe internally and `Element` is `Sendable` so it's verified to be safe to send concurrently + nonisolated public var stream: AsyncStream { + AsyncStream { continuation in + Task { + await self.add(continuation: continuation) + } + } + } // MARK: - Initialization public init(_ initialValue: Element) { - self._currentValue = initialValue - - /// We use `.bufferingNewest(1)` to ensure that the stream always holds the most recent value. When a new iterator is - /// created for the stream, it will receive this buffered value first. - let (stream, continuation) = AsyncStream.makeStream(of: Element.self, bufferingPolicy: .bufferingNewest(1)) - self.stream = stream - self.continuation = continuation - self.continuation.yield(initialValue) + self.currentValue = initialValue } // MARK: - Functions public func send(_ newValue: Element) { - _currentValue = newValue - continuation.yield(newValue) + currentValue = newValue + lifecycleManager.send(newValue) } public func finish() { - continuation.finish() + lifecycleManager.finish() + } + + // MARK: - Internal Functions + + private func add(continuation: AsyncStream.Continuation) { + let id: UUID = lifecycleManager.track(continuation) + + continuation.onTermination = { @Sendable [lifecycleManager] _ in + lifecycleManager.untrack(id: id) + } + + /// Since we've added a new subscriber we need to yield the current value to them + continuation.yield(currentValue) } } diff --git a/SessionUtilitiesKit/Types/StreamLifecycleManager.swift b/SessionUtilitiesKit/Types/StreamLifecycleManager.swift new file mode 100644 index 0000000000..4a9f426e51 --- /dev/null +++ b/SessionUtilitiesKit/Types/StreamLifecycleManager.swift @@ -0,0 +1,52 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +final class StreamLifecycleManager: @unchecked Sendable { + private let lock: NSLock = NSLock() + private var continuations: [UUID: AsyncStream.Continuation] = [:] + + // MARK: - Initialization + + public init() {} + + deinit { + finish() + } + + // MARK: - Functions + + func track(_ continuation: AsyncStream.Continuation) -> UUID { + let id: UUID = UUID() + + lock.withLock { continuations[id] = continuation } + + return id + } + + func untrack(id: UUID) { + _ = lock.withLock { continuations.removeValue(forKey: id) } + } + + func send(_ value: Element) { + /// Capture current continuations before sending to avoid deadlocks where yielding could result in a new continuation being + /// added while the lock is held + let currentContinuations: [UUID: AsyncStream.Continuation] = lock.withLock { continuations } + + for continuation in currentContinuations.values { + continuation.yield(value) + } + } + + func finish() { + let currentContinuations: [UUID: AsyncStream.Continuation] = lock.withLock { + let continuationsToFinish: [UUID: AsyncStream.Continuation] = continuations + continuations.removeAll() + return continuationsToFinish + } + + for continuation in currentContinuations.values { + continuation.finish() + } + } +} diff --git a/SessionUtilitiesKit/Utilities/Task+Utilities.swift b/SessionUtilitiesKit/Utilities/Task+Utilities.swift index 2509ecd539..70448e3edc 100644 --- a/SessionUtilitiesKit/Utilities/Task+Utilities.swift +++ b/SessionUtilitiesKit/Utilities/Task+Utilities.swift @@ -9,4 +9,19 @@ public extension Task where Success == Never, Failure == Never { let nanosecondsToSleep: UInt64 = (UInt64(interval.milliseconds) * 1_000_000) try await Task.sleep(nanoseconds: nanosecondsToSleep) } + + static func sleep( + for interval: DispatchTimeInterval, + checkingEvery checkInterval: DispatchTimeInterval = .milliseconds(100), + until condition: () async -> Bool + ) async throws { + var currentWaitDuration: Int = 0 + + while currentWaitDuration < interval.milliseconds { + guard await !condition() else { return } + + try await Task.sleep(for: checkInterval) + currentWaitDuration += checkInterval.milliseconds + } + } } From d14cf60ee1ca81ae0e6a8bb84a204d3ef420d796 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 22 Aug 2025 16:02:30 +1000 Subject: [PATCH 08/59] Removed a redundant extension --- .../Settings/ThreadSettingsViewModel.swift | 2 +- Session/Onboarding/Onboarding.swift | 2 +- .../Sending & Receiving/MessageReceiver.swift | 2 +- .../Sending & Receiving/MessageSender.swift | 10 +++++----- .../MessageViewModel+DeletionActions.swift | 2 +- SessionMessagingKit/Utilities/ExtensionHelper.swift | 2 +- .../LibSession/LibSession+Networking.swift | 2 +- SessionUtilitiesKit/Database/Models/Identity.swift | 2 +- SessionUtilitiesKit/Database/Storage.swift | 3 +-- SessionUtilitiesKit/Utilities/Result+Utilities.swift | 7 ------- 10 files changed, 13 insertions(+), 21 deletions(-) diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 5045825fb5..378dd5fd6f 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -1894,7 +1894,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob }, completion: { [weak self, dependencies] result in guard - let numPinnedConversations: Int = try? result.successOrThrow(), + let numPinnedConversations: Int = try? result.get(), numPinnedConversations > 0 else { return } diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 4ca9d056dd..4c2419ff15 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -112,7 +112,7 @@ extension Onboarding { dependencies[singleton: .storage].readAsync( retrieve: { db in Identity.fetchUserEd25519KeyPair(db) }, completion: { result in - ed25519KeyPair = ((try? result.successOrThrow()) ?? .empty) + ed25519KeyPair = ((try? result.get()) ?? .empty) semaphore.signal() } ) diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index e17035bdcb..bb36426149 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -175,7 +175,7 @@ public enum MessageReceiver { let proto: SNProtoContent = try (customProto ?? Result(catching: { try SNProtoContent.parseData(plaintext) }) .onFailure { Log.error(.messageReceiver, "Couldn't parse proto due to error: \($0).") } - .successOrThrow()) + .get()) let message: Message = try (customMessage ?? Message.createMessageFrom(proto, sender: sender, using: dependencies)) message.sender = sender message.serverHash = serverHash diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index a31872dccb..95e863861a 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -397,7 +397,7 @@ public final class MessageSender { return try Result(proto.serializedData().paddedMessageBody()) .mapError { MessageSenderError.other(nil, "Couldn't serialize proto", $0) } - .successOrThrow() + .get() default: guard @@ -415,7 +415,7 @@ public final class MessageSender { } } .mapError { MessageSenderError.other(nil, "Couldn't serialize proto", $0) } - .successOrThrow() + .get() } }() @@ -431,7 +431,7 @@ public final class MessageSender { ) ) .mapError { MessageSenderError.other(nil, "Couldn't wrap message", $0) } - .successOrThrow() + .get() let ciphertext: Data = try dependencies[singleton: .crypto].tryGenerate( .ciphertextForGroupMessage( @@ -474,7 +474,7 @@ public final class MessageSender { ) ) .mapError { MessageSenderError.other(nil, "Couldn't wrap message", $0) } - .successOrThrow() + .get() /// Community messages should be sent in plaintext case (.openGroup, _): return plaintext @@ -489,7 +489,7 @@ public final class MessageSender { ) ) .mapError { MessageSenderError.other(nil, "Couldn't encrypt message for destination: \(destination)", $0) } - .successOrThrow() + .get() /// Config messages should be sent directly rather than via this method case (.closedGroup(let groupId), _) where (try? SessionId.Prefix(from: groupId)) == .group: diff --git a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift index b1f70daec5..0fe3192b88 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift @@ -357,7 +357,7 @@ public extension MessageViewModel.DeletionBehaviours { } }, completion: { result in - deletionBehaviours = try? result.successOrThrow() + deletionBehaviours = try? result.get() semaphore.signal() } ) diff --git a/SessionMessagingKit/Utilities/ExtensionHelper.swift b/SessionMessagingKit/Utilities/ExtensionHelper.swift index ed44a08963..a91e73cfca 100644 --- a/SessionMessagingKit/Utilities/ExtensionHelper.swift +++ b/SessionMessagingKit/Utilities/ExtensionHelper.swift @@ -411,7 +411,7 @@ public class ExtensionHelper: ExtensionHelperType { completion: { [weak self] result in guard let self = self, - let dumps: [ConfigDump] = try? result.successOrThrow() + let dumps: [ConfigDump] = try? result.get() else { return } /// Persist each dump to disk (if there isn't already one there, or it was updated before the dump was fetched from diff --git a/SessionNetworkingKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift index 7e5d318a60..0e98f62fae 100644 --- a/SessionNetworkingKit/LibSession/LibSession+Networking.swift +++ b/SessionNetworkingKit/LibSession/LibSession+Networking.swift @@ -75,7 +75,7 @@ class LibSessionNetwork: NetworkType { value: (try? result.get())?.count ?? 0 ) } - return try result.successOrThrow() + return try result.get() } .eraseToAnyPublisher() } diff --git a/SessionUtilitiesKit/Database/Models/Identity.swift b/SessionUtilitiesKit/Database/Models/Identity.swift index 945e0c7d3f..d374164ec0 100644 --- a/SessionUtilitiesKit/Database/Models/Identity.swift +++ b/SessionUtilitiesKit/Database/Models/Identity.swift @@ -124,7 +124,7 @@ public extension Identity { ) }, completion: { result in - let (hasStoredXKeyPair, hasStoredEdKeyPair) = ((try? result.successOrThrow()) ?? (false, false)) + let (hasStoredXKeyPair, hasStoredEdKeyPair) = ((try? result.get()) ?? (false, false)) // stringlint:ignore_start let dbStates: [String] = [ diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 7d80293638..6d1c899bdf 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -801,8 +801,7 @@ open class Storage { addCall(info) defer { removeCall(info) } - return try await Storage.performOperation(info, dbWriter, operation, dependencies) - .successOrThrow() + return try await Storage.performOperation(info, dbWriter, operation, dependencies).get() } private func addCall(_ call: CallInfo) { diff --git a/SessionUtilitiesKit/Utilities/Result+Utilities.swift b/SessionUtilitiesKit/Utilities/Result+Utilities.swift index 69b3d09444..380e0e26bd 100644 --- a/SessionUtilitiesKit/Utilities/Result+Utilities.swift +++ b/SessionUtilitiesKit/Utilities/Result+Utilities.swift @@ -16,11 +16,4 @@ public extension Result where Failure == Error { return self } - - func successOrThrow() throws -> Success { - switch self { - case .success(let value): return value - case .failure(let error): throw error - } - } } From 43585dbf5b050ed4f69551bb281acad9efe44954 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 22 Aug 2025 16:03:22 +1000 Subject: [PATCH 09/59] Added an event to notify of any conversation deletion --- .../Utilities/ObservableKey+SessionMessagingKit.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SessionMessagingKit/Utilities/ObservableKey+SessionMessagingKit.swift b/SessionMessagingKit/Utilities/ObservableKey+SessionMessagingKit.swift index 969a79da3b..01b9897ee6 100644 --- a/SessionMessagingKit/Utilities/ObservableKey+SessionMessagingKit.swift +++ b/SessionMessagingKit/Utilities/ObservableKey+SessionMessagingKit.swift @@ -51,6 +51,7 @@ public extension ObservableKey { static func conversationDeleted(_ id: String) -> ObservableKey { ObservableKey("conversationDeleted-\(id)", .conversationDeleted) } + static let anyConversationDeleted: ObservableKey = "anyConversationDeleted" // MARK: - Messages @@ -240,7 +241,9 @@ public extension ObservingDatabase { switch type { case .created: addEvent(ObservedEvent(key: .conversationCreated, value: event)) case .updated: addEvent(ObservedEvent(key: .conversationUpdated(id), value: event)) - case .deleted: addEvent(ObservedEvent(key: .conversationDeleted(id), value: event)) + case .deleted: + addEvent(ObservedEvent(key: .conversationDeleted(id), value: event)) + addEvent(ObservedEvent(key: .anyConversationDeleted, value: event)) } } } From 2adac2898c7c296eec0c876c3991023a177ab04a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 22 Aug 2025 16:03:48 +1000 Subject: [PATCH 10/59] Added code to schedule a missing recurring job (could be lost) --- SessionMessagingKit/Configuration.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 5260915ddc..5c34f09724 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -90,7 +90,8 @@ public enum SNMessagingKit { // Just to make the external API nice (.updateProfilePicture, .recurringOnActive, false, false), (.retrieveDefaultOpenGroupRooms, .recurringOnActive, false, false), (.garbageCollection, .recurringOnActive, false, false), - (.failedGroupInvitesAndPromotions, .recurringOnLaunch, true, false) + (.failedGroupInvitesAndPromotions, .recurringOnLaunch, true, false), + (.checkForAppUpdates, .recurring, false, false) ] ) } From dc24dca3836060f5f25115aad686e49c3b95c193 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 22 Aug 2025 16:05:15 +1000 Subject: [PATCH 11/59] Removed database usage from SnodeAPI --- SessionNetworkingKit/SnodeAPI/SnodeAPI.swift | 22 +++++-------------- .../SnodeAPI/SnodeAPIError.swift | 9 +++----- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift index df699e4a16..02c22ab32e 100644 --- a/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift +++ b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift @@ -4,7 +4,6 @@ import Foundation import Combine -import GRDB import Punycode import SessionUtilitiesKit @@ -18,8 +17,8 @@ public final class SnodeAPI { public typealias PollResponse = [SnodeAPI.Namespace: (info: ResponseInfoType, data: PreparedGetMessagesResponse?)] public static func preparedPoll( - _ db: ObservingDatabase, namespaces: [SnodeAPI.Namespace], + lastHashes: [SnodeAPI.Namespace: String], refreshingConfigHashes: [String] = [], from snode: LibSession.Snode, authMethod: AuthenticationMethod, @@ -53,9 +52,9 @@ public final class SnodeAPI { requests.append( contentsOf: try namespaces.map { namespace -> any ErasedPreparedRequest in try SnodeAPI.preparedGetMessages( - db, namespace: namespace, snode: snode, + lastHash: lastHashes[namespace], maxSize: namespaceMaxSizeMap[namespace] .defaulting(to: fallbackSize), authMethod: authMethod, @@ -153,22 +152,13 @@ public final class SnodeAPI { public typealias PreparedGetMessagesResponse = (messages: [SnodeReceivedMessage], lastHash: String?) public static func preparedGetMessages( - _ db: ObservingDatabase, namespace: SnodeAPI.Namespace, snode: LibSession.Snode, + lastHash: String? = nil, maxSize: Int64? = nil, authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest { - let maybeLastHash: String? = try SnodeReceivedMessageInfo - .fetchLastNotExpired( - db, - for: snode, - namespace: namespace, - swarmPublicKey: try authMethod.swarmPublicKey, - using: dependencies - )? - .hash let preparedRequest: Network.PreparedRequest = try { // Check if this namespace requires authentication guard namespace.requiresReadAuthentication else { @@ -178,7 +168,7 @@ public final class SnodeAPI { swarmPublicKey: try authMethod.swarmPublicKey, body: LegacyGetMessagesRequest( pubkey: try authMethod.swarmPublicKey, - lastHash: (maybeLastHash ?? ""), + lastHash: (lastHash ?? ""), namespace: namespace, maxCount: nil, maxSize: maxSize @@ -194,7 +184,7 @@ public final class SnodeAPI { endpoint: .getMessages, swarmPublicKey: try authMethod.swarmPublicKey, body: GetMessagesRequest( - lastHash: (maybeLastHash ?? ""), + lastHash: (lastHash ?? ""), namespace: namespace, authMethod: authMethod, timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs(), @@ -217,7 +207,7 @@ public final class SnodeAPI { rawMessage: rawMessage ) }, - maybeLastHash + lastHash ) } } diff --git a/SessionNetworkingKit/SnodeAPI/SnodeAPIError.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPIError.swift index 0de7eb2ba8..424169acf0 100644 --- a/SessionNetworkingKit/SnodeAPI/SnodeAPIError.swift +++ b/SessionNetworkingKit/SnodeAPI/SnodeAPIError.swift @@ -33,7 +33,7 @@ public enum SnodeAPIError: Error, CustomStringConvertible { // Quic case invalidPayload case missingSecretKey - case nodeNotFound(Int?, String) + case nodeNotFound(String) case unassociatedPubkey case unableToRetrieveSwarm @@ -70,11 +70,8 @@ public enum SnodeAPIError: Error, CustomStringConvertible { // Quic case .invalidPayload: return "Invalid payload (SnodeAPIError.invalidPayload)." case .missingSecretKey: return "Missing secret key (SnodeAPIError.missingSecretKey)." - case .nodeNotFound(let nodeIndex, _): - switch nodeIndex { - case .some(let index): return "Error in Onion request path, with hop \(index) (SnodeAPIError.nodeNotFound)." - case .none: return "Error in Onion request path (SnodeAPIError.nodeNotFound)." - } + case .nodeNotFound(let nodeHex): + return "Error in Onion request path, with node \(nodeHex) (SnodeAPIError.nodeNotFound)." case .unassociatedPubkey: return "The service node is no longer associated with the public key (SnodeAPIError.unassociatedPubkey)." case .unableToRetrieveSwarm: return "Unable to retrieve the swarm for the given public key (SnodeAPIError.unableToRetrieveSwarm)." From 8fc4ed2488977b8304e5e40a292bce683c808c8d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 09:58:24 +1000 Subject: [PATCH 12/59] Updated actor-based networking wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Cleaned up the AppDelegate setup process a bit • Updated the LibSessionNetwork to be an actor (no more cache + singleton) • Updated LibSessionNetwork to support async/await (also supports Combine, but that is now deprecated) • Updated network observation to use async/await • Refactored IP2Country to be an actor and use async/await • Refactored the pollers to use async/await • Refactored the getSessionId API call to use async/await • Refactored the AppSetup and database migrations to be async/await --- .../Calls/Call Management/SessionCall.swift | 22 +- .../Call Management/SessionCallManager.swift | 14 +- Session/Home/HomeVC.swift | 2 +- Session/Home/HomeViewModel.swift | 2 +- .../GIFs/GifPickerViewController.swift | 11 +- Session/Meta/AppDelegate.swift | 437 +++-- Session/Meta/Session+SNUIKit.swift | 1 + Session/Meta/SessionApp.swift | 18 +- .../PushRegistrationManager.swift | 8 +- Session/Path/PathStatusView.swift | 42 +- Session/Path/PathVC.swift | 191 ++- .../Settings/DeveloperSettingsViewModel.swift | 8 +- Session/Utilities/BackgroundPoller.swift | 347 ++-- Session/Utilities/IP2Country.swift | 120 +- .../Database/Models/ClosedGroup.swift | 8 +- .../Jobs/ConfigurationSyncJob.swift | 75 +- ...ProcessPendingGroupMemberRemovalsJob.swift | 1 - .../Open Groups/OpenGroupAPI.swift | 2 +- .../Open Groups/OpenGroupManager.swift | 20 +- .../MessageSender+Groups.swift | 8 +- .../Pollers/CommunityPoller.swift | 924 ++++++----- .../Pollers/CurrentUserPoller.swift | 105 +- .../Pollers/GroupPoller.swift | 339 ++-- .../Pollers/PollerType.swift | 264 +-- .../Pollers/SwarmPoller.swift | 337 ++-- SessionMessagingKit/Utilities/AppSetup.swift | 109 ++ .../LibSession/LibSession+Networking.swift | 1439 ++++++++--------- .../SnodeAPI/Request+SnodeAPI.swift | 58 +- SessionNetworkingKit/SnodeAPI/SnodeAPI.swift | 6 +- SessionNetworkingKit/Types/Destination.swift | 24 +- SessionNetworkingKit/Types/Network.swift | 41 +- .../Types/PreparedRequest+Sending.swift | 49 +- .../Types/PreparedRequest.swift | 119 +- .../NotificationServiceExtension.swift | 2 +- .../ShareNavController.swift | 68 +- SessionShareExtension/ThreadPickerVC.swift | 321 ++-- SessionUtilitiesKit/Database/Storage.swift | 121 +- .../Dependency Injection/Dependencies.swift | 8 +- SessionUtilitiesKit/JobRunner/JobRunner.swift | 2 + .../Types/CancellationAwareAsyncStream.swift | 72 + .../Types/CurrentValueAsyncStream.swift | 35 +- .../Types/StreamLifecycleManager.swift | 27 +- SignalUtilitiesKit/Utilities/AppSetup.swift | 135 -- 43 files changed, 2952 insertions(+), 2990 deletions(-) create mode 100644 SessionMessagingKit/Utilities/AppSetup.swift create mode 100644 SessionUtilitiesKit/Types/CancellationAwareAsyncStream.swift delete mode 100644 SignalUtilitiesKit/Utilities/AppSetup.swift diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index f7a9a64ec7..d7b8d8b859 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -13,6 +13,7 @@ import SessionNetworkingKit public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { private let dependencies: Dependencies + private var networkObservationTask: Task? public let webRTCSession: WebRTCSession var currentConnectionStep: ConnectionStep @@ -176,6 +177,10 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { } } + deinit { + networkObservationTask?.cancel() + } + // stringlint:ignore_contents func reportIncomingCallIfNeeded(completion: @escaping (Error?) -> Void) { guard case .answer = mode else { @@ -476,16 +481,15 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { // Register a callback to get the current network status then remove it immediately as we only // care about the current status - dependencies[cache: .libSessionNetwork].networkStatus - .sinkUntilComplete( - receiveValue: { [weak self, dependencies] status in - guard status != .connected else { return } - - self?.reconnectTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: 5, repeats: false, using: dependencies) { _ in - self?.tryToReconnect() - } + networkObservationTask = Task { [weak self, dependencies] in + for await status in dependencies[singleton: .network].networkStatus { + guard status != .connected else { return } + + self?.reconnectTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: 5, repeats: false, using: dependencies) { _ in + self?.tryToReconnect() } - ) + } + } let sessionId: String = self.sessionId let webRTCSession: WebRTCSession = self.webRTCSession diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index 2d7f8eb9dc..5773282969 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -194,9 +194,11 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { // Stop all jobs except for message sending and when completed suspend the database dependencies[singleton: .jobRunner].stopAndClearPendingJobs(exceptForVariant: .messageSend) { [dependencies] _ in if self.currentCall?.hasEnded != false { - dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() } - dependencies[singleton: .storage].suspendDatabaseAccess() - Log.flush() + Task { [dependencies] in + await dependencies[singleton: .network].suspendNetworkAccess() + dependencies[singleton: .storage].suspendDatabaseAccess() + Log.flush() + } } } } @@ -293,8 +295,10 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { dependencies[defaults: .appGroup, key: .lastCallPreOffer] = nil if dependencies[singleton: .appContext].isNotInForeground { - dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [dependencies] in - dependencies[singleton: .currentUserPoller].stop() + dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [poller = dependencies[singleton: .currentUserPoller]] in + Task(priority: .userInitiated) { + await poller.stop() + } } Log.flush() } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index dc3db601d7..87ceafc7e6 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -344,7 +344,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi } // Onion request path countries cache - viewModel.dependencies.warmCache(cache: .ip2Country) + viewModel.dependencies.warm(singleton: .ip2Country) // Bind the UI to the view model bindViewModel() diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index ade9c3f15e..71f81abccc 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -363,7 +363,7 @@ public class HomeViewModel: NavigatableStateHolder { /// Generate the new state return State( - viewState: (loadResult.info.totalCount == 0 ? + viewState: (loadResult.info.totalCount == 0 && unreadMessageRequestThreadCount == 0 ? .empty(isNewUser: (startedAsNewUser && !hasSavedThread && !hasSavedMessage)) : .loaded ), diff --git a/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift b/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift index b1557c2166..a1e3a4dc98 100644 --- a/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift +++ b/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift @@ -41,6 +41,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect var progressiveSearchTimer: Timer? + private var networkObservationTask: Task? private var disposables: Set = Set() // MARK: - Initialization @@ -64,6 +65,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect deinit { NotificationCenter.default.removeObserver(self) + networkObservationTask?.cancel() progressiveSearchTimer?.invalidate() } @@ -104,13 +106,12 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect createViews() - dependencies[cache: .libSessionNetwork].networkStatus - .receive(on: DispatchQueue.main, using: dependencies) - .sink(receiveValue: { [weak self] _ in + networkObservationTask = Task { [weak self, dependencies] in + for await status in dependencies[singleton: .network].networkStatus { // Prod cells to try to load when connectivity changes. self?.ensureCellState() - }) - .store(in: &disposables) + } + } NotificationCenter.default.addObserver( self, diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 942992dc90..bb5c0a52b2 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -35,8 +35,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Lifecycle func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - /// If we are running automated tests we should process environment variables before we do anything else - DeveloperSettingsViewModel.processUnitTestEnvVariablesIfNeeded(using: dependencies) + Log.info(.cat, "didFinishLaunchingWithOptions called.") + startTime = CACurrentMediaTime() #if DEBUG /// If we are running unit tests then we don't want to run the usual application startup process (as it could slow down and/or @@ -50,116 +50,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } #endif - Log.info(.cat, "didFinishLaunchingWithOptions called.") - startTime = CACurrentMediaTime() - - // These should be the first things we do (the startup process can fail without them) + /// These should be the first things we do (the startup process can fail without them) dependencies.set(singleton: .appContext, to: MainAppContext(using: dependencies)) verifyDBKeysAvailableBeforeBackgroundLaunch() - - dependencies.warmCache(cache: .appVersion) - dependencies[singleton: .pushRegistrationManager].createVoipRegistryIfNecessary() - - // Prevent the device from sleeping during database view async registration - // (e.g. long database upgrades). - // - // This block will be cleared in storageIsReady. - dependencies[singleton: .deviceSleepManager].addBlock(blockObject: self) let mainWindow: UIWindow = TraitObservingWindow(frame: UIScreen.main.bounds) self.loadingViewController = LoadingViewController() - AppSetup.setupEnvironment( - appSpecificBlock: { [dependencies] in - Log.setup(with: Logger(primaryPrefix: "Session", using: dependencies)) - Log.info(.cat, "Setting up environment.") - - /// Create a proper `SessionCallManager` for the main app (defaults to a no-op version) - dependencies.set(singleton: .callManager, to: SessionCallManager(using: dependencies)) - - // Setup LibSession - LibSession.setupLogger(using: dependencies) - dependencies.warmCache(cache: .libSessionNetwork) - - // Configure the different targets - SNUtilitiesKit.configure( - networkMaxFileSize: Network.maxFileSize, - maxValidImageDimention: ImageDataManager.DataSource.maxValidDimension, - using: dependencies - ) - SNMessagingKit.configure(using: dependencies) - - // Update state of current call - if dependencies[singleton: .callManager].currentCall == nil { - dependencies[defaults: .appGroup, key: .isCallOngoing] = false - dependencies[defaults: .appGroup, key: .lastCallPreOffer] = nil - } - - // Note: Intentionally dispatching sync as we want to wait for these to complete before - // continuing - DispatchQueue.main.sync { - dependencies[singleton: .screenLock].setupWithRootWindow(rootWindow: mainWindow) - OWSWindowManager.shared().setup( - withRootWindow: mainWindow, - screenBlockingWindow: dependencies[singleton: .screenLock].window, - backgroundWindowLevel: .background - ) - } - }, - migrationProgressChanged: { [weak self] progress, minEstimatedTotalTime in - self?.loadingViewController?.updateProgress( - progress: progress, - minEstimatedTotalTime: minEstimatedTotalTime - ) - }, - migrationsCompletion: { [weak self, dependencies] result in - if case .failure(let error) = result { - DispatchQueue.main.async { - self?.initialLaunchFailed = true - self?.showFailedStartupAlert( - calledFrom: .finishLaunching, - error: .databaseError(error) - ) - } - return - } - - /// Because the `SessionUIKit` target doesn't depend on the `SessionUtilitiesKit` dependency (it shouldn't - /// need to since it should just be UI) but since the theme settings are stored in the database we need to pass these through - /// to `SessionUIKit` and expose a mechanism to save updated settings - this is done here (once the migrations complete) - Task { @MainActor in - SNUIKit.configure( - with: SessionSNUIKitConfig(using: dependencies), - themeSettings: dependencies.mutate(cache: .libSession) { cache -> ThemeSettings in - ( - cache.get(.theme), - cache.get(.themePrimaryColor), - cache.get(.themeMatchSystemDayNightCycle) - ) - } - ) - } - - /// Adding this to prevent new users being asked for local network permission in the wrong order in the permission chain. - /// We need to check the local nework permission status every time the app is activated to refresh the UI in Settings screen. - /// And after granting or denying a system permission request will trigger the local nework permission status check in applicationDidBecomeActive(:) - /// The only way we can check the status of local network permission will trigger the system prompt to ask for the permission. - /// So we need this to keep it the correct order of the permission chain. - /// For users who already enabled the calls permission and made calls, the local network permission should already be asked for. - /// It won't affect anything. - dependencies[defaults: .standard, key: .hasRequestedLocalNetworkPermission] = dependencies.mutate(cache: .libSession) { cache in - cache.get(.areCallsEnabled) - } - - /// Now that the theme settings have been applied we can complete the migrations - self?.completePostMigrationSetup(calledFrom: .finishLaunching) - }, - using: dependencies - ) + /// Kick of a task to perform the app setup + Task(priority: .userInitiated) { [weak self, mainWindow] in + await self?.setupEnvironment(mainWindow: mainWindow) + } - // No point continuing if we are running tests - guard !SNUtilitiesKit.isRunningTests else { return true } - self.window = mainWindow dependencies[singleton: .appContext].setMainWindow(mainWindow) @@ -197,8 +99,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// Apple's documentation on the matter) dependencies[singleton: .notificationsManager].setDelegate(self) - dependencies[singleton: .storage].resumeDatabaseAccess() - dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() } + Task { + dependencies[singleton: .storage].resumeDatabaseAccess() + await dependencies[singleton: .network].resumeNetworkAccess() + } // Reset the 'startTime' (since it would be invalid from the last launch) startTime = CACurrentMediaTime() @@ -219,31 +123,28 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } // Dispatch async so things can continue to be progressed if a migration does need to run - DispatchQueue.global(qos: .userInitiated).async { [weak self, dependencies] in - AppSetup.runPostSetupMigrations( - migrationProgressChanged: { progress, minEstimatedTotalTime in + Task(priority: .userInitiated) { [weak self, dependencies] in + do { + try await AppSetup.performDatabaseMigrations(using: dependencies) { [weak self] progress, minEstimatedTotalTime in self?.loadingViewController?.updateProgress( progress: progress, minEstimatedTotalTime: minEstimatedTotalTime ) - }, - migrationsCompletion: { result in - if case .failure(let error) = result { - DispatchQueue.main.async { - self?.showFailedStartupAlert( - calledFrom: .enterForeground(initialLaunchFailed: initialLaunchFailed), - error: .databaseError(error) - ) - } - return - } - - self?.completePostMigrationSetup( - calledFrom: .enterForeground(initialLaunchFailed: initialLaunchFailed) + } + try await AppSetup.postMigrationSetup(using: dependencies) + + self?.completePostMigrationSetup( + calledFrom: .enterForeground(initialLaunchFailed: initialLaunchFailed) + ) + } + catch { + await MainActor.run { [weak self] in + self?.showFailedStartupAlert( + calledFrom: .enterForeground(initialLaunchFailed: initialLaunchFailed), + error: .databaseError(error) ) - }, - using: dependencies - ) + } + } } } } @@ -260,10 +161,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Stop all jobs except for message sending and when completed suspend the database dependencies[singleton: .jobRunner].stopAndClearPendingJobs(exceptForVariant: .messageSend) { [dependencies] neededBackgroundProcessing in if !self.hasCallOngoing() && (!neededBackgroundProcessing || dependencies[singleton: .appContext].isInBackground) { - dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() } - dependencies[singleton: .storage].suspendDatabaseAccess() - Log.info(.cat, "completed network and database shutdowns.") - Log.flush() + Task { + await dependencies[singleton: .network].suspendNetworkAccess() + dependencies[singleton: .storage].suspendDatabaseAccess() + Log.info(.cat, "completed network and database shutdowns.") + Log.flush() + } } } } @@ -287,8 +190,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD dependencies[defaults: .appGroup, key: .isMainAppActive] = true // FIXME: Seems like there are some discrepancies between the expectations of how the iOS lifecycle methods work, we should look into them and ensure the code behaves as expected (in this case there were situations where these two wouldn't get called when returning from the background) - dependencies[singleton: .storage].resumeDatabaseAccess() - dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() } + Task { + dependencies[singleton: .storage].resumeDatabaseAccess() + await dependencies[singleton: .network].resumeNetworkAccess() + } ensureRootViewController(calledFrom: .didBecomeActive) @@ -360,12 +265,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD Log.appResumedExecution() Log.info(.backgroundPoller, "Starting background fetch.") - dependencies[singleton: .storage].resumeDatabaseAccess() - dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() } + Task { + dependencies[singleton: .storage].resumeDatabaseAccess() + await dependencies[singleton: .network].resumeNetworkAccess() + } let queue: DispatchQueue = DispatchQueue(label: "com.session.backgroundPoll") let poller: BackgroundPoller = BackgroundPoller() - var cancellable: AnyCancellable? + var pollTask: Task? /// Background tasks only last for a certain amount of time (which can result in a crash and a prompt appearing for the user), /// we want to avoid this and need to make sure to suspend the database again before the background task ends so we start @@ -377,15 +284,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let timer: DispatchSourceTimer = DispatchSource.makeTimerSource(queue: queue) timer.schedule(deadline: .now() + .milliseconds(durationRemainingMs)) timer.setEventHandler { [poller, dependencies] in - guard cancellable != nil else { return } + guard pollTask != nil else { return } Log.info(.backgroundPoller, "Background poll failed due to manual timeout.") - cancellable?.cancel() + pollTask?.cancel() if dependencies[singleton: .appContext].isInBackground && !self.hasCallOngoing() { - dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() } - dependencies[singleton: .storage].suspendDatabaseAccess() - Log.flush() + Task { + await dependencies[singleton: .network].suspendNetworkAccess() + dependencies[singleton: .storage].suspendDatabaseAccess() + Log.flush() + } } _ = poller // Capture poller to ensure it doesn't go out of scope @@ -403,57 +312,143 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// /// **Note:** We **MUST** capture both `poller` and `timer` strongly in the completion handler to ensure neither /// go out of scope until we want them to (we essentually want a retain cycle in this case) - cancellable = poller - .poll(using: dependencies) - .subscribe(on: queue, using: dependencies) - .receive(on: queue, using: dependencies) - .sink( - receiveCompletion: { [timer, poller] result in - // Ensure we haven't timed out yet - guard timer.isCancelled == false else { return } - - // Immediately cancel the timer to prevent the timeout being triggered - timer.cancel() - - // Update the app badge in case the unread count changed - if - let unreadCount: Int = dependencies[singleton: .storage].read({ db in - try Interaction.fetchAppBadgeUnreadCount(db, using: dependencies) - }) - { - try? dependencies[singleton: .extensionHelper].saveUserMetadata( - sessionId: dependencies[cache: .general].sessionId, - ed25519SecretKey: dependencies[cache: .general].ed25519SecretKey, - unreadCount: unreadCount - ) - - DispatchQueue.main.async(using: dependencies) { - UIApplication.shared.applicationIconBadgeNumber = unreadCount - } - } - - // If we are still running in the background then suspend the network & database - if dependencies[singleton: .appContext].isInBackground && !self.hasCallOngoing() { - dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() } - dependencies[singleton: .storage].suspendDatabaseAccess() - Log.flush() - } - - _ = poller // Capture poller to ensure it doesn't go out of scope - - // Complete the background task - switch result { - case .failure: completionHandler(.failed) - case .finished: completionHandler(.newData) - } - }, - receiveValue: { _ in } - ) + pollTask = Task(priority: .userInitiated) { [dependencies] in + let hadValidMessages: Bool = await poller.poll(using: dependencies) + + do { try Task.checkCancellation() } + catch { return } + + // Ensure we haven't timed out yet + guard timer.isCancelled == false else { return } + + // Immediately cancel the timer to prevent the timeout being triggered + timer.cancel() + + // Update the app badge in case the unread count changed + if + let unreadCount: Int = try? await dependencies[singleton: .storage].readAsync(value: { db in + try Interaction.fetchAppBadgeUnreadCount(db, using: dependencies) + }) + { + try? dependencies[singleton: .extensionHelper].saveUserMetadata( + sessionId: dependencies[cache: .general].sessionId, + ed25519SecretKey: dependencies[cache: .general].ed25519SecretKey, + unreadCount: unreadCount + ) + + await MainActor.run { + UIApplication.shared.applicationIconBadgeNumber = unreadCount + } + } + + // If we are still running in the background then suspend the network & database + if dependencies[singleton: .appContext].isInBackground && !self.hasCallOngoing() { + await dependencies[singleton: .network].suspendNetworkAccess() + dependencies[singleton: .storage].suspendDatabaseAccess() + Log.flush() + } + + // Complete the background task + completionHandler(hadValidMessages ? .newData : .failed) + } } } // MARK: - App Readiness + private func setupEnvironment(mainWindow: UIWindow) async { + var backgroundTask: SessionBackgroundTask? = SessionBackgroundTask(label: #function, using: dependencies) + + Log.setup(with: Logger(primaryPrefix: "Session", using: dependencies)) + LibSession.setupLogger(using: dependencies) + Log.info(.cat, "Setting up environment.") + + /// If we are running automated tests we should process environment variables before we do anything else + await DeveloperSettingsViewModel.processUnitTestEnvVariablesIfNeeded(using: dependencies) + + /// Setup the VoiP registry + dependencies[singleton: .pushRegistrationManager].createVoipRegistryIfNecessary() + + /// Prevent the device from sleeping during database view async registration (e.g. long database upgrades) + /// + /// This block will be cleared in storageIsReady + dependencies[singleton: .deviceSleepManager].addBlock(blockObject: self) + + do { + /// Initial app setup + try await AppSetup.performSetup(using: dependencies) + + /// Create a proper `SessionCallManager` for the main app (defaults to a no-op version) + dependencies.set(singleton: .callManager, to: SessionCallManager(using: dependencies)) + + /// Update state of current call + if dependencies[singleton: .callManager].currentCall == nil { + dependencies[defaults: .appGroup, key: .isCallOngoing] = false + dependencies[defaults: .appGroup, key: .lastCallPreOffer] = nil + } + + /// **Note:** We want to wait for these to complete before continuing + await MainActor.run { + dependencies[singleton: .screenLock].setupWithRootWindow(rootWindow: mainWindow) + OWSWindowManager.shared().setup( + withRootWindow: mainWindow, + screenBlockingWindow: dependencies[singleton: .screenLock].window, + backgroundWindowLevel: .background + ) + } + + try await AppSetup.performDatabaseMigrations(using: dependencies) { [weak self] progress, minEstimatedTotalTime in + self?.loadingViewController?.updateProgress( + progress: progress, + minEstimatedTotalTime: minEstimatedTotalTime + ) + } + try await AppSetup.postMigrationSetup(using: dependencies) + + /// Because the `SessionUIKit` target doesn't depend on the `SessionUtilitiesKit` dependency (it shouldn't + /// need to since it should just be UI) but since the theme settings are stored in the database we need to pass these through + /// to `SessionUIKit` and expose a mechanism to save updated settings - this is done here (once the migrations complete) + await MainActor.run { + SNUIKit.configure( + with: SessionSNUIKitConfig(using: dependencies), + themeSettings: dependencies.mutate(cache: .libSession) { cache -> ThemeSettings in + ( + cache.get(.theme), + cache.get(.themePrimaryColor), + cache.get(.themeMatchSystemDayNightCycle) + ) + } + ) + } + + /// Adding this to prevent new users being asked for local network permission in the wrong order in the permission chain. + /// We need to check the local nework permission status every time the app is activated to refresh the UI in Settings screen. + /// And after granting or denying a system permission request will trigger the local nework permission status check in applicationDidBecomeActive(:) + /// The only way we can check the status of local network permission will trigger the system prompt to ask for the permission. + /// So we need this to keep it the correct order of the permission chain. + /// For users who already enabled the calls permission and made calls, the local network permission should already be asked for. + /// It won't affect anything. + dependencies[defaults: .standard, key: .hasRequestedLocalNetworkPermission] = dependencies.mutate(cache: .libSession) { cache in + cache.get(.areCallsEnabled) + } + + /// Now that the theme settings have been applied we can complete the migrations + self.completePostMigrationSetup(calledFrom: .finishLaunching) + } + catch { + await MainActor.run { [weak self] in + self?.initialLaunchFailed = true + self?.showFailedStartupAlert( + calledFrom: .finishLaunching, + error: .databaseError(error) + ) + } + } + + /// The 'if' is only there to prevent the "variable never read" warning from showing + if backgroundTask != nil { backgroundTask = nil } + } + private func completePostMigrationSetup(calledFrom lifecycleMethod: LifecycleMethod) { Log.info(.cat, "Migrations completed, performing setup and ensuring rootViewController") dependencies[singleton: .jobRunner].setExecutor(SyncPushTokensJob.self, for: .syncPushTokens) @@ -595,7 +590,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD message: "databaseErrorRestoreDataWarning".localized(), preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "clear".localized(), style: .destructive) { _ in + alert.addAction(UIAlertAction(title: "clear".localized(), style: .destructive) { [weak self] _ in // Reset the current database for a clean migration dependencies[singleton: .storage].resetForCleanMigration() @@ -603,29 +598,27 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD TopBannerController.hide() // The re-run the migration (should succeed since there is no data) - AppSetup.runPostSetupMigrations( - migrationProgressChanged: { [weak self] progress, minEstimatedTotalTime in - self?.loadingViewController?.updateProgress( - progress: progress, - minEstimatedTotalTime: minEstimatedTotalTime - ) - }, - migrationsCompletion: { [weak self] result in - switch result { - case .failure: - DispatchQueue.main.async { - self?.showFailedStartupAlert( - calledFrom: lifecycleMethod, - error: .failedToRestore - ) - } - - case .success: - self?.completePostMigrationSetup(calledFrom: lifecycleMethod) + Task(priority: .userInitiated) { [weak self] in + do { + try await AppSetup.performDatabaseMigrations(using: dependencies) { [weak self] progress, minEstimatedTotalTime in + self?.loadingViewController?.updateProgress( + progress: progress, + minEstimatedTotalTime: minEstimatedTotalTime + ) } - }, - using: dependencies - ) + try await AppSetup.postMigrationSetup(using: dependencies) + + self?.completePostMigrationSetup(calledFrom: lifecycleMethod) + } + catch { + await MainActor.run { + self?.showFailedStartupAlert( + calledFrom: lifecycleMethod, + error: .failedToRestore + ) + } + } + } }) alert.addAction(UIAlertAction(title: "cancel".localized(), style: .default) { _ in @@ -704,7 +697,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD self?.startPollersIfNeeded() - SessionNetworkAPI.client.initialize(using: dependencies) + Task { await dependencies[singleton: .sessionNetworkApiClient].fetchInfoInBackground() } if dependencies[singleton: .appContext].isMainApp { DispatchQueue.main.async { @@ -797,7 +790,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } // Navigate to the approriate screen depending on the onboarding state - dependencies.warmCache(cache: .onboarding) + dependencies.warm(cache: .onboarding) switch dependencies[cache: .onboarding].state { case .noUser, .noUserInvalidKeyPair, .noUserInvalidSeedGeneration: @@ -984,22 +977,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// Start the pollers on a background thread so that any database queries they need to run don't /// block the main thread - DispatchQueue.global(qos: .background).async { [dependencies] in - dependencies[singleton: .currentUserPoller].startIfNeeded() - dependencies.mutate(cache: .groupPollers) { $0.startAllPollers() } - dependencies.mutate(cache: .communityPollers) { $0.startAllPollers() } + Task(priority: .userInitiated) { [dependencies] in + await dependencies[singleton: .currentUserPoller].startIfNeeded() + await dependencies[singleton: .groupPollerManager].startAllPollers() + await dependencies[singleton: .communityPollerManager].startAllPollers() } } public func stopPollers(shouldStopUserPoller: Bool = true) { guard dependencies[cache: .onboarding].state == .completed else { return } - if shouldStopUserPoller { - dependencies[singleton: .currentUserPoller].stop() + Task(priority: .userInitiated) { [dependencies] in + if shouldStopUserPoller { + await dependencies[singleton: .currentUserPoller].stop() + } + + await dependencies[singleton: .groupPollerManager].stopAndRemoveAllPollers() + await dependencies[singleton: .communityPollerManager].stopAndRemoveAllPollers() } - - dependencies.mutate(cache: .groupPollers) { $0.stopAndRemoveAllPollers() } - dependencies.mutate(cache: .communityPollers) { $0.stopAndRemoveAllPollers() } } // MARK: - App Link diff --git a/Session/Meta/Session+SNUIKit.swift b/Session/Meta/Session+SNUIKit.swift index 2c7a2aedfe..242f7009e3 100644 --- a/Session/Meta/Session+SNUIKit.swift +++ b/Session/Meta/Session+SNUIKit.swift @@ -4,6 +4,7 @@ import UIKit import AVFoundation import SessionUIKit import SessionNetworkingKit +import SessionMessagingKit import SessionUtilitiesKit // MARK: - SessionSNUIKitConfig diff --git a/Session/Meta/SessionApp.swift b/Session/Meta/SessionApp.swift index 412d4be4b3..50d31a602f 100644 --- a/Session/Meta/SessionApp.swift +++ b/Session/Meta/SessionApp.swift @@ -126,16 +126,14 @@ public class SessionApp: SessionAppType { homeViewController.present(navigationController, animated: true, completion: nil) } - public func resetData(onReset: (() -> ())) { + public func resetData(onReset: (() async -> ())) async { homeViewController = nil dependencies.remove(cache: .general) dependencies.remove(cache: .snodeAPI) dependencies.remove(cache: .libSession) - dependencies.mutate(cache: .libSessionNetwork) { - $0.suspendNetworkAccess() - $0.clearSnodeCache() - $0.clearCallbacks() - } + await dependencies[singleton: .network].suspendNetworkAccess() + await dependencies[singleton: .network].clearCache() + dependencies.remove(singleton: .network) dependencies[singleton: .storage].resetAllStorage() dependencies[singleton: .extensionHelper].deleteCache() dependencies[singleton: .displayPictureManager].resetStorage() @@ -144,14 +142,14 @@ public class SessionApp: SessionAppType { try? dependencies[singleton: .keychain].removeAll() UserDefaults.removeAll(using: dependencies) - onReset() + await onReset() LibSession.clearLoggers() Log.info("Data Reset Complete.") Log.flush() /// Wait until the next run loop to kill the app (hoping to avoid a crash due to the connection closes /// triggering logs) - DispatchQueue.main.async { + await MainActor.run { exit(0) } } @@ -252,10 +250,10 @@ public protocol SessionAppType { animated: Bool ) func createNewConversation() - func resetData(onReset: (() -> ())) + func resetData(onReset: (() async -> ())) async func showPromotedScreen() } public extension SessionAppType { - func resetData() { resetData(onReset: {}) } + func resetData() async { await resetData(onReset: {}) } } diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 8a64f2e822..9ade44ceed 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -301,13 +301,15 @@ public class PushRegistrationManager: NSObject, PKPushRegistryDelegate { Log.info(.calls, "Succeeded to report incoming call to CallKit") dependencies[singleton: .storage].resumeDatabaseAccess() - dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() } + Task { [network = dependencies[singleton: .network]] in await network.resumeNetworkAccess() } dependencies[singleton: .jobRunner].appDidBecomeActive() - dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [dependencies] in + dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [poller = dependencies[singleton: .currentUserPoller]] in // NOTE: Just start 1-1 poller so that it won't wait for polling group messages - dependencies[singleton: .currentUserPoller].startIfNeeded(forceStartInBackground: true) + Task(priority: .userInitiated) { [poller] in + await poller.startIfNeeded(forceStartInBackground: true) + } } } } diff --git a/Session/Path/PathStatusView.swift b/Session/Path/PathStatusView.swift index 33108de585..8fe4d81ce5 100644 --- a/Session/Path/PathStatusView.swift +++ b/Session/Path/PathStatusView.swift @@ -31,7 +31,7 @@ final class PathStatusView: UIView { private let dependencies: Dependencies private let size: Size - private var disposables: Set = Set() + private var statusObservationTask: Task? init(size: Size = .small, using dependencies: Dependencies) { self.dependencies = dependencies @@ -41,13 +41,17 @@ final class PathStatusView: UIView { setUpViewHierarchy() setStatus(to: .unknown) // Default to the unknown status - registerObservers() + startObservingNetwork() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + statusObservationTask?.cancel() + } + // MARK: - Layout private func setUpViewHierarchy() { @@ -72,24 +76,28 @@ final class PathStatusView: UIView { // MARK: - Functions - private func registerObservers() { - /// Register for status updates (will be called immediately with current status) - dependencies[cache: .libSessionNetwork].networkStatus - .receive(on: DispatchQueue.main, using: dependencies) - .sink( - receiveCompletion: { [weak self] _ in - /// If the stream completes it means the network cache was reset in which case we want to - /// re-register for updates in the next run loop (as the new cache should be created by then) - DispatchQueue.global(qos: .background).async { - self?.registerObservers() + private func startObservingNetwork() { + statusObservationTask?.cancel() + statusObservationTask = Task.detached(priority: .background) { [weak self, dependencies] in + var specificNetworkObservationTask: Task? + + for await network in dependencies.stream(singleton: .network) { + specificNetworkObservationTask?.cancel() + specificNetworkObservationTask = Task { + do { + for await status in network.networkStatus { + try Task.checkCancellation() + + await self?.setStatus(to: status) + } } - }, - receiveValue: { [weak self] status in self?.setStatus(to: status) } - ) - .store(in: &disposables) + catch { Log.info("PathStatusView networkStatus observation ended, restarting.") } + } + } + } } - private func setStatus(to status: NetworkStatus) { + @MainActor private func setStatus(to status: NetworkStatus) { themeBackgroundColor = status.themeColor layer.themeShadowColor = status.themeColor } diff --git a/Session/Path/PathVC.swift b/Session/Path/PathVC.swift index 130224d297..f82a7f0b20 100644 --- a/Session/Path/PathVC.swift +++ b/Session/Path/PathVC.swift @@ -14,8 +14,7 @@ final class PathVC: BaseVC { private static let rowHeight: CGFloat = (isIPhone5OrSmaller ? 52 : 75) private let dependencies: Dependencies - private var lastPath: [LibSession.Snode] = [] - private var disposables: Set = Set() + private var statusObservationTask: Task? // MARK: - Components @@ -65,6 +64,10 @@ final class PathVC: BaseVC { fatalError("init(coder:) has not been implemented") } + deinit { + statusObservationTask?.cancel() + } + // MARK: - Lifecycle override func viewDidLoad() { @@ -72,6 +75,7 @@ final class PathVC: BaseVC { setUpNavBar() setUpViewHierarchy() + startObservingNetwork() } private func setUpNavBar() { @@ -128,44 +132,66 @@ final class PathVC: BaseVC { // Set up spacer constraints topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor).isActive = true - - // Register for path country updates - dependencies[cache: .ip2Country].cacheLoaded - .receive(on: DispatchQueue.main, using: dependencies) - .sink(receiveValue: { [weak self] _ in - switch (self?.lastPath, self?.lastPath.isEmpty == true) { - case (.none, _), (_, true): self?.update(paths: [], force: true) - case (.some(let lastPath), _): self?.update(paths: [lastPath], force: true) - } - }) - .store(in: &disposables) - - // Register for network updates - registerNetworkObservables() } // MARK: - Updating - private func registerNetworkObservables() { - /// Register for status updates (will be called immediately with current paths) - dependencies[cache: .libSessionNetwork].paths - .subscribe(on: DispatchQueue.global(qos: .background), using: dependencies) - .receive(on: DispatchQueue.main, using: dependencies) - .sink( - receiveCompletion: { [weak self] _ in - /// If the stream completes it means the network cache was reset in which case we want to - /// re-register for updates in the next run loop (as the new cache should be created by then) - DispatchQueue.global(qos: .background).async { - self?.registerNetworkObservables() + private func startObservingNetwork() { + statusObservationTask?.cancel() + statusObservationTask = Task { [weak self, dependencies] in + var specificNetworkObservationTask: Task? + + for await network in dependencies.stream(singleton: .network) { + specificNetworkObservationTask?.cancel() + specificNetworkObservationTask = Task { + do { + for await _ in network.networkStatus { + try Task.checkCancellation() + + await self?.loadPathsAsync() + } } - }, - receiveValue: { [weak self] paths in self?.update(paths: paths, force: false) } - ) - .store(in: &disposables) + catch { Log.info("PathStatusView networkStatus observation ended, restarting.") } + } + } + } + } + + private func loadPathsAsync() async { + let userSessionId: SessionId = dependencies[cache: .general].sessionId + + guard + let currentUserSwarmPubkeys: Set = try? await Set(dependencies[singleton: .network] + .getSwarm(for: userSessionId.hexString) + .map { $0.ed25519PubkeyHex }), + let paths: [LibSession.Path] = try? await dependencies[singleton: .network].getActivePaths(), + let targetPath: LibSession.Path = paths + /// Sanity check to make sure the sorting doesn't crash + .filter({ !$0.nodes.isEmpty }) + /// Deterministic ordering + .sorted(by: { $0.nodes[0].ed25519PubkeyHex < $1.nodes[0].ed25519PubkeyHex }) + .first(where: { path in + switch path.category { + case .standard: return true + case .download, .upload: return false + case .none: + guard let pubkey: String = path.destinationPubkey else { + return false + } + + return currentUserSwarmPubkeys.contains(pubkey) + } + }) + else { + self.update(path: nil, force: true) + return + } + + self.update(path: targetPath, force: true) } - private func update(paths: [[LibSession.Snode]], force: Bool) { - guard let pathToDisplay: [LibSession.Snode] = paths.first else { + @MainActor private func update(path: LibSession.Path?, force: Bool) { + guard let pathToDisplay: LibSession.Path = path else { pathStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } spinner.startAnimating() @@ -174,37 +200,44 @@ final class PathVC: BaseVC { } return } - guard force || lastPath != pathToDisplay else { return } // Cache the path that was used to avoid recreating the UI if not needed - lastPath = pathToDisplay pathStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - let dotAnimationRepeatInterval = Double(pathToDisplay.count) + 2 - let snodeRows: [UIStackView] = pathToDisplay.enumerated().map { index, snode in - let isGuardSnode = (snode == pathToDisplay.first) + let dotAnimationRepeatInterval = Double(pathToDisplay.nodes.count) + 2 + let snodeRows: [UIStackView] = pathToDisplay.nodes.enumerated().map { index, snode in + let isGuardSnode = (snode == pathToDisplay.nodes.first) return getPathRow( - snode: snode, + title: (isGuardSnode ? + "onionRoutingPathEntryNode".localized() : + "onionRoutingPathServiceNode".localized() + ), + subtitleResolver: { [ip2Country = dependencies[singleton: .ip2Country]] in + try? await Task.sleep(for: .seconds(5), checkingEvery: .milliseconds(100)) { + await ip2Country.isLoaded + } + + return await ip2Country.country(for: snode.ip) + }, location: .middle, dotAnimationStartDelay: Double(index) + 2, - dotAnimationRepeatInterval: dotAnimationRepeatInterval, - isGuardSnode: isGuardSnode + dotAnimationRepeatInterval: dotAnimationRepeatInterval ) } let youRow = getPathRow( title: "you".localized(), - subtitle: nil, + subtitleResolver: nil, location: .top, dotAnimationStartDelay: 1, dotAnimationRepeatInterval: dotAnimationRepeatInterval ) let destinationRow = getPathRow( title: "onionRoutingPathDestination".localized(), - subtitle: nil, + subtitleResolver: nil, location: .bottom, - dotAnimationStartDelay: Double(pathToDisplay.count) + 2, + dotAnimationStartDelay: Double(pathToDisplay.nodes.count) + 2, dotAnimationRepeatInterval: dotAnimationRepeatInterval ) let rows = [ youRow ] + snodeRows + [ destinationRow ] @@ -218,7 +251,13 @@ final class PathVC: BaseVC { // MARK: - General - private func getPathRow(title: String, subtitle: String?, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double) -> UIStackView { + private func getPathRow( + title: String, + subtitleResolver: (() async -> String)?, + location: LineView.Location, + dotAnimationStartDelay: Double, + dotAnimationRepeatInterval: Double + ) -> UIStackView { let lineView = LineView( location: location, dotAnimationStartDelay: dotAnimationStartDelay, @@ -237,13 +276,17 @@ final class PathVC: BaseVC { let titleStackView = UIStackView(arrangedSubviews: [ titleLabel ]) titleStackView.axis = .vertical - if let subtitle = subtitle { + if let subtitleResolver: () async -> String = subtitleResolver { let subtitleLabel = UILabel() subtitleLabel.font = .systemFont(ofSize: Values.verySmallFontSize) - subtitleLabel.text = subtitle + subtitleLabel.text = "resolving".localized() subtitleLabel.themeTextColor = .textPrimary subtitleLabel.lineBreakMode = .byTruncatingTail titleStackView.addArrangedSubview(subtitleLabel) + + Task { [weak subtitleLabel] in + subtitleLabel?.text = await subtitleResolver() + } } let stackView = UIStackView(arrangedSubviews: [ lineView, titleStackView ]) @@ -253,19 +296,6 @@ final class PathVC: BaseVC { return stackView } - - private func getPathRow(snode: LibSession.Snode, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView { - return getPathRow( - title: (isGuardSnode ? - "onionRoutingPathEntryNode".localized() : - "onionRoutingPathServiceNode".localized() - ), - subtitle: dependencies[cache: .ip2Country].country(for: snode.ip), - location: location, - dotAnimationStartDelay: dotAnimationStartDelay, - dotAnimationRepeatInterval: dotAnimationRepeatInterval - ) - } // MARK: - Interaction @@ -279,13 +309,14 @@ final class PathVC: BaseVC { // MARK: - Line View private final class LineView: UIView { + private let dependencies: Dependencies private let location: Location private let dotAnimationStartDelay: Double private let dotAnimationRepeatInterval: Double private var dotViewWidthConstraint: NSLayoutConstraint! private var dotViewHeightConstraint: NSLayoutConstraint! private var dotViewAnimationTimer: Timer! - private var disposables: Set = Set() + private var statusObservationTask: Task? enum Location { case top, middle, bottom @@ -294,6 +325,7 @@ private final class LineView: UIView { // MARK: - Initialization init(location: Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, using dependencies: Dependencies) { + self.dependencies = dependencies self.location = location self.dotAnimationStartDelay = dotAnimationStartDelay self.dotAnimationRepeatInterval = dotAnimationRepeatInterval @@ -301,7 +333,7 @@ private final class LineView: UIView { super.init(frame: CGRect.zero) setUpViewHierarchy() - registerObservers(using: dependencies) + startObservingNetwork() } override init(frame: CGRect) { @@ -313,6 +345,7 @@ private final class LineView: UIView { } deinit { + statusObservationTask?.cancel() dotViewAnimationTimer?.invalidate() } @@ -375,21 +408,25 @@ private final class LineView: UIView { } } - private func registerObservers(using dependencies: Dependencies) { - /// Register for status updates (will be called immediately with current status) - dependencies[cache: .libSessionNetwork].networkStatus - .receive(on: DispatchQueue.main, using: dependencies) - .sink( - receiveCompletion: { [weak self] _ in - /// If the stream completes it means the network cache was reset in which case we want to - /// re-register for updates in the next run loop (as the new cache should be created by then) - DispatchQueue.global(qos: .background).async { - self?.registerObservers(using: dependencies) + private func startObservingNetwork() { + statusObservationTask?.cancel() + statusObservationTask = Task { [weak self, dependencies] in + var specificNetworkObservationTask: Task? + + for await network in dependencies.stream(singleton: .network) { + specificNetworkObservationTask?.cancel() + specificNetworkObservationTask = Task { + do { + for await status in network.networkStatus { + try Task.checkCancellation() + + self?.setStatus(to: status) + } } - }, - receiveValue: { [weak self] status in self?.setStatus(to: status) } - ) - .store(in: &disposables) + catch { Log.info("PathStatusView networkStatus observation ended, restarting.") } + } + } + } } private func animate() { @@ -415,7 +452,7 @@ private final class LineView: UIView { } } - private func setStatus(to status: NetworkStatus) { + @MainActor private func setStatus(to status: NetworkStatus) { dotView.themeBackgroundColor = status.themeColor dotView.layer.themeShadowColor = status.themeColor } diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index d007a2097e..436d6f7a80 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -1161,9 +1161,9 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Log.info("[DevSettings] Swapping to \(String(describing: updatedNetwork)), clearing data") /// Stop all pollers - dependencies[singleton: .currentUserPoller].stop() - dependencies.remove(cache: .groupPollers) - dependencies.remove(cache: .communityPollers) + dependencies.remove(singleton: .currentUserPoller) + dependencies.remove(singleton: .groupPollerManager) + dependencies.remove(singleton: .communityPollerManager) /// Reset the network /// @@ -1225,7 +1225,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, dependencies.set(feature: .serviceNetwork, to: updatedNetwork) /// Start the new network cache and clear out the old one - dependencies.warmCache(cache: .libSessionNetwork) + dependencies.warm(singleton: .network) /// Free the `oldNetworkCache` so it can be destroyed(the 'if' is only there to prevent the "variable never read" warning) if oldNetworkCache != nil { oldNetworkCache = nil } diff --git a/Session/Utilities/BackgroundPoller.swift b/Session/Utilities/BackgroundPoller.swift index b03942a719..957b03985d 100644 --- a/Session/Utilities/BackgroundPoller.swift +++ b/Session/Utilities/BackgroundPoller.swift @@ -17,208 +17,195 @@ public extension Log.Category { // MARK: - BackgroundPoller -public final class BackgroundPoller { - typealias Pollers = ( - currentUser: CurrentUserPoller, - groups: [GroupPoller], - communities: [CommunityPoller] - ) - - public func poll(using dependencies: Dependencies) -> AnyPublisher { - let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 +public actor BackgroundPoller { + public func poll(using dependencies: Dependencies) async -> Bool { + typealias PollerData = ( + groupIds: Set, + servers: Set, + rooms: [String] + ) - return dependencies[singleton: .storage] - .readPublisher { db -> (Set, Set, [String]) in - ( - try ClosedGroup - .select(.threadId) - .joining( - required: ClosedGroup.members - .filter(GroupMember.Columns.profileId == dependencies[cache: .general].sessionId.hexString) - ) - .asRequest(of: String.self) - .fetchSet(db), - /// The default room promise creates an OpenGroup with an empty `roomToken` value, we - /// don't want to start a poller for this as the user hasn't actually joined a room - /// - /// We also want to exclude any rooms which have failed to poll too many times in a row from - /// the background poll as they are likely to fail again - try OpenGroup - .select(.server) - .filter( - OpenGroup.Columns.roomToken != "" && - OpenGroup.Columns.isActive && - OpenGroup.Columns.pollFailureCount < CommunityPoller.maxRoomFailureCountForBackgroundPoll - ) - .distinct() - .asRequest(of: String.self) - .fetchSet(db), - try OpenGroup - .select(.roomToken) - .filter( - OpenGroup.Columns.roomToken != "" && - OpenGroup.Columns.isActive && - OpenGroup.Columns.pollFailureCount < CommunityPoller.maxRoomFailureCountForBackgroundPoll - ) - .distinct() - .asRequest(of: String.self) - .fetchAll(db) - ) - } - .catch { _ in Just(([], [], [])).eraseToAnyPublisher() } - .handleEvents( - receiveOutput: { groupIds, servers, rooms in - Log.info(.backgroundPoller, "Fetching Users: 1, Groups: \(groupIds.count), Communities: \(servers.count) (\(rooms.count) room(s)).") - } - ) - .map { groupIds, servers, _ -> Pollers in - let currentUserPoller: CurrentUserPoller = CurrentUserPoller( - pollerName: "Background Main Poller", - pollerQueue: DispatchQueue.main, - pollerDestination: .swarm(dependencies[cache: .general].sessionId.hexString), - pollerDrainBehaviour: .limitedReuse(count: 6), - namespaces: CurrentUserPoller.namespaces, - shouldStoreMessages: true, - logStartAndStopCalls: false, - using: dependencies - ) - let groupPollers: [GroupPoller] = groupIds.map { groupId in - GroupPoller( - pollerName: "Background Group poller for: \(groupId)", // stringlint:ignore - pollerQueue: DispatchQueue.main, - pollerDestination: .swarm(groupId), - pollerDrainBehaviour: .alwaysRandom, - namespaces: GroupPoller.namespaces(swarmPublicKey: groupId), - shouldStoreMessages: true, - logStartAndStopCalls: false, - using: dependencies + let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + let maybeData: PollerData? = try? await dependencies[singleton: .storage].readAsync { db in + ( + try ClosedGroup + .select(.threadId) + .joining( + required: ClosedGroup.members + .filter(GroupMember.Columns.profileId == dependencies[cache: .general].sessionId.hexString) ) - } - let communityPollers: [CommunityPoller] = servers.map { server in - CommunityPoller( - pollerName: "Background Community poller for: \(server)", // stringlint:ignore - pollerQueue: DispatchQueue.main, - pollerDestination: .server(server), - failureCount: 0, - shouldStoreMessages: true, - logStartAndStopCalls: false, - using: dependencies + .asRequest(of: String.self) + .fetchSet(db), + /// The default room promise creates an OpenGroup with an empty `roomToken` value, we + /// don't want to start a poller for this as the user hasn't actually joined a room + /// + /// We also want to exclude any rooms which have failed to poll too many times in a row from + /// the background poll as they are likely to fail again + try OpenGroup + .select(.server) + .filter( + OpenGroup.Columns.roomToken != "" && + OpenGroup.Columns.isActive && + OpenGroup.Columns.pollFailureCount < CommunityPoller.maxRoomFailureCountForBackgroundPoll ) - } - - return (currentUserPoller, groupPollers, communityPollers) - } - .flatMap { currentUserPoller, groupPollers, communityPollers in - /// Need to map back to the pollers to ensure they don't get released until after the polling finishes - Publishers.MergeMany( - [BackgroundPoller.pollUserMessages(poller: currentUserPoller, using: dependencies)] - .appending(contentsOf: BackgroundPoller.poll(pollers: groupPollers, using: dependencies)) - .appending(contentsOf: BackgroundPoller.poll(pollerInfo: communityPollers, using: dependencies)) - ) - .collect() - .map { _ in (currentUserPoller, groupPollers, communityPollers) } - } - .map { _ in () } - .handleEvents( - receiveOutput: { _ in - let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - let duration: TimeUnit = .seconds(endTime - pollStart) - Log.info(.backgroundPoller, "Finished polling after \(duration, unit: .s).") - } + .distinct() + .asRequest(of: String.self) + .fetchSet(db), + try OpenGroup + .select(.roomToken) + .filter( + OpenGroup.Columns.roomToken != "" && + OpenGroup.Columns.isActive && + OpenGroup.Columns.pollFailureCount < CommunityPoller.maxRoomFailureCountForBackgroundPoll + ) + .distinct() + .asRequest(of: String.self) + .fetchAll(db) + ) + } + + guard let data: PollerData = maybeData else { return false } + + Log.info(.backgroundPoller, "Fetching Users: 1, Groups: \(data.groupIds.count), Communities: \(data.servers.count) (\(data.rooms.count) room(s)).") + let currentUserPoller: CurrentUserPoller = CurrentUserPoller( + pollerName: "Background Main Poller", + destination: .swarm(dependencies[cache: .general].sessionId.hexString), + swarmDrainStrategy: .alwaysRandom, + namespaces: CurrentUserPoller.namespaces, + shouldStoreMessages: true, + logStartAndStopCalls: false, + using: dependencies + ) + let groupPollers: [GroupPoller] = data.groupIds.map { groupId in + GroupPoller( + pollerName: "Background Group poller for: \(groupId)", // stringlint:ignore + destination: .swarm(groupId), + swarmDrainStrategy: .alwaysRandom, + namespaces: GroupPoller.namespaces(swarmPublicKey: groupId), + shouldStoreMessages: true, + logStartAndStopCalls: false, + using: dependencies + ) + } + let communityPollers: [CommunityPoller] = data.servers.map { server in + CommunityPoller( + pollerName: "Background Community poller for: \(server)", // stringlint:ignore + destination: .server(server), + failureCount: 0, + shouldStoreMessages: true, + logStartAndStopCalls: false, + using: dependencies + ) + } + + let hadMessages: Bool = await withTaskGroup { group in + BackgroundPoller.pollUserMessages( + poller: currentUserPoller, + in: &group, + using: dependencies ) - .eraseToAnyPublisher() + BackgroundPoller.poll( + pollers: groupPollers, + in: &group, + using: dependencies + ) + BackgroundPoller.poll( + pollerInfo: communityPollers, + in: &group, + using: dependencies + ) + + return await group.reduce(false) { $0 || $1 } + } + + let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + let duration: TimeUnit = .seconds(endTime - pollStart) + Log.info(.backgroundPoller, "Finished polling after \(duration, unit: .s).") + + return hadMessages } private static func pollUserMessages( poller: CurrentUserPoller, + in group: inout TaskGroup, using dependencies: Dependencies - ) -> AnyPublisher { - let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - - return poller - .pollFromBackground() - .handleEvents( - receiveOutput: { [pollerName = poller.pollerName] _, _, validMessageCount, _ in - let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - let duration: TimeUnit = .seconds(endTime - pollStart) - Log.info(.backgroundPoller, "\(pollerName) received \(validMessageCount) valid message(s) after \(duration, unit: .s).") - }, - receiveCompletion: { [pollerName = poller.pollerName] result in - switch result { - case .finished: break - case .failure(let error): - let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - let duration: TimeUnit = .seconds(endTime - pollStart) - Log.error(.backgroundPoller, "\(pollerName) failed after \(duration, unit: .s) due to error: \(error).") - } - } - ) - .map { _ in () } - .catch { _ in Just(()).eraseToAnyPublisher() } - .eraseToAnyPublisher() + ) { + group.addTask { + let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + + do { + let validMessageCount: Int = try await poller.pollFromBackground().validMessageCount + let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + let duration: TimeUnit = .seconds(endTime - pollStart) + Log.info(.backgroundPoller, "\(await poller.pollerName) received \(validMessageCount) valid message(s) after \(duration, unit: .s).") + + return (validMessageCount > 0) + } + catch { + let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + let duration: TimeUnit = .seconds(endTime - pollStart) + Log.error(.backgroundPoller, "\(await poller.pollerName) failed after \(duration, unit: .s) due to error: \(error).") + + return false + } + } } private static func poll( pollers: [GroupPoller], + in group: inout TaskGroup, using dependencies: Dependencies - ) -> [AnyPublisher] { + ) { // Fetch all closed groups (excluding any don't contain the current user as a // GroupMemeber as the user is no longer a member of those) - return pollers.map { poller in - let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - - return poller - .pollFromBackground() - .handleEvents( - receiveOutput: { [pollerName = poller.pollerName] _, _, validMessageCount, _ in - let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - let duration: TimeUnit = .seconds(endTime - pollStart) - Log.info(.backgroundPoller, "\(pollerName) received \(validMessageCount) valid message(s) after \(duration, unit: .s).") - }, - receiveCompletion: { [pollerName = poller.pollerName] result in - switch result { - case .finished: break - case .failure(let error): - let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - let duration: TimeUnit = .seconds(endTime - pollStart) - Log.error(.backgroundPoller, "\(pollerName) failed after \(duration, unit: .s) due to error: \(error).") - } - } - ) - .map { _ in () } - .catch { _ in Just(()).eraseToAnyPublisher() } - .eraseToAnyPublisher() + for poller in pollers { + group.addTask { + let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + + do { + let validMessageCount: Int = try await poller.pollFromBackground().validMessageCount + let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + let duration: TimeUnit = .seconds(endTime - pollStart) + Log.info(.backgroundPoller, "\(await poller.pollerName) received \(validMessageCount) valid message(s) after \(duration, unit: .s).") + + return (validMessageCount > 0) + } + catch { + let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + let duration: TimeUnit = .seconds(endTime - pollStart) + Log.error(.backgroundPoller, "\(await poller.pollerName) failed after \(duration, unit: .s) due to error: \(error).") + + return false + } + } } } private static func poll( pollerInfo: [CommunityPoller], + in group: inout TaskGroup, using dependencies: Dependencies - ) -> [AnyPublisher] { - return pollerInfo.map { poller -> AnyPublisher in - let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - - return poller - .pollFromBackground() - .handleEvents( - receiveOutput: { [pollerName = poller.pollerName] _, _, rawMessageCount, _ in - let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - let duration: TimeUnit = .seconds(endTime - pollStart) - Log.info(.backgroundPoller, "\(pollerName) received \(rawMessageCount) message(s) succeeded after \(duration, unit: .s).") - }, - receiveCompletion: { [pollerName = poller.pollerName] result in - switch result { - case .finished: break - case .failure(let error): - let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - let duration: TimeUnit = .seconds(endTime - pollStart) - Log.error(.backgroundPoller, "\(pollerName) failed after \(duration, unit: .s) due to error: \(error).") - } - } - ) - .map { _ in () } - .catch { _ in Just(()).eraseToAnyPublisher() } - .eraseToAnyPublisher() + ) { + for poller in pollerInfo { + group.addTask { + let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + + do { + let rawMessageCount: Int = try await poller.pollFromBackground().rawMessageCount + let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + let duration: TimeUnit = .seconds(endTime - pollStart) + Log.info(.backgroundPoller, "\(await poller.pollerName) received \(rawMessageCount) message(s) succeeded after \(duration, unit: .s).") + + return (rawMessageCount > 0) + } + catch { + let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + let duration: TimeUnit = .seconds(endTime - pollStart) + Log.error(.backgroundPoller, "\(await poller.pollerName) failed after \(duration, unit: .s) due to error: \(error).") + + return false + } + } } } } diff --git a/Session/Utilities/IP2Country.swift b/Session/Utilities/IP2Country.swift index 56b9102bba..350603a1b5 100644 --- a/Session/Utilities/IP2Country.swift +++ b/Session/Utilities/IP2Country.swift @@ -10,12 +10,10 @@ import SessionUtilitiesKit // MARK: - Cache -public extension Cache { - static let ip2Country: CacheConfig = Dependencies.create( +public extension Singleton { + static let ip2Country: SingletonConfig = Dependencies.create( identifier: "ip2Country", - createInstance: { dependencies in IP2Country(using: dependencies) }, - mutableInstance: { $0 }, - immutableInstance: { $0 } + createInstance: { dependencies in IP2Country(using: dependencies) } ) } @@ -27,9 +25,8 @@ public extension Log.Category { // MARK: - IP2Country -fileprivate class IP2Country: IP2CountryCacheType { +fileprivate actor IP2Country: IP2CountryType { private var countryNamesCache: [String: String] = [:] - private let _cacheLoaded: CurrentValueSubject = CurrentValueSubject(false) private var disposables: Set = Set() private var currentLocale: String { let result: String? = Locale.current.identifier @@ -44,9 +41,7 @@ fileprivate class IP2Country: IP2CountryCacheType { return (result ?? "en") // Fallback to English } - public var cacheLoaded: AnyPublisher { - _cacheLoaded.filter { $0 }.eraseToAnyPublisher() - } + public var isLoaded: Bool = false // MARK: - Tables @@ -166,91 +161,50 @@ fileprivate class IP2Country: IP2CountryCacheType { // MARK: - Initialization init(using dependencies: Dependencies) { - /// Ensure the lookup tables get loaded in the background - DispatchQueue.global(qos: .utility).async { [weak self] in - _ = self?.cache - - /// Then register for path change callbacks which will be used to update the country name cache - self?.registerNetworkObservables(using: dependencies) + Task { [weak self] in + _ = await self?.cache + await self?.setLoaded(true) + Log.info(.ip2Country, "IP2Country cache loaded.") } } // MARK: - Functions - private func registerNetworkObservables(using dependencies: Dependencies) { - /// Register for path change callbacks which will be used to update the country name cache - dependencies[cache: .libSessionNetwork].paths - .subscribe(on: DispatchQueue.global(qos: .utility), using: dependencies) - .receive(on: DispatchQueue.global(qos: .utility), using: dependencies) - .sink( - receiveCompletion: { [weak self] _ in - /// If the stream completes it means the network cache was reset in which case we want to - /// re-register for updates in the next run loop (as the new cache should be created by then) - DispatchQueue.global(qos: .background).async { - self?.registerNetworkObservables(using: dependencies) - } - }, - receiveValue: { [weak self] paths in - dependencies.mutate(cache: .ip2Country) { _ in - self?.populateCacheIfNeeded(paths: paths) - } - } - ) - .store(in: &disposables) - } - - private func populateCacheIfNeeded(paths: [[LibSession.Snode]]) { - guard !paths.isEmpty else { return } - - paths.forEach { path in - path.forEach { snode in - self.cacheCountry(for: snode.ip, inCache: &countryNamesCache) - } - } - - self._cacheLoaded.send(true) - Log.info(.ip2Country, "Update onion request path countries.") + private func setLoaded(_ loaded: Bool) { + self.isLoaded = loaded } - private func cacheCountry(for ip: String, inCache nameCache: inout [String: String]) { - let currentLocale: String = self.currentLocale // Store local copy for efficiency - - guard nameCache["\(ip)-\(currentLocale)"] == nil else { return } + public func country(for ip: String) async -> String { + guard isLoaded else { return "onionRoutingPathUnknownCountry".localized() } - guard - let ipAsInt: Int64 = IPv4.toInt(ip), - let countryBlockGeonameIdIndex: Int = cache.countryBlocksIPInt.firstIndex(where: { $0 > ipAsInt }).map({ $0 - 1 }), - let localeStartIndex: Int = cache.countryLocationsLocaleCode.firstIndex(where: { $0 == currentLocale }), - let countryNameIndex: Int = Array(cache.countryLocationsGeonameId[localeStartIndex...]).firstIndex(where: { geonameId in - geonameId == cache.countryBlocksGeonameId[countryBlockGeonameIdIndex] - }), - (localeStartIndex + countryNameIndex) < cache.countryLocationsCountryName.count - else { return } - - let result: String = cache.countryLocationsCountryName[localeStartIndex + countryNameIndex] - nameCache["\(ip)-\(currentLocale)"] = result - } - - // MARK: - Functions - - public func country(for ip: String) -> String { - guard _cacheLoaded.value else { return "resolving".localized() } + let currentLocale: String = self.currentLocale /// Store local copy for efficiency + let key: String = "\(ip)-\(currentLocale)" - return (countryNamesCache["\(ip)-\(currentLocale)"] ?? "onionRoutingPathUnknownCountry".localized()) + switch countryNamesCache[key] { + case .some(let value): return value + case .none: + guard + let ipAsInt: Int64 = IPv4.toInt(ip), + let countryBlockGeonameIdIndex: Int = cache.countryBlocksIPInt.firstIndex(where: { $0 > ipAsInt }).map({ $0 - 1 }), + let localeStartIndex: Int = cache.countryLocationsLocaleCode.firstIndex(where: { $0 == currentLocale }), + let countryNameIndex: Int = Array(cache.countryLocationsGeonameId[localeStartIndex...]).firstIndex(where: { geonameId in + geonameId == cache.countryBlocksGeonameId[countryBlockGeonameIdIndex] + }), + (localeStartIndex + countryNameIndex) < cache.countryLocationsCountryName.count + else { return "onionRoutingPathUnknownCountry".localized() } + + let result: String = cache.countryLocationsCountryName[localeStartIndex + countryNameIndex] + countryNamesCache[key] = result + + return result + } } } -// MARK: - IP2CountryCacheType - -/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way -public protocol IP2CountryImmutableCacheType: ImmutableCacheType { - var cacheLoaded: AnyPublisher { get } - - func country(for ip: String) -> String -} +// MARK: - IP2CountryType -public protocol IP2CountryCacheType: IP2CountryImmutableCacheType, MutableCacheType { - var cacheLoaded: AnyPublisher { get } +public protocol IP2CountryType { + var isLoaded: Bool { get async } - func country(for ip: String) -> String + func country(for ip: String) async -> String } diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index b2a680ed7a..7ad3fbfc69 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -220,8 +220,8 @@ public extension ClosedGroup { } /// Start the poller - dependencies.mutate(cache: .groupPollers) { - $0.getOrCreatePoller(for: group.id).startIfNeeded() + Task.detached(priority: .userInitiated) { [manager = dependencies[singleton: .groupPollerManager]] in + await manager.getOrCreatePoller(for: group.id).startIfNeeded() } /// Subscribe for group push notifications @@ -276,7 +276,9 @@ public extension ClosedGroup { if !dataToRemove.asSet().intersection([.poller, .pushNotifications, .libSessionState]).isEmpty { threadIds.forEach { threadId in if dataToRemove.contains(.poller) { - dependencies.mutate(cache: .groupPollers) { $0.stopAndRemovePoller(for: threadId) } + Task { [manager = dependencies[singleton: .groupPollerManager]] in + await manager.stopAndRemovePoller(for: threadId) + } } if dataToRemove.contains(.libSessionState) { diff --git a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift index bdfbfdd262..e642bb2ff6 100644 --- a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift +++ b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift @@ -91,12 +91,14 @@ public enum ConfigurationSyncJob: JobExecutor { let additionalTransientData: AdditionalTransientData? = (job.transientData as? AdditionalTransientData) Log.info(.cat, "For \(swarmPublicKey) started with changes: \(pendingPushes.pushData.count), old hashes: \(pendingPushes.obsoleteHashes.count)") - dependencies[singleton: .storage] - .readPublisher { db -> AuthenticationMethod in - try Authentication.with(db, swarmPublicKey: swarmPublicKey, using: dependencies) - } - .tryFlatMap { authMethod -> AnyPublisher<(ResponseInfoType, Network.BatchResponse), Error> in - try SnodeAPI.preparedSequence( + AnyPublisher + .lazy { () -> Network.PreparedRequest in + let authMethod: AuthenticationMethod = try Authentication.with( + swarmPublicKey: swarmPublicKey, + using: dependencies + ) + + return try SnodeAPI.preparedSequence( requests: [] .appending(contentsOf: additionalTransientData?.beforeSequenceRequests) .appending( @@ -131,11 +133,11 @@ public enum ConfigurationSyncJob: JobExecutor { .appending(contentsOf: additionalTransientData?.afterSequenceRequests), requireAllBatchResponses: (additionalTransientData?.requireAllBatchResponses == true), swarmPublicKey: swarmPublicKey, - snodeRetrievalRetryCount: 0, // This job has it's own retry mechanism overallTimeout: Network.defaultTimeout, using: dependencies - ).send(using: dependencies) + ) } + .flatMap { request in request.send(using: dependencies) } .subscribe(on: scheduler, using: dependencies) .receive(on: scheduler, using: dependencies) .tryMap { (_: ResponseInfoType, response: Network.BatchResponse) -> [ConfigDump] in @@ -193,37 +195,32 @@ public enum ConfigurationSyncJob: JobExecutor { // If the failure is due to being offline then we should automatically // retry if the connection is re-established - dependencies[cache: .libSessionNetwork].networkStatus - .first() - .sinkUntilComplete( - receiveValue: { status in - switch status { - // If we are currently connected then use the standard - // retry behaviour - case .connected: failure(job, error, false) - - // If not then permanently fail the job and reschedule it - // to run again if we re-establish the connection - default: - failure(job, error, true) - - dependencies[cache: .libSessionNetwork].networkStatus - .filter { $0 == .connected } - .first() - .sinkUntilComplete( - receiveCompletion: { _ in - dependencies[singleton: .storage].writeAsync { db in - ConfigurationSyncJob.enqueue( - db, - swarmPublicKey: swarmPublicKey, - using: dependencies - ) - } - } - ) - } - } - ) + Task { [dependencies] in + let currentStatus: NetworkStatus = (await dependencies[singleton: .network] + .networkStatus + .first(where: { _ in true }) ?? .unknown) + + // If we are currently connected then use the standard retry behaviour + guard currentStatus != .connected else { + return failure(job, error, false) + } + + // Otherwise we should permanently fail the job and reschedule it + // to run again if we re-establish the connection + failure(job, error, true) + + _ = await dependencies[singleton: .network].networkStatus.first(where: { + $0 == .connected + }) + + try? await dependencies[singleton: .storage].writeAsync { db in + ConfigurationSyncJob.enqueue( + db, + swarmPublicKey: swarmPublicKey, + using: dependencies + ) + } + } } }, receiveValue: { (configDumps: [ConfigDump]) in diff --git a/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift b/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift index 096d6729ff..2c9e409da0 100644 --- a/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift +++ b/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift @@ -184,7 +184,6 @@ public enum ProcessPendingGroupMemberRemovalsJob: JobExecutor { .compactMap { $0 }, requireAllBatchResponses: true, swarmPublicKey: groupSessionId.hexString, - snodeRetrievalRetryCount: 0, // Job has a built-in retry using: dependencies ) } diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index d52c44a010..57795bbf92 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -1368,7 +1368,7 @@ private extension OpenGroupAPI { private extension Network.Destination { func signed(data: OpenGroupAPI.AdditionalSigningData, body: Data?, using dependencies: Dependencies) throws -> Network.Destination { switch self { - case .snode, .randomSnode, .randomSnodeLatestNetworkTimeTarget: throw NetworkError.unauthorised + case .snode, .randomSnode: throw NetworkError.unauthorised case .cached: return self case .server(let info): return .server(info: try info.signed(data, body, using: dependencies)) case .serverUpload(let info, let fileName): diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 9cd978e457..33c5292e28 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -125,7 +125,7 @@ public final class OpenGroupManager { } // First check if there is no poller for the specified server - if Set(dependencies[cache: .communityPollers].serversBeingPolled).intersection(serverOptions).isEmpty { + if Set(dependencies[singleton: .communityPollerManager].syncState.serversBeingPolled).intersection(serverOptions).isEmpty { return false } @@ -210,7 +210,7 @@ public final class OpenGroupManager { guard successfullyAddedGroup, !dependencies[singleton: .storage].isSuspended, - !dependencies[cache: .libSessionNetwork].isSuspended + !dependencies[singleton: .network].syncState.isSuspended else { return Just(()) .setFailureType(to: Error.self) @@ -274,12 +274,12 @@ public final class OpenGroupManager { receiveCompletion: { [dependencies] result in switch result { case .finished: - // (Re)start the poller if needed (want to force it to poll immediately in the next - // run loop to avoid a big delay before the next poll) - dependencies.mutate(cache: .communityPollers) { cache in - let poller: CommunityPollerType = cache.getOrCreatePoller(for: server.lowercased()) - poller.stop() - poller.startIfNeeded() + /// (Re)start the poller if needed (want to force it to poll immediately in the next run loop to avoid + /// a big delay before the next poll) + Task { [communityPollerManager = dependencies[singleton: .communityPollerManager]] in + let poller = await communityPollerManager.getOrCreatePoller(for: server.lowercased()) + await poller.stop() + await poller.startIfNeeded() } case .failure(let error): Log.error(.openGroup, "Failed to join open group with error: \(error).") @@ -317,8 +317,8 @@ public final class OpenGroupManager { .defaulting(to: 1) if numActiveRooms == 1, let server: String = server?.lowercased() { - dependencies.mutate(cache: .communityPollers) { - $0.stopAndRemovePoller(for: server) + Task { [manager = dependencies[singleton: .communityPollerManager]] in + await manager.stopAndRemovePoller(for: server) } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift index c22b57d576..401ec56db4 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift @@ -191,9 +191,9 @@ extension MessageSender { let userSessionId: SessionId = dependencies[cache: .general].sessionId // Start polling - dependencies - .mutate(cache: .groupPollers) { $0.getOrCreatePoller(for: thread.id) } - .startIfNeeded() + Task.detached(priority: .userInitiated) { [manager = dependencies[singleton: .groupPollerManager]] in + await manager.getOrCreatePoller(for: thread.id).startIfNeeded() + } // Subscribe for push notifications (if PNs are enabled) preparedNotificationSubscription? @@ -384,7 +384,7 @@ extension MessageSender { using: dependencies ) - case .groupUpdateTo(let url, let key, let fileName): + case .groupUpdateTo(let url, let key, _): try ClosedGroup .filter(id: groupSessionId) .updateAllAndConfig( diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift index 39b2d0956f..52858b3c2b 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift @@ -8,32 +8,49 @@ import SessionUtilitiesKit // MARK: - Cache -public extension Cache { - static let communityPollers: CacheConfig = Dependencies.create( +public extension Singleton { + static let communityPollerManager: SingletonConfig = Dependencies.create( identifier: "communityPollers", - createInstance: { dependencies in CommunityPoller.Cache(using: dependencies) }, - mutableInstance: { $0 }, - immutableInstance: { $0 } + createInstance: { dependencies in CommunityPollerManager(using: dependencies) } ) } -// MARK: - CommunityPollerType +// MARK: - CommunityPoller Convenience -public protocol CommunityPollerType { - typealias PollResponse = (info: ResponseInfoType, data: Network.BatchResponseMap) - - var isPolling: Bool { get } - var receivedPollResponse: AnyPublisher { get } - - func startIfNeeded() - func stop() +public extension PollerType where PollResponse == CommunityPoller.PollResponse { + init( + pollerName: String, + destination: PollerDestination, + failureCount: Int = 0, + shouldStoreMessages: Bool, + logStartAndStopCalls: Bool, + customAuthMethod: AuthenticationMethod? = nil, + using dependencies: Dependencies + ) { + self.init( + pollerName: pollerName, + destination: destination, + swarmDrainStrategy: .alwaysRandom, + namespaces: [], + failureCount: failureCount, + shouldStoreMessages: shouldStoreMessages, + logStartAndStopCalls: logStartAndStopCalls, + customAuthMethod: customAuthMethod, + using: dependencies + ) + } } // MARK: - CommunityPoller private typealias Capabilities = OpenGroupAPI.Capabilities -public final class CommunityPoller: CommunityPollerType & PollerType { +public actor CommunityPoller: PollerType { + public typealias PollResponse = ( + info: ResponseInfoType, + data: Network.BatchResponseMap + ) + // MARK: - Settings private static let minPollInterval: TimeInterval = 3 @@ -51,220 +68,206 @@ public final class CommunityPoller: CommunityPollerType & PollerType { // MARK: - PollerType public let dependencies: Dependencies - public let pollerQueue: DispatchQueue public let pollerName: String - public let pollerDestination: PollerDestination + public let destination: PollerDestination public let logStartAndStopCalls: Bool - public var receivedPollResponse: AnyPublisher { - receivedPollResponseSubject.eraseToAnyPublisher() - } + nonisolated public var receivedPollResponse: AsyncStream { responseStream.stream } - public var isPolling: Bool = false + public var pollTask: Task? public var pollCount: Int = 0 public var failureCount: Int public var lastPollStart: TimeInterval = 0 public var cancellable: AnyCancellable? private let shouldStoreMessages: Bool - private let receivedPollResponseSubject: PassthroughSubject = PassthroughSubject() + nonisolated private let responseStream: CancellationAwareAsyncStream = CancellationAwareAsyncStream() // MARK: - Initialization - required public init( + public init( pollerName: String, - pollerQueue: DispatchQueue, - pollerDestination: PollerDestination, - pollerDrainBehaviour: ThreadSafeObject = .alwaysRandom, - namespaces: [SnodeAPI.Namespace] = [], + destination: PollerDestination, + swarmDrainStrategy: SwarmDrainer.Strategy, + namespaces: [SnodeAPI.Namespace], failureCount: Int, shouldStoreMessages: Bool, logStartAndStopCalls: Bool, customAuthMethod: AuthenticationMethod? = nil, using dependencies: Dependencies ) { + let (stream, continuation) = AsyncStream.makeStream() self.dependencies = dependencies self.pollerName = pollerName - self.pollerQueue = pollerQueue - self.pollerDestination = pollerDestination + self.destination = destination self.failureCount = failureCount self.shouldStoreMessages = shouldStoreMessages self.logStartAndStopCalls = logStartAndStopCalls } - // MARK: - Abstract Methods + deinit { + // Send completion events to the observables + Task { [stream = responseStream] in + await stream.finishCurrentStreams() + } + + pollTask?.cancel() + } + + // MARK: - PollerType - public func nextPollDelay() -> AnyPublisher { + public func nextPollDelay() async -> TimeInterval { // Arbitrary backoff factor... - return Just(min(CommunityPoller.maxPollInterval, CommunityPoller.minPollInterval + pow(2, Double(failureCount)))) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() + return min( + CommunityPoller.maxPollInterval, + (CommunityPoller.minPollInterval + pow(2, Double(failureCount))) + ) } - public func handlePollError(_ error: Error, _ lastError: Error?) -> PollerErrorResponse { + public func handlePollError(_ error: Error) async { /// We want to custom handle a '400' error code due to not having blinded auth as it likely means that we join the /// OpenGroup before blinding was enabled and need to update it's capabilities /// /// **Note:** To prevent an infinite loop caused by a server-side bug we want to prevent this capabilities request from /// happening multiple times in a row - switch (error.isMissingBlindedAuthError, lastError?.isMissingBlindedAuthError) { - case (true, .none), (true, false): break - default: - /// Save the updated failure count to the database - dependencies[singleton: .storage].writeAsync { [pollerDestination, failureCount] db in - try OpenGroup - .filter(OpenGroup.Columns.server == pollerDestination.target) - .updateAll( - db, - OpenGroup.Columns.pollFailureCount.set(to: failureCount + 1) - ) - } - return .continuePolling + guard error.isMissingBlindedAuthError else { + /// Save the updated failure count to the database + _ = try? await dependencies[singleton: .storage].writeAsync { [destination, failureCount] db in + try OpenGroup + .filter(OpenGroup.Columns.server == destination.target) + .updateAll( + db, + OpenGroup.Columns.pollFailureCount.set(to: failureCount) + ) + } + return + } + + /// Since we have gotten here we should update the SOGS capabilities before triggering the next poll + do { + let authMethod: AuthenticationMethod = try await dependencies[singleton: .storage].readAsync { [destination, dependencies] db -> AuthenticationMethod in + try Authentication.with( + db, + server: destination.target, + forceBlinded: true, + using: dependencies + ) + } + let request: Network.PreparedRequest = try OpenGroupAPI.preparedCapabilities( + authMethod: authMethod, + using: dependencies + ) + let response: OpenGroupAPI.Capabilities = try await request.send(using: dependencies) + + try await dependencies[singleton: .storage].writeAsync { [destination] db in + OpenGroupManager.handleCapabilities( + db, + capabilities: response, + on: destination.target + ) + } } - //[pollerName, pollerDestination, failureCount, dependencies] - func handleError(_ error: Error) throws -> AnyPublisher { + catch { /// Log the error first Log.error(.poller, "\(pollerName) failed to update capabilities due to error: \(error).") /// If the polling has failed 10+ times then try to prune any invalid rooms that /// aren't visible (they would have been added via config messages and will /// likely always fail but the user has no way to delete them) - guard (failureCount + 1) > CommunityPoller.maxHiddenRoomFailureCount else { + guard failureCount > CommunityPoller.maxHiddenRoomFailureCount else { /// Save the updated failure count to the database - dependencies[singleton: .storage].writeAsync { [pollerDestination, failureCount] db in + _ = try? await dependencies[singleton: .storage].writeAsync { [destination, failureCount] db in try OpenGroup - .filter(OpenGroup.Columns.server == pollerDestination.target) + .filter(OpenGroup.Columns.server == destination.target) .updateAll( db, - OpenGroup.Columns.pollFailureCount.set(to: failureCount + 1) + OpenGroup.Columns.pollFailureCount.set(to: failureCount) ) } - - throw error + return } - return dependencies[singleton: .storage] - .writePublisher { [pollerDestination, failureCount, dependencies] db -> [String] in - /// Save the updated failure count to the database - try OpenGroup - .filter(OpenGroup.Columns.server == pollerDestination.target) - .updateAll( - db, - OpenGroup.Columns.pollFailureCount.set(to: failureCount + 1) - ) - - /// Prune any hidden rooms - let roomIds: Set = try OpenGroup - .filter( - OpenGroup.Columns.server == pollerDestination.target && - OpenGroup.Columns.isActive == true - ) - .select(.roomToken) - .asRequest(of: String.self) - .fetchSet(db) - .map { OpenGroup.idFor(roomToken: $0, server: pollerDestination.target) } - .asSet() - let hiddenRoomIds: Set = try SessionThread - .select(.id) - .filter(ids: roomIds) - .filter( - SessionThread.Columns.shouldBeVisible == false || - SessionThread.Columns.pinnedPriority == LibSession.hiddenPriority - ) - .asRequest(of: String.self) - .fetchSet(db) - - try hiddenRoomIds.forEach { id in - try dependencies[singleton: .openGroupManager].delete( - db, - openGroupId: id, - /// **Note:** We pass `skipLibSessionUpdate` as `true` - /// here because we want to avoid syncing this deletion as the room might - /// not be in an invalid state on other devices - one of the other devices - /// will eventually trigger a new config update which will re-add this room - /// and hopefully at that time it'll work again - skipLibSessionUpdate: true - ) - } + let hiddenRoomIds: [String]? = try? await dependencies[singleton: .storage].writeAsync { [destination, failureCount, dependencies] db -> [String] in + /// Save the updated failure count to the database + try OpenGroup + .filter(OpenGroup.Columns.server == destination.target) + .updateAll( + db, + OpenGroup.Columns.pollFailureCount.set(to: failureCount) + ) + + /// Prune any hidden rooms + let roomIds: Set = try OpenGroup + .filter( + OpenGroup.Columns.server == destination.target && + OpenGroup.Columns.isActive == true + ) + .select(.roomToken) + .asRequest(of: String.self) + .fetchSet(db) + .map { OpenGroup.idFor(roomToken: $0, server: destination.target) } + .asSet() + let hiddenRoomIds: Set = try SessionThread + .select(.id) + .filter(ids: roomIds) + .filter( + SessionThread.Columns.shouldBeVisible == false || + SessionThread.Columns.pinnedPriority == LibSession.hiddenPriority + ) + .asRequest(of: String.self) + .fetchSet(db) - return Array(hiddenRoomIds) + try hiddenRoomIds.forEach { id in + try dependencies[singleton: .openGroupManager].delete( + db, + openGroupId: id, + /// **Note:** We pass `skipLibSessionUpdate` as `true` + /// here because we want to avoid syncing this deletion as the room might + /// not be in an invalid state on other devices - one of the other devices + /// will eventually trigger a new config update which will re-add this room + /// and hopefully at that time it'll work again + skipLibSessionUpdate: true + ) } - .handleEvents( - receiveOutput: { [pollerName, pollerDestination] hiddenRoomIds in - guard !hiddenRoomIds.isEmpty else { return } - - // Add a note to the logs that this happened - let rooms: String = hiddenRoomIds - .sorted() - .compactMap { $0.components(separatedBy: pollerDestination.target).last } - .joined(separator: ", ") - Log.error(.poller, "\(pollerName) failure count surpassed \(CommunityPoller.maxHiddenRoomFailureCount), removed hidden rooms [\(rooms)].") - } - ) - .map { _ in () } - .eraseToAnyPublisher() - } - - /// Since we have gotten here we should update the SOGS capabilities before triggering the next poll - cancellable = dependencies[singleton: .storage] - .readPublisher { [pollerDestination, dependencies] db -> AuthenticationMethod in - try Authentication.with( - db, - server: pollerDestination.target, - forceBlinded: true, - using: dependencies - ) - } - .subscribe(on: pollerQueue, using: dependencies) - .receive(on: pollerQueue, using: dependencies) - .tryMap { [dependencies] authMethod in - try OpenGroupAPI.preparedCapabilities( - authMethod: authMethod, - using: dependencies - ) - } - .flatMap { [dependencies] in $0.send(using: dependencies) } - .flatMapStorageWritePublisher(using: dependencies) { [pollerDestination] (db: ObservingDatabase, response: (info: ResponseInfoType, data: OpenGroupAPI.Capabilities)) in - OpenGroupManager.handleCapabilities( - db, - capabilities: response.data, - on: pollerDestination.target - ) + + return Array(hiddenRoomIds) } - .tryCatch { try handleError($0) } - .asResult() - .flatMapOptional { [weak self] _ in self?.nextPollDelay() } - .sink( - receiveCompletion: { _ in }, // Never called - receiveValue: { [weak self, pollerQueue, dependencies] nextPollDelay in - let nextPollInterval: TimeUnit = .seconds(nextPollDelay) - - // Schedule the next poll - pollerQueue.asyncAfter(deadline: .now() + .milliseconds(Int(nextPollInterval.timeInterval * 1000)), qos: .default, using: dependencies) { - self?.pollRecursively(error) - } - } - ) - - /// Stop polling at this point (we will resume once the above publisher completes - return .stopPolling + + guard let hiddenRoomIds: [String] = hiddenRoomIds, !hiddenRoomIds.isEmpty else { return } + + /// Add a note to the logs that this happened + let rooms: String = hiddenRoomIds + .sorted() + .compactMap { $0.components(separatedBy: destination.target).last } + .joined(separator: ", ") + Log.error(.poller, "\(pollerName) failure count surpassed \(CommunityPoller.maxHiddenRoomFailureCount), removed hidden rooms [\(rooms)].") + } } // MARK: - Polling public func pollerDidStart() {} + public func pollerReceivedResponse(_ response: PollResponse) async { + await responseStream.send(response) + } + + public func pollerDidStop() { + Task { await responseStream.finishCurrentStreams() } + } + /// Polls based on it's configuration and processes any messages, returning an array of messages that were /// successfully processed /// /// **Note:** The returned messages will have already been processed by the `Poller`, they are only returned /// for cases where we need explicit/custom behaviours to occur (eg. Onboarding) - public func poll(forceSynchronousProcessing: Bool = false) -> AnyPublisher { + public func poll(forceSynchronousProcessing: Bool = false) async throws -> PollResult { typealias PollInfo = ( roomInfo: [OpenGroupAPI.RoomInfo], lastInboxMessageId: Int64, lastOutboxMessageId: Int64, authMethod: AuthenticationMethod ) + typealias APIValue = Network.BatchResponseMap let lastSuccessfulPollTimestamp: TimeInterval = (self.lastPollStart > 0 ? lastPollStart : dependencies.mutate(cache: .openGroupManager) { cache in @@ -272,70 +275,60 @@ public final class CommunityPoller: CommunityPollerType & PollerType { } ) - return dependencies[singleton: .storage] - .readPublisher { [pollerDestination, dependencies] db -> PollInfo in - /// **Note:** The `OpenGroup` type converts to lowercase in init - let server: String = pollerDestination.target.lowercased() - let roomInfo: [OpenGroupAPI.RoomInfo] = try OpenGroup - .select(.roomToken, .infoUpdates, .sequenceNumber) + let pollInfo: PollInfo = try await dependencies[singleton: .storage].readAsync { [destination, dependencies] db in + /// **Note:** The `OpenGroup` type converts to lowercase in init + let server: String = destination.target.lowercased() + let roomInfo: [OpenGroupAPI.RoomInfo] = try OpenGroup + .select(.roomToken, .infoUpdates, .sequenceNumber) + .filter(OpenGroup.Columns.server == server) + .filter(OpenGroup.Columns.isActive == true) + .filter(OpenGroup.Columns.roomToken != "") + .asRequest(of: OpenGroupAPI.RoomInfo.self) + .fetchAll(db) + + guard !roomInfo.isEmpty else { throw OpenGroupAPIError.invalidPoll } + + return ( + roomInfo, + (try? OpenGroup + .select(.inboxLatestMessageId) .filter(OpenGroup.Columns.server == server) - .filter(OpenGroup.Columns.isActive == true) - .filter(OpenGroup.Columns.roomToken != "") - .asRequest(of: OpenGroupAPI.RoomInfo.self) - .fetchAll(db) - - guard !roomInfo.isEmpty else { throw OpenGroupAPIError.invalidPoll } - - return ( - roomInfo, - (try? OpenGroup - .select(.inboxLatestMessageId) - .filter(OpenGroup.Columns.server == server) - .asRequest(of: Int64.self) - .fetchOne(db)) - .defaulting(to: 0), - (try? OpenGroup - .select(.outboxLatestMessageId) - .filter(OpenGroup.Columns.server == server) - .asRequest(of: Int64.self) - .fetchOne(db)) - .defaulting(to: 0), - try Authentication.with(db, server: server, using: dependencies) - ) - } - .tryFlatMap { [pollCount, dependencies] pollInfo -> AnyPublisher<(ResponseInfoType, Network.BatchResponseMap), Error> in - try OpenGroupAPI - .preparedPoll( - roomInfo: pollInfo.roomInfo, - lastInboxMessageId: pollInfo.lastInboxMessageId, - lastOutboxMessageId: pollInfo.lastOutboxMessageId, - hasPerformedInitialPoll: (pollCount > 0), - timeSinceLastPoll: (dependencies.dateNow.timeIntervalSince1970 - lastSuccessfulPollTimestamp), - authMethod: pollInfo.authMethod, - using: dependencies - ) - .send(using: dependencies) - } - .flatMapOptional { [weak self, failureCount, dependencies] info, response in - self?.handlePollResponse( - info: info, - response: response, - failureCount: failureCount, - using: dependencies - ) - } - .handleEvents( - receiveOutput: { [weak self, dependencies] _ in - self?.pollCount += 1 - - dependencies.mutate(cache: .openGroupManager) { cache in - cache.setLastSuccessfulCommunityPollTimestamp( - dependencies.dateNow.timeIntervalSince1970 - ) - } - } + .asRequest(of: Int64.self) + .fetchOne(db)) + .defaulting(to: 0), + (try? OpenGroup + .select(.outboxLatestMessageId) + .filter(OpenGroup.Columns.server == server) + .asRequest(of: Int64.self) + .fetchOne(db)) + .defaulting(to: 0), + try Authentication.with(db, server: server, using: dependencies) + ) + } + let request: Network.PreparedRequest = try OpenGroupAPI.preparedPoll( + roomInfo: pollInfo.roomInfo, + lastInboxMessageId: pollInfo.lastInboxMessageId, + lastOutboxMessageId: pollInfo.lastOutboxMessageId, + hasPerformedInitialPoll: (pollCount > 0), + timeSinceLastPoll: (dependencies.dateNow.timeIntervalSince1970 - lastSuccessfulPollTimestamp), + authMethod: pollInfo.authMethod, + using: dependencies + ) + let response: (info: ResponseInfoType, value: APIValue) = try await request.send(using: dependencies) + let result: PollResult = try await handlePollResponse( + info: response.info, + response: response.value, + failureCount: failureCount, + using: dependencies + ) + pollCount += 1 + dependencies.mutate(cache: .openGroupManager) { cache in + cache.setLastSuccessfulCommunityPollTimestamp( + dependencies.dateNow.timeIntervalSince1970 ) - .eraseToAnyPublisher() + } + + return result } private func handlePollResponse( @@ -343,7 +336,7 @@ public final class CommunityPoller: CommunityPollerType & PollerType { response: Network.BatchResponseMap, failureCount: Int, using dependencies: Dependencies - ) -> AnyPublisher { + ) async throws -> PollResult { var rawMessageCount: Int = 0 let validResponses: [OpenGroupAPI.Endpoint: Any] = response.data .filter { endpoint, data in @@ -412,9 +405,7 @@ public final class CommunityPoller: CommunityPollerType & PollerType { // If there are no remaining 'validResponses' and there hasn't been a failure then there is // no need to do anything else guard !validResponses.isEmpty || failureCount != 0 else { - return Just(((info, response), rawMessageCount, 0, true)) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() + return PollResult((info, response), rawMessageCount, 0, true) } // Retrieve the current capability & group info to check if anything changed @@ -426,173 +417,164 @@ public final class CommunityPoller: CommunityPollerType & PollerType { default: return nil } } + let currentInfo: (capabilities: OpenGroupAPI.Capabilities, groups: [OpenGroup]) = try await dependencies[singleton: .storage].readAsync { [destination] db in + let allCapabilities: [Capability] = try Capability + .filter(Capability.Columns.openGroupServer == destination.target) + .fetchAll(db) + let capabilities: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities( + capabilities: allCapabilities + .filter { !$0.isMissing } + .map { $0.variant }, + missing: { + let missingCapabilities: [Capability.Variant] = allCapabilities + .filter { $0.isMissing } + .map { $0.variant } + + return (missingCapabilities.isEmpty ? nil : missingCapabilities) + }() + ) + let openGroupIds: [String] = rooms + .map { OpenGroup.idFor(roomToken: $0, server: destination.target) } + let groups: [OpenGroup] = try OpenGroup + .filter(ids: openGroupIds) + .fetchAll(db) + + return (capabilities, groups) + } - return dependencies[singleton: .storage] - .readPublisher { [pollerDestination] db -> (capabilities: OpenGroupAPI.Capabilities, groups: [OpenGroup]) in - let allCapabilities: [Capability] = try Capability - .filter(Capability.Columns.openGroupServer == pollerDestination.target) - .fetchAll(db) - let capabilities: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities( - capabilities: allCapabilities - .filter { !$0.isMissing } - .map { $0.variant }, - missing: { - let missingCapabilities: [Capability.Variant] = allCapabilities - .filter { $0.isMissing } - .map { $0.variant } - - return (missingCapabilities.isEmpty ? nil : missingCapabilities) - }() - ) - let openGroupIds: [String] = rooms - .map { OpenGroup.idFor(roomToken: $0, server: pollerDestination.target) } - let groups: [OpenGroup] = try OpenGroup - .filter(ids: openGroupIds) - .fetchAll(db) - - return (capabilities, groups) - } - .flatMap { [pollerDestination, dependencies] (capabilities: OpenGroupAPI.Capabilities, groups: [OpenGroup]) -> AnyPublisher in - let changedResponses: [OpenGroupAPI.Endpoint: Any] = validResponses - .filter { endpoint, data in - switch endpoint { - case .capabilities: - guard - let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, - let responseBody: OpenGroupAPI.Capabilities = responseData.body - else { return false } - - return (responseBody != capabilities) - - case .roomPollInfo(let roomToken, _): - guard - let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, - let responseBody: OpenGroupAPI.RoomPollInfo = responseData.body - else { return false } - guard let existingOpenGroup: OpenGroup = groups.first(where: { $0.roomToken == roomToken }) else { - return true - } - - return ( - responseBody.activeUsers != existingOpenGroup.userCount || ( - responseBody.details != nil && - responseBody.details?.infoUpdates != existingOpenGroup.infoUpdates - ) || - OpenGroup.Permissions(roomInfo: responseBody) != existingOpenGroup.permissions - ) - - default: return true - } + let changedResponses: [OpenGroupAPI.Endpoint: Any] = validResponses.filter { endpoint, data in + switch endpoint { + case .capabilities: + guard + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, + let responseBody: OpenGroupAPI.Capabilities = responseData.body + else { return false } + + return (responseBody != currentInfo.capabilities) + + case .roomPollInfo(let roomToken, _): + guard + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, + let responseBody: OpenGroupAPI.RoomPollInfo = responseData.body + else { return false } + guard let existingOpenGroup: OpenGroup = currentInfo.groups.first(where: { $0.roomToken == roomToken }) else { + return true } + + return ( + responseBody.activeUsers != existingOpenGroup.userCount || ( + responseBody.details != nil && + responseBody.details?.infoUpdates != existingOpenGroup.infoUpdates + ) || + OpenGroup.Permissions(roomInfo: responseBody) != existingOpenGroup.permissions + ) - // If there are no 'changedResponses' and there hasn't been a failure then there is - // no need to do anything else - guard !changedResponses.isEmpty || failureCount != 0 else { - return Just(((info, response), rawMessageCount, 0, true)) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - - return dependencies[singleton: .storage] - .writePublisher { db -> PollResult in - // Reset the failure count - if failureCount > 0 { - try OpenGroup - .filter(OpenGroup.Columns.server == pollerDestination.target) - .updateAll(db, OpenGroup.Columns.pollFailureCount.set(to: 0)) - } + default: return true + } + } + + // If there are no 'changedResponses' and there hasn't been a failure then there is + // no need to do anything else + guard !changedResponses.isEmpty || failureCount != 0 else { + return PollResult((info, response), rawMessageCount, 0, true) + } + + return try await dependencies[singleton: .storage].writeAsync { [destination] db -> PollResult in + // Reset the failure count + if failureCount > 0 { + try OpenGroup + .filter(OpenGroup.Columns.server == destination.target) + .updateAll(db, OpenGroup.Columns.pollFailureCount.set(to: 0)) + } + + var interactionInfo: [MessageReceiver.InsertedInteractionInfo?] = [] + try changedResponses.forEach { endpoint, data in + switch endpoint { + case .capabilities: + guard + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, + let responseBody: OpenGroupAPI.Capabilities = responseData.body + else { return } - var interactionInfo: [MessageReceiver.InsertedInteractionInfo?] = [] - try changedResponses.forEach { endpoint, data in + OpenGroupManager.handleCapabilities( + db, + capabilities: responseBody, + on: destination.target + ) + + case .roomPollInfo(let roomToken, _): + guard + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, + let responseBody: OpenGroupAPI.RoomPollInfo = responseData.body + else { return } + + try OpenGroupManager.handlePollInfo( + db, + pollInfo: responseBody, + publicKey: nil, + for: roomToken, + on: destination.target, + using: dependencies + ) + + case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _): + guard + let responseData: Network.BatchSubResponse<[Failable]> = data as? Network.BatchSubResponse<[Failable]>, + let responseBody: [Failable] = responseData.body + else { return } + + interactionInfo.append( + contentsOf: OpenGroupManager.handleMessages( + db, + messages: responseBody.compactMap { $0.value }, + for: roomToken, + on: destination.target, + using: dependencies + ) + ) + + case .inbox, .inboxSince, .outbox, .outboxSince: + guard + let responseData: Network.BatchSubResponse<[OpenGroupAPI.DirectMessage]?> = data as? Network.BatchSubResponse<[OpenGroupAPI.DirectMessage]?>, + !responseData.failedToParseBody + else { return } + + // Double optional because the server can return a `304` with an empty body + let messages: [OpenGroupAPI.DirectMessage] = ((responseData.body ?? []) ?? []) + let fromOutbox: Bool = { switch endpoint { - case .capabilities: - guard - let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, - let responseBody: OpenGroupAPI.Capabilities = responseData.body - else { return } - - OpenGroupManager.handleCapabilities( - db, - capabilities: responseBody, - on: pollerDestination.target - ) - - case .roomPollInfo(let roomToken, _): - guard - let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, - let responseBody: OpenGroupAPI.RoomPollInfo = responseData.body - else { return } - - try OpenGroupManager.handlePollInfo( - db, - pollInfo: responseBody, - publicKey: nil, - for: roomToken, - on: pollerDestination.target, - using: dependencies - ) - - case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _): - guard - let responseData: Network.BatchSubResponse<[Failable]> = data as? Network.BatchSubResponse<[Failable]>, - let responseBody: [Failable] = responseData.body - else { return } - - interactionInfo.append( - contentsOf: OpenGroupManager.handleMessages( - db, - messages: responseBody.compactMap { $0.value }, - for: roomToken, - on: pollerDestination.target, - using: dependencies - ) - ) - - case .inbox, .inboxSince, .outbox, .outboxSince: - guard - let responseData: Network.BatchSubResponse<[OpenGroupAPI.DirectMessage]?> = data as? Network.BatchSubResponse<[OpenGroupAPI.DirectMessage]?>, - !responseData.failedToParseBody - else { return } - - // Double optional because the server can return a `304` with an empty body - let messages: [OpenGroupAPI.DirectMessage] = ((responseData.body ?? []) ?? []) - let fromOutbox: Bool = { - switch endpoint { - case .outbox, .outboxSince: return true - default: return false - } - }() - - interactionInfo.append( - contentsOf: OpenGroupManager.handleDirectMessages( - db, - messages: messages, - fromOutbox: fromOutbox, - on: pollerDestination.target, - using: dependencies - ) - ) - - default: break // No custom handling needed + case .outbox, .outboxSince: return true + default: return false } - } + }() - /// Notify about the received message - interactionInfo.forEach { info in - MessageReceiver.prepareNotificationsForInsertedInteractions( + interactionInfo.append( + contentsOf: OpenGroupManager.handleDirectMessages( db, - insertedInteractionInfo: info, - isMessageRequest: false, /// Communities can't be message requests + messages: messages, + fromOutbox: fromOutbox, + on: destination.target, using: dependencies ) - } + ) - /// Assume all messages were handled - return ((info, response), rawMessageCount, rawMessageCount, true) - } - .eraseToAnyPublisher() + default: break // No custom handling needed + } } - .eraseToAnyPublisher() + + /// Notify about the received message + interactionInfo.forEach { info in + MessageReceiver.prepareNotificationsForInsertedInteractions( + db, + insertedInteractionInfo: info, + isMessageRequest: false, /// Communities can't be message requests + using: dependencies + ) + } + + /// Assume all messages were handled + return PollResult((info, response), rawMessageCount, rawMessageCount, true) + } } } @@ -611,7 +593,7 @@ fileprivate extension Error { } } -// MARK: - GroupPoller Cache +// MARK: - CommunityPollerManager public extension CommunityPoller { struct Info: Equatable, FetchableRecord, Decodable, ColumnExpressible { @@ -624,111 +606,119 @@ public extension CommunityPoller { public let server: String public let pollFailureCount: Int64 } +} + +// MARK: - CommunityPollerManager - class Cache: CommunityPollerCacheType { - private let dependencies: Dependencies - private var _pollers: [String: CommunityPoller] = [:] // One for each server - - public var serversBeingPolled: Set { Set(_pollers.keys) } - public var allPollers: [CommunityPollerType] { Array(_pollers.values) } - - // MARK: - Initialization - - public init(using dependencies: Dependencies) { - self.dependencies = dependencies - } - - deinit { - _pollers.forEach { _, poller in poller.stop() } - _pollers.removeAll() - } - - // MARK: - Functions - - public func startAllPollers() { - // On the communityPollerQueue fetch all SOGS and start the pollers - Threading.communityPollerQueue.async(using: dependencies) { [weak self, dependencies] in - dependencies[singleton: .storage].readAsync( - retrieve: { db -> [Info] in - // The default room promise creates an OpenGroup with an empty `roomToken` value, - // we don't want to start a poller for this as the user hasn't actually joined a room - try OpenGroup - .select( - OpenGroup.Columns.server, - max(OpenGroup.Columns.pollFailureCount).forKey(Info.Columns.pollFailureCount) - ) - .filter(OpenGroup.Columns.isActive == true) - .filter(OpenGroup.Columns.roomToken != "") - .group(OpenGroup.Columns.server) - .asRequest(of: Info.self) - .fetchAll(db) - }, - completion: { [weak self] result in - switch result { - case .failure: break - case .success(let infos): - Threading.communityPollerQueue.async(using: dependencies) { [weak self] in - infos.forEach { info in - self?.getOrCreatePoller(for: info).startIfNeeded() - } - } - } - } - ) +actor CommunityPollerManager: CommunityPollerManagerType { + private let dependencies: Dependencies + private var pollers: [String: CommunityPoller] = [:] // One for each server + + nonisolated public let syncState: CommunityPollerManagerSyncState = CommunityPollerManagerSyncState() + public var serversBeingPolled: Set { Set(pollers.keys) } + public var allPollers: [any PollerType] { Array(pollers.values) } + + // MARK: - Initialization + + public init(using dependencies: Dependencies) { + self.dependencies = dependencies + } + + deinit { + Task { [pollers] in + for poller in pollers.values { + await poller.stop() } } - - @discardableResult public func getOrCreatePoller(for info: CommunityPoller.Info) -> CommunityPollerType { - guard let poller: CommunityPoller = _pollers[info.server.lowercased()] else { - let poller: CommunityPoller = CommunityPoller( - pollerName: "Community poller for: \(info.server)", // stringlint:ignore - pollerQueue: Threading.communityPollerQueue, - pollerDestination: .server(info.server), - failureCount: Int(info.pollFailureCount), - shouldStoreMessages: true, - logStartAndStopCalls: false, - using: dependencies - ) - _pollers[info.server.lowercased()] = poller - return poller + } + + // MARK: - Functions + + public func startAllPollers() async { + Task { + let communityInfo: [CommunityPoller.Info] = try await dependencies[singleton: .storage].readAsync { db in + // The default room promise creates an OpenGroup with an empty `roomToken` value, + // we don't want to start a poller for this as the user hasn't actually joined a room + try OpenGroup + .select( + OpenGroup.Columns.server, + max(OpenGroup.Columns.pollFailureCount).forKey(CommunityPoller.Info.Columns.pollFailureCount) + ) + .filter(OpenGroup.Columns.isActive == true) + .filter(OpenGroup.Columns.roomToken != "") + .group(OpenGroup.Columns.server) + .asRequest(of: CommunityPoller.Info.self) + .fetchAll(db) } + for info in communityInfo { + await getOrCreatePoller(for: info).startIfNeeded() + } + } + } + + @discardableResult public func getOrCreatePoller(for info: CommunityPoller.Info) async -> any PollerType { + guard let poller: CommunityPoller = pollers[info.server.lowercased()] else { + let poller: CommunityPoller = CommunityPoller( + pollerName: "Community poller for: \(info.server)", // stringlint:ignore + destination: .server(info.server), + failureCount: Int(info.pollFailureCount), + shouldStoreMessages: true, + logStartAndStopCalls: false, + using: dependencies + ) + pollers[info.server.lowercased()] = poller + syncState.update(serversBeingPolled: Set(pollers.keys)) return poller } + + return poller + } - public func stopAndRemovePoller(for server: String) { - _pollers[server.lowercased()]?.stop() - _pollers[server.lowercased()] = nil + public func stopAndRemovePoller(for server: String) async { + await pollers[server.lowercased()]?.stop() + pollers[server.lowercased()] = nil + syncState.update(serversBeingPolled: Set(pollers.keys)) + } + + public func stopAndRemoveAllPollers() async { + for poller in pollers.values { + await poller.stop() } - public func stopAndRemoveAllPollers() { - _pollers.forEach { _, poller in poller.stop() } - _pollers.removeAll() - } + pollers.removeAll() + syncState.update(serversBeingPolled: Set(pollers.keys)) } } -// MARK: - GroupPollerCacheType +/// We manually handle thread-safety using the `NSLock` so can ensure this is `Sendable` +public final class CommunityPollerManagerSyncState: @unchecked Sendable { + private let lock = NSLock() + private var _serversBeingPolled: Set = [] + + public var serversBeingPolled: Set { lock.withLock { _serversBeingPolled } } -/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way -public protocol CommunityPollerImmutableCacheType: ImmutableCacheType { - var serversBeingPolled: Set { get } - var allPollers: [CommunityPollerType] { get } + func update(serversBeingPolled: Set) { + lock.withLock { self._serversBeingPolled = serversBeingPolled } + } } -public protocol CommunityPollerCacheType: CommunityPollerImmutableCacheType, MutableCacheType { - var serversBeingPolled: Set { get } - var allPollers: [CommunityPollerType] { get } +// MARK: - CommunityPollerManagerType + +public protocol CommunityPollerManagerType { + nonisolated var syncState: CommunityPollerManagerSyncState { get } + var serversBeingPolled: Set { get async } + var allPollers: [any PollerType] { get async } - func startAllPollers() - @discardableResult func getOrCreatePoller(for info: CommunityPoller.Info) -> CommunityPollerType - func stopAndRemovePoller(for server: String) - func stopAndRemoveAllPollers() + func startAllPollers() async + @discardableResult func getOrCreatePoller(for info: CommunityPoller.Info) async -> any PollerType + func stopAndRemovePoller(for server: String) async + func stopAndRemoveAllPollers() async } -public extension CommunityPollerCacheType { - @discardableResult func getOrCreatePoller(for server: String) -> CommunityPollerType { - return getOrCreatePoller(for: CommunityPoller.Info(server: server, pollFailureCount: 0)) +public extension CommunityPollerManagerType { + @discardableResult func getOrCreatePoller(for server: String) async -> any PollerType { + return await getOrCreatePoller(for: CommunityPoller.Info(server: server, pollFailureCount: 0)) } } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift index 804ed95182..93149aa1aa 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift @@ -18,9 +18,8 @@ public extension Singleton { /// it isn't actually getting messages from other snodes. return CurrentUserPoller( pollerName: "Main Poller", // stringlint:ignore - pollerQueue: Threading.pollerQueue, - pollerDestination: .swarm(dependencies[cache: .general].sessionId.hexString), - pollerDrainBehaviour: .limitedReuse(count: 6), + destination: .swarm(dependencies[cache: .general].sessionId.hexString), + swarmDrainStrategy: .limitedReuse(count: 6), namespaces: CurrentUserPoller.namespaces, shouldStoreMessages: true, logStartAndStopCalls: true, @@ -32,7 +31,7 @@ public extension Singleton { // MARK: - CurrentUserPoller -public final class CurrentUserPoller: SwarmPoller { +public final actor CurrentUserPoller: SwarmPollerType { public static let namespaces: [SnodeAPI.Namespace] = [ .default, .configUserProfile, .configContacts, .configConvoInfoVolatile, .configUserGroups ] @@ -40,37 +39,83 @@ public final class CurrentUserPoller: SwarmPoller { private let retryInterval: TimeInterval = 0.25 private let maxRetryInterval: TimeInterval = 15 - // MARK: - Abstract Methods + public let dependencies: Dependencies + public let pollerName: String + public let destination: PollerDestination + public let swarmDrainer: SwarmDrainer + public let logStartAndStopCalls: Bool + nonisolated public var receivedPollResponse: AsyncStream { responseStream.stream } + public var pollTask: Task? + public var pollCount: Int = 0 + public var failureCount: Int + public var lastPollStart: TimeInterval = 0 + public var cancellable: AnyCancellable? - override public func nextPollDelay() -> AnyPublisher { - // If there have been no failures then just use the 'minPollInterval' - guard failureCount > 0 else { - return Just(pollInterval) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() + public let namespaces: [SnodeAPI.Namespace] + public let customAuthMethod: AuthenticationMethod? + public let shouldStoreMessages: Bool + nonisolated private let responseStream: CancellationAwareAsyncStream = CancellationAwareAsyncStream() + + // MARK: - Initialization + + public init( + pollerName: String, + destination: PollerDestination, + swarmDrainStrategy: SwarmDrainer.Strategy, + namespaces: [SnodeAPI.Namespace], + failureCount: Int, + shouldStoreMessages: Bool, + logStartAndStopCalls: Bool, + customAuthMethod: AuthenticationMethod?, + using dependencies: Dependencies + ) { + let (stream, continuation) = AsyncStream.makeStream() + self.dependencies = dependencies + self.pollerName = pollerName + self.destination = destination + self.swarmDrainer = SwarmDrainer( + strategy: swarmDrainStrategy, + nextRetrievalAfterDrain: .resetState, + logDetails: SwarmDrainer.LogDetails(cat: .poller, name: pollerName), + using: dependencies + ) + self.namespaces = namespaces + self.failureCount = failureCount + self.customAuthMethod = customAuthMethod + self.shouldStoreMessages = shouldStoreMessages + self.logStartAndStopCalls = logStartAndStopCalls + } + + deinit { + // Send completion events to the observables + Task { [stream = responseStream] in + await stream.finishCurrentStreams() } - // Otherwise use a simple back-off with the 'retryInterval' - let nextDelay: TimeInterval = TimeInterval(retryInterval * (Double(failureCount) * 1.2)) - - return Just(min(maxRetryInterval, nextDelay)) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() + pollTask?.cancel() } - // stringlint:ignore_contents - override public func handlePollError(_ error: Error, _ lastError: Error?) -> PollerErrorResponse { - if !dependencies[defaults: .appGroup, key: .isMainAppActive] { - // Do nothing when an error gets throws right after returning from the background (happens frequently) - } - else if case .limitedReuse(_, .some(let targetSnode), _, _, _) = pollerDrainBehaviour { - setDrainBehaviour(pollerDrainBehaviour.clearTargetSnode()) - return .continuePollingInfo("Switching from \(targetSnode) to next snode.") - } - else { - return .continuePollingInfo("Had no target snode.") - } + // MARK: - Polling + + public func pollerDidStart() {} + + public func pollerReceivedResponse(_ response: PollResponse) async { + await responseStream.send(response) + } + + public func pollerDidStop() { + Task { await responseStream.finishCurrentStreams() } + } + + // MARK: - PollerType + + public func nextPollDelay() async -> TimeInterval { + // If there have been no failures then just use the 'minPollInterval' + guard failureCount > 0 else { return pollInterval } + + // Otherwise use a simple back-off with the 'retryInterval' + let nextDelay: TimeInterval = TimeInterval(retryInterval * (Double(failureCount) * 1.2)) - return .continuePolling + return min(maxRetryInterval, nextDelay) } } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift index 1ec82b4124..25b1951728 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift @@ -8,18 +8,16 @@ import SessionUtilitiesKit // MARK: - Cache -public extension Cache { - static let groupPollers: CacheConfig = Dependencies.create( - identifier: "groupPollers", - createInstance: { dependencies in GroupPoller.Cache(using: dependencies) }, - mutableInstance: { $0 }, - immutableInstance: { $0 } +public extension Singleton { + static let groupPollerManager: SingletonConfig = Dependencies.create( + identifier: "groupPollerManager", + createInstance: { dependencies in GroupPollerManager(using: dependencies) } ) } // MARK: - GroupPoller -public final class GroupPoller: SwarmPoller { +public actor GroupPoller: SwarmPollerType { private let minPollInterval: Double = 3 private let maxPollInterval: Double = 30 @@ -37,9 +35,68 @@ public final class GroupPoller: SwarmPoller { ] } - public override func pollerDidStart() { + public let dependencies: Dependencies + public let pollerName: String + public let destination: PollerDestination + public let swarmDrainer: SwarmDrainer + public let logStartAndStopCalls: Bool + nonisolated public var receivedPollResponse: AsyncStream { responseStream.stream } + + public var pollTask: Task? + public var pollCount: Int = 0 + public var failureCount: Int + public var lastPollStart: TimeInterval = 0 + public var cancellable: AnyCancellable? + + public let namespaces: [SnodeAPI.Namespace] + public let customAuthMethod: AuthenticationMethod? + public let shouldStoreMessages: Bool + nonisolated private let responseStream: CancellationAwareAsyncStream = CancellationAwareAsyncStream() + + // MARK: - Initialization + + public init( + pollerName: String, + destination: PollerDestination, + swarmDrainStrategy: SwarmDrainer.Strategy, + namespaces: [SnodeAPI.Namespace], + failureCount: Int, + shouldStoreMessages: Bool, + logStartAndStopCalls: Bool, + customAuthMethod: AuthenticationMethod?, + using dependencies: Dependencies + ) { + let (stream, continuation) = AsyncStream.makeStream() + self.dependencies = dependencies + self.pollerName = pollerName + self.destination = destination + self.swarmDrainer = SwarmDrainer( + strategy: swarmDrainStrategy, + nextRetrievalAfterDrain: .resetState, + logDetails: SwarmDrainer.LogDetails(cat: .poller, name: pollerName), + using: dependencies + ) + self.namespaces = namespaces + self.failureCount = failureCount + self.customAuthMethod = customAuthMethod + self.shouldStoreMessages = shouldStoreMessages + self.logStartAndStopCalls = logStartAndStopCalls + } + + deinit { + // Send completion events to the observables + Task { [stream = responseStream] in + await stream.finishCurrentStreams() + } + + pollTask?.cancel() + } + + // MARK: - Polling + + public func pollerDidStart() { guard - let sessionId: SessionId = try? SessionId(from: pollerDestination.target), + let sessionId: SessionId = try? SessionId(from: destination.target), sessionId.prefix == .group else { return } @@ -50,43 +107,52 @@ public final class GroupPoller: SwarmPoller { /// If the keys generation is greated than `0` then it means we have a valid config so shouldn't continue guard numKeys == 0 else { return } - dependencies[singleton: .storage] - .readPublisher { [pollerDestination] db in + Task { [destination, dependencies] in + let isExpired: Bool? = try await dependencies[singleton: .storage].readAsync { [destination] db in try ClosedGroup - .filter(id: pollerDestination.target) + .filter(id: destination.target) .select(.expired) .asRequest(of: Bool.self) .fetchOne(db) } - .filter { ($0 != true) } - .flatMap { [receivedPollResponse] _ in receivedPollResponse } - .first() - .map { $0.filter { $0.isConfigMessage } } - .filter { !$0.contains(where: { $0.namespace == SnodeAPI.Namespace.configGroupKeys }) } - .sinkUntilComplete( - receiveValue: { [pollerDestination, pollerName, dependencies] configMessages in - Log.error(.poller, "\(pollerName) received no config messages in it's first poll, flagging as expired.") - - dependencies[singleton: .storage].writeAsync { db in - try ClosedGroup - .filter(id: pollerDestination.target) - .updateAllAndConfig( - db, - ClosedGroup.Columns.expired.set(to: true), - using: dependencies - ) - } - } - ) + + /// If we haven't set the `expired` value then we should check the first poll response to see if it's missing the + /// `GroupKeys` config message + guard + isExpired != true, + let response: PollResponse = await receivedPollResponse.first(where: { _ in true }), + !response.contains(where: { $0.namespace == .configGroupKeys }) + else { return } + + /// There isn't `GroupKeys` config so flag the group as `expired` + Log.error(.poller, "\(pollerName) received no config messages in it's first poll, flagging as expired.") + try await dependencies[singleton: .storage].writeAsync { db in + try ClosedGroup + .filter(id: destination.target) + .updateAllAndConfig( + db, + ClosedGroup.Columns.expired.set(to: true), + using: dependencies + ) + } + } + } + + public func pollerReceivedResponse(_ response: PollResponse) async { + await responseStream.send(response) + } + + public func pollerDidStop() { + Task { await responseStream.finishCurrentStreams() } } - // MARK: - Abstract Methods + // MARK: - PollerType - override public func nextPollDelay() -> AnyPublisher { + public func nextPollDelay() async -> TimeInterval { let lastReadDate: Date = dependencies .mutate(cache: .libSession) { cache in cache.conversationLastRead( - threadId: pollerDestination.target, + threadId: destination.target, threadVariant: .group, openGroupUrlInfo: nil ) @@ -98,131 +164,114 @@ public final class GroupPoller: SwarmPoller { } .defaulting(to: dependencies.dateNow.addingTimeInterval(-5 * 60)) - // Get the received date of the last message in the thread. If we don't have - // any messages yet, pick some reasonable fake time interval to use instead - return dependencies[singleton: .storage] - .readPublisher { [pollerDestination] db in - try Interaction - .filter(Interaction.Columns.threadId == pollerDestination.target) - .select(.receivedAtTimestampMs) - .order(Interaction.Columns.timestampMs.desc) - .asRequest(of: Int64.self) - .fetchOne(db) - } - .map { [dependencies] receivedAtTimestampMs -> Date in - guard - let receivedAtTimestampMs: Int64 = receivedAtTimestampMs, - receivedAtTimestampMs > 0 - else { return dependencies.dateNow.addingTimeInterval(-5 * 60) } - - return Date(timeIntervalSince1970: TimeInterval(Double(receivedAtTimestampMs) / 1000)) - } - .map { [maxPollInterval, minPollInterval, dependencies] lastMessageDate in - let timeSinceLastMessage: TimeInterval = dependencies.dateNow - .timeIntervalSince(max(lastMessageDate, lastReadDate)) - let limit: Double = (12 * 60 * 60) - let a: TimeInterval = ((maxPollInterval - minPollInterval) / limit) - let nextPollInterval: TimeInterval = a * min(timeSinceLastMessage, limit) + minPollInterval - - return nextPollInterval - } - .eraseToAnyPublisher() - } - - override public func handlePollError(_ error: Error, _ lastError: Error?) -> PollerErrorResponse { - return .continuePolling + /// Get the received date of the last message in the thread. If we don't have any messages yet, pick some reasonable fake time + /// interval to use instead + let receivedAtTimestampMs: Int64? = try? await dependencies[singleton: .storage].readAsync { [destination] db in + try Interaction + .filter(Interaction.Columns.threadId == destination.target) + .select(.receivedAtTimestampMs) + .order(Interaction.Columns.timestampMs.desc) + .asRequest(of: Int64.self) + .fetchOne(db) + } + let lastMessageDate: Date = { + guard + let receivedAtTimestampMs: Int64 = receivedAtTimestampMs, + receivedAtTimestampMs > 0 + else { return dependencies.dateNow.addingTimeInterval(-5 * 60) } + + return Date(timeIntervalSince1970: TimeInterval(Double(receivedAtTimestampMs) / 1000)) + }() + + let timeSinceLastMessage: TimeInterval = dependencies.dateNow + .timeIntervalSince(max(lastMessageDate, lastReadDate)) + let limit: Double = (12 * 60 * 60) + let a: TimeInterval = ((maxPollInterval - minPollInterval) / limit) + let nextPollInterval: TimeInterval = a * min(timeSinceLastMessage, limit) + minPollInterval + + return nextPollInterval } } -// MARK: - GroupPoller Cache +// MARK: - GroupPollerManager -public extension GroupPoller { - class Cache: GroupPollerCacheType { - private let dependencies: Dependencies - private var _pollers: [String: GroupPoller] = [:] // One for each swarm - - // MARK: - Initialization - - public init(using dependencies: Dependencies) { - self.dependencies = dependencies - } - - deinit { - _pollers.forEach { _, poller in poller.stop() } - _pollers.removeAll() - } - - // MARK: - Functions - - public func startAllPollers() { - // On the group poller queue fetch all closed groups which should poll and start the pollers - Threading.groupPollerQueue.async(using: dependencies) { [weak self, dependencies] in - dependencies[singleton: .storage].readAsync( - retrieve: { db -> Set in - try ClosedGroup - .select(.threadId) - .filter(ClosedGroup.Columns.shouldPoll == true) - .filter( - ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString - ) - .asRequest(of: String.self) - .fetchSet(db) - }, - completion: { [weak self] result in - switch result { - case .failure: break - case .success(let publicKeys): - Threading.groupPollerQueue.async(using: dependencies) { [weak self] in - publicKeys.forEach { swarmPublicKey in - self?.getOrCreatePoller(for: swarmPublicKey).startIfNeeded() - } - } - } - } - ) +public actor GroupPollerManager: GroupPollerManagerType { + private let dependencies: Dependencies + private var pollers: [String: GroupPoller] = [:] // One for each swarm + + // MARK: - Initialization + + public init(using dependencies: Dependencies) { + self.dependencies = dependencies + } + + deinit { + Task { [pollers] in + for poller in pollers.values { + await poller.stop() } } - - @discardableResult public func getOrCreatePoller(for swarmPublicKey: String) -> SwarmPollerType { - guard let poller: GroupPoller = _pollers[swarmPublicKey.lowercased()] else { - let poller: GroupPoller = GroupPoller( - pollerName: "Closed group poller with public key: \(swarmPublicKey)", // stringlint:ignore - pollerQueue: Threading.groupPollerQueue, - pollerDestination: .swarm(swarmPublicKey), - pollerDrainBehaviour: .alwaysRandom, - namespaces: GroupPoller.namespaces(swarmPublicKey: swarmPublicKey), - shouldStoreMessages: true, - logStartAndStopCalls: false, - using: dependencies - ) - _pollers[swarmPublicKey.lowercased()] = poller - return poller + } + + // MARK: - Functions + + public func startAllPollers() async { + Task { + let groupPublicKeys: Set = try await dependencies[singleton: .storage].readAsync { db in + try ClosedGroup + .select(.threadId) + .filter(ClosedGroup.Columns.shouldPoll == true) + .filter( + ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && + ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString + ) + .asRequest(of: String.self) + .fetchSet(db) } + for swarmPublicKey in groupPublicKeys { + await getOrCreatePoller(for: swarmPublicKey).startIfNeeded() + } + } + } + + @discardableResult public func getOrCreatePoller(for swarmPublicKey: String) async -> any SwarmPollerType { + guard let poller: GroupPoller = pollers[swarmPublicKey.lowercased()] else { + let poller: GroupPoller = GroupPoller( + pollerName: "Closed group poller with public key: \(swarmPublicKey)", // stringlint:ignore + destination: .swarm(swarmPublicKey), + swarmDrainStrategy: .alwaysRandom, + namespaces: GroupPoller.namespaces(swarmPublicKey: swarmPublicKey), + shouldStoreMessages: true, + logStartAndStopCalls: false, + using: dependencies + ) + pollers[swarmPublicKey.lowercased()] = poller return poller } - public func stopAndRemovePoller(for swarmPublicKey: String) { - _pollers[swarmPublicKey.lowercased()]?.stop() - _pollers[swarmPublicKey.lowercased()] = nil + return poller + } + + public func stopAndRemovePoller(for swarmPublicKey: String) async { + await pollers[swarmPublicKey.lowercased()]?.stop() + pollers[swarmPublicKey.lowercased()] = nil + } + + public func stopAndRemoveAllPollers() async { + for poller in pollers.values { + await poller.stop() } - public func stopAndRemoveAllPollers() { - _pollers.forEach { _, poller in poller.stop() } - _pollers.removeAll() - } + pollers.removeAll() } } -// MARK: - GroupPollerCacheType - -/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way -public protocol GroupPollerImmutableCacheType: ImmutableCacheType {} +// MARK: - GroupPollerManagerType -public protocol GroupPollerCacheType: GroupPollerImmutableCacheType, MutableCacheType { - func startAllPollers() - @discardableResult func getOrCreatePoller(for swarmPublicKey: String) -> SwarmPollerType - func stopAndRemovePoller(for swarmPublicKey: String) - func stopAndRemoveAllPollers() +public protocol GroupPollerManagerType { + func startAllPollers() async + @discardableResult func getOrCreatePoller(for swarmPublicKey: String) async -> any SwarmPollerType + func stopAndRemovePoller(for swarmPublicKey: String) async + func stopAndRemoveAllPollers() async } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift index 91ef8cf1b9..9a50228715 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift @@ -15,7 +15,7 @@ public extension Log.Category { // MARK: - PollerDestination -public enum PollerDestination { +public enum PollerDestination: Sendable { case swarm(String) case server(String) @@ -26,44 +26,47 @@ public enum PollerDestination { } } -// MARK: - PollerErrorResponse +// MARK: - PollResult -public enum PollerErrorResponse { - case stopPolling - case continuePolling - case continuePollingInfo(String) +public struct PollResult { + public let response: R + public let rawMessageCount: Int + public let validMessageCount: Int + public let hadValidHashUpdate: Bool + + public init( + _ response: R, + _ rawMessageCount: Int = 0, + _ validMessageCount: Int = 0, + _ hadValidHashUpdate: Bool = false + ) { + self.response = response + self.rawMessageCount = rawMessageCount + self.validMessageCount = validMessageCount + self.hadValidHashUpdate = hadValidHashUpdate + } } // MARK: - PollerType -public protocol PollerType: AnyObject { +public protocol PollerType: Actor { associatedtype PollResponse - typealias PollResult = ( - response: PollResponse, - rawMessageCount: Int, - validMessageCount: Int, - hadValidHashUpdate: Bool - ) - var dependencies: Dependencies { get } - var pollerQueue: DispatchQueue { get } var pollerName: String { get } - var pollerDestination: PollerDestination { get } + var destination: PollerDestination { get } var logStartAndStopCalls: Bool { get } - var receivedPollResponse: AnyPublisher { get } + nonisolated var receivedPollResponse: AsyncStream { get } - var isPolling: Bool { get set } var pollCount: Int { get set } var failureCount: Int { get set } var lastPollStart: TimeInterval { get set } - var cancellable: AnyCancellable? { get set } + var pollTask: Task? { get set } init( pollerName: String, - pollerQueue: DispatchQueue, - pollerDestination: PollerDestination, - pollerDrainBehaviour: ThreadSafeObject, + destination: PollerDestination, + swarmDrainStrategy: SwarmDrainer.Strategy, namespaces: [SnodeAPI.Namespace], failureCount: Int, shouldStoreMessages: Bool, @@ -72,136 +75,153 @@ public protocol PollerType: AnyObject { using dependencies: Dependencies ) - func startIfNeeded(forceStartInBackground: Bool) + func startIfNeeded(forceStartInBackground: Bool) async func stop() func pollerDidStart() - func poll(forceSynchronousProcessing: Bool) -> AnyPublisher - func nextPollDelay() -> AnyPublisher - func handlePollError(_ error: Error, _ lastError: Error?) -> PollerErrorResponse + func pollerReceivedResponse(_ response: PollResponse) async + func pollerDidStop() + func poll(forceSynchronousProcessing: Bool) async throws -> PollResult + func pollFromBackground() async throws -> PollResult + func nextPollDelay() async -> TimeInterval + func handlePollError(_ error: Error) async } // MARK: - Default Implementations public extension PollerType { - func startIfNeeded() { startIfNeeded(forceStartInBackground: false) } + func startIfNeeded() async { await startIfNeeded(forceStartInBackground: false) } - func startIfNeeded(forceStartInBackground: Bool) { - Task { @MainActor [weak self, pollerName, pollerQueue, appContext = dependencies[singleton: .appContext], dependencies] in - guard - forceStartInBackground || - appContext.isMainAppAndActive - else { return Log.info(.poller, "Ignoring call to start \(pollerName) due to not being active.") } - - pollerQueue.async(using: dependencies) { [weak self] in - guard self?.isPolling != true else { return } - - // Might be a race condition that the setUpPolling finishes too soon, - // and the timer is not created, if we mark the group as is polling - // after setUpPolling. So the poller may not work, thus misses messages - self?.isPolling = true - self?.pollRecursively(nil) - - if self?.logStartAndStopCalls == true { - Log.info(.poller, "Started \(pollerName).") - } - - self?.pollerDidStart() - } + func startIfNeeded(forceStartInBackground: Bool) async { + var canStartWhenInactive: Bool = forceStartInBackground + + if !canStartWhenInactive { + canStartWhenInactive = await dependencies[singleton: .appContext].isMainAppAndActive } + + guard canStartWhenInactive else { + return Log.info(.poller, "Ignoring call to start \(pollerName) due to not being active.") + } + + guard pollTask == nil else { return } + + await pollRecursively() + + if logStartAndStopCalls { + Log.info(.poller, "Started \(pollerName).") + } + + pollerDidStart() } func stop() { - pollerQueue.async(using: dependencies) { [weak self, pollerName] in - self?.isPolling = false - self?.cancellable?.cancel() - - if self?.logStartAndStopCalls == true { - Log.info(.poller, "Stopped \(pollerName).") - } + pollTask?.cancel() + + if logStartAndStopCalls { + Log.info(.poller, "Stopped \(pollerName).") } + + pollerDidStop() } - internal func pollRecursively(_ lastError: Error?) { - guard isPolling else { return } - guard - !dependencies[singleton: .storage].isSuspended && - !dependencies[cache: .libSessionNetwork].isSuspended - else { - let suspendedDependency: String = { - guard !dependencies[singleton: .storage].isSuspended else { - return "storage" + internal func pollRecursively() async { + typealias TimeInfo = ( + duration: TimeUnit, + nextPollDelay: TimeInterval, + nextPollInterval: TimeUnit + ) + + pollTask = Task { + /// Don't bother trying to poll if we don't have a network connection, just wait for one to be established + let networkStatus: NetworkStatus? = await dependencies[singleton: .network].networkStatus + .first(where: { _ in true }) + + if networkStatus != .connected { + Log.info(.poller, "\(pollerName) waiting for network to connect before starting to poll.") + _ = await dependencies[singleton: .network].networkStatus.first(where: { $0 == .connected }) + } + + /// Now that we have a connection just poll indefinitely + while true { + try Task.checkCancellation() + + guard + !dependencies[singleton: .storage].isSuspended, + await dependencies[singleton: .network].isSuspended == false + else { + let suspendedDependency: String = { + guard !dependencies[singleton: .storage].isSuspended else { + return "storage" + } + + return "network" + }() + Log.warn(.poller, "Stopped \(pollerName) due to \(suspendedDependency) being suspended.") + self.stop() + return } - return "network" - }() - Log.warn(.poller, "Stopped \(pollerName) due to \(suspendedDependency) being suspended.") - self.stop() - return - } - - self.lastPollStart = dependencies.dateNow.timeIntervalSince1970 - - cancellable = poll(forceSynchronousProcessing: false) - .subscribe(on: pollerQueue, using: dependencies) - .receive(on: pollerQueue, using: dependencies) - .asResult() - .flatMapOptional { [weak self] value in self?.nextPollDelay().map { (value, $0) } } - .sink( - receiveCompletion: { _ in }, // Never called - receiveValue: { [weak self, pollerName, pollerQueue, lastPollStart, failureCount, dependencies] result, nextPollDelay in - // If the polling has been cancelled then don't continue - guard self?.isPolling == true else { return } - + lastPollStart = dependencies.dateNow.timeIntervalSince1970 + let getTimeInfo: () async throws -> TimeInfo = { [lastPollStart, dependencies] in let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 let duration: TimeUnit = .seconds(endTime - lastPollStart) + let nextPollDelay: TimeInterval = await self.nextPollDelay() let nextPollInterval: TimeUnit = .seconds(nextPollDelay) - var errorFromPoll: Error? - // Log information about the poll - switch result { - case .failure(let error): - // Increment the failure count - self?.failureCount = (failureCount + 1) - errorFromPoll = error - - // Determine if the error should stop us from polling anymore - switch self?.handlePollError(error, lastError) { - case .stopPolling: return - case .continuePollingInfo(let info): - Log.error(.poller, "\(pollerName) failed to process any messages after \(duration, unit: .s) due to error: \(error). \(info). Setting failure count to \(failureCount). Next poll in \(nextPollInterval, unit: .s).") - - case .continuePolling, .none: - Log.error(.poller, "\(pollerName) failed to process any messages after \(duration, unit: .s) due to error: \(error). Setting failure count to \(failureCount). Next poll in \(nextPollInterval, unit: .s).") - } + return (duration, nextPollDelay, nextPollInterval) + } + var timeInfo: TimeInfo = try await getTimeInfo() + + do { + let result: PollResult = try await poll(forceSynchronousProcessing: false) + try Task.checkCancellation() + + /// Notify any observers that we got a result + await pollerReceivedResponse(result.response) + + /// Reset the failure count + failureCount = 0 + timeInfo = try await getTimeInfo() + + /// Log the poll result + switch (result.rawMessageCount, result.validMessageCount, result.hadValidHashUpdate) { + case (0, _, _): + Log.info(.poller, "Received no new messages in \(pollerName) after \(timeInfo.duration, unit: .s). Next poll in \(timeInfo.nextPollInterval, unit: .s).") - case .success(let response): - // Reset the failure count - self?.failureCount = 0 + case (_, 0, false): + Log.info(.poller, "Received \(result.rawMessageCount) new message(s) in \(pollerName) after \(timeInfo.duration, unit: .s), all duplicates - marked the hash we polled with as invalid. Next poll in \(timeInfo.nextPollInterval, unit: .s).") - switch (response.rawMessageCount, response.validMessageCount, response.hadValidHashUpdate) { - case (0, _, _): - Log.info(.poller, "Received no new messages in \(pollerName) after \(duration, unit: .s). Next poll in \(nextPollInterval, unit: .s).") - - case (_, 0, false): - Log.info(.poller, "Received \(response.rawMessageCount) new message(s) in \(pollerName) after \(duration, unit: .s), all duplicates - marked the hash we polled with as invalid. Next poll in \(nextPollInterval, unit: .s).") - - default: - Log.info(.poller, "Received \(response.validMessageCount) new message(s) in \(pollerName) after \(duration, unit: .s) (duplicates: \(response.rawMessageCount - response.validMessageCount)). Next poll in \(nextPollInterval, unit: .s).") - } + default: + Log.info(.poller, "Received \(result.validMessageCount) new message(s) in \(pollerName) after \(timeInfo.duration, unit: .s) (duplicates: \(result.rawMessageCount - result.validMessageCount)). Next poll in \(timeInfo.nextPollInterval, unit: .s).") } + } + catch is CancellationError { + /// If we were cancelled then we don't want to continue + break + } + catch { + try Task.checkCancellation() - // Schedule the next poll - pollerQueue.asyncAfter(deadline: .now() + .milliseconds(Int(nextPollInterval.timeInterval * 1000)), qos: .default, using: dependencies) { - self?.pollRecursively(errorFromPoll) - } + /// Increment the failure count and log the error + failureCount = (failureCount + 1) + timeInfo = try await getTimeInfo() + Log.error(.poller, "\(pollerName) failed to process any messages after \(timeInfo.duration, unit: .s) due to error: \(error). Setting failure count to \(failureCount). Next poll in \(timeInfo.nextPollInterval, unit: .s).") + + /// Perform any custom error handling + await handlePollError(error) } - ) + + /// Sleep until the next poll + try await Task.sleep(for: .milliseconds(Int(timeInfo.nextPollDelay * 1000))) + } + } } /// This doesn't do anything functional _but_ does mean if we get a crash from the `BackgroundPoller` we can better distinguish /// it from a crash from a foreground poll - func pollFromBackground() -> AnyPublisher { - return poll(forceSynchronousProcessing: true) + func pollFromBackground() async throws -> PollResult { + return try await poll(forceSynchronousProcessing: true) } + + func handlePollError(_ error: Error) async {} } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift index 2f3ca1906a..6411dd1903 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift @@ -8,221 +8,172 @@ import SessionUtilitiesKit // MARK: - SwarmPollerType -public protocol SwarmPollerType { - typealias PollResponse = [ProcessedMessage] +public protocol SwarmPollerType: PollerType where PollResponse == SwarmPoller.PollResponse { + var swarmDrainer: SwarmDrainer { get } + var namespaces: [SnodeAPI.Namespace] { get } + var customAuthMethod: AuthenticationMethod? { get } + var shouldStoreMessages: Bool { get } - var receivedPollResponse: AnyPublisher { get } - - func startIfNeeded() - func stop() + init( + pollerName: String, + destination: PollerDestination, + swarmDrainStrategy: SwarmDrainer.Strategy, + namespaces: [SnodeAPI.Namespace], + failureCount: Int, + shouldStoreMessages: Bool, + logStartAndStopCalls: Bool, + customAuthMethod: AuthenticationMethod?, + using dependencies: Dependencies + ) } -// MARK: - SwarmPoller - -public class SwarmPoller: SwarmPollerType & PollerType { - public enum PollSource: Equatable { - case snode(LibSession.Snode) - case pushNotification - } - - public let dependencies: Dependencies - public let pollerQueue: DispatchQueue - public let pollerName: String - public let pollerDestination: PollerDestination - @ThreadSafeObject public var pollerDrainBehaviour: SwarmDrainBehaviour - public let logStartAndStopCalls: Bool - public var receivedPollResponse: AnyPublisher { - receivedPollResponseSubject.eraseToAnyPublisher() - } - - public var isPolling: Bool = false - public var pollCount: Int = 0 - public var failureCount: Int - public var lastPollStart: TimeInterval = 0 - public var cancellable: AnyCancellable? - - private let namespaces: [SnodeAPI.Namespace] - private let customAuthMethod: AuthenticationMethod? - private let shouldStoreMessages: Bool - private let receivedPollResponseSubject: PassthroughSubject = PassthroughSubject() +// MARK: - SwarmPollerType Convenience - // MARK: - Initialization - - required public init( +extension SwarmPollerType { + public init( pollerName: String, - pollerQueue: DispatchQueue, - pollerDestination: PollerDestination, - pollerDrainBehaviour: ThreadSafeObject, + destination: PollerDestination, + swarmDrainStrategy: SwarmDrainer.Strategy, namespaces: [SnodeAPI.Namespace], failureCount: Int = 0, shouldStoreMessages: Bool, logStartAndStopCalls: Bool, - customAuthMethod: AuthenticationMethod? = nil, using dependencies: Dependencies ) { - self.dependencies = dependencies - self.pollerName = pollerName - self.pollerQueue = pollerQueue - self.pollerDestination = pollerDestination - self._pollerDrainBehaviour = pollerDrainBehaviour - self.namespaces = namespaces - self.failureCount = failureCount - self.customAuthMethod = customAuthMethod - self.shouldStoreMessages = shouldStoreMessages - self.logStartAndStopCalls = logStartAndStopCalls + self.init( + pollerName: pollerName, + destination: destination, + swarmDrainStrategy: swarmDrainStrategy, + namespaces: namespaces, + failureCount: failureCount, + shouldStoreMessages: shouldStoreMessages, + logStartAndStopCalls: logStartAndStopCalls, + customAuthMethod: nil, + using: dependencies + ) } - // MARK: - Abstract Methods - - /// Calculate the delay which should occur before the next poll - public func nextPollDelay() -> AnyPublisher { - preconditionFailure("abstract class - override in subclass") - } - - /// Perform and logic which should occur when the poll errors, will stop polling if `false` is returned - public func handlePollError(_ error: Error, _ lastError: Error?) -> PollerErrorResponse { - preconditionFailure("abstract class - override in subclass") - } - - // MARK: - Internal Functions - - internal func setDrainBehaviour(_ behaviour: SwarmDrainBehaviour) { - _pollerDrainBehaviour.set(to: behaviour) - } - - // MARK: - Polling - - public func pollerDidStart() {} - /// Polls based on it's configuration and processes any messages, returning an array of messages that were /// successfully processed /// /// **Note:** The returned messages will have already been processed by the `Poller`, they are only returned /// for cases where we need explicit/custom behaviours to occur (eg. Onboarding) - public func poll(forceSynchronousProcessing: Bool) -> AnyPublisher { - let pollerQueue: DispatchQueue = self.pollerQueue + public func poll(forceSynchronousProcessing: Bool) async throws -> PollResult { + /// Select the node to poll + let swarm: Set = try await dependencies[singleton: .network] + .getSwarm(for: destination.target) + await swarmDrainer.updateSwarmIfNeeded(swarm) + let snode: LibSession.Snode = try await swarmDrainer.selectNextNode() + + /// Fetch the messages (refreshing the current config hashes) + let authMethod: AuthenticationMethod = try (customAuthMethod ?? Authentication.with( + swarmPublicKey: destination.target, + using: dependencies + )) let activeHashes: [String] = dependencies.mutate(cache: .libSession) { cache in - cache.activeHashes(for: pollerDestination.target) + cache.activeHashes(for: destination.target) } + let lastHashes: [SnodeAPI.Namespace: String] = try await dependencies[singleton: .storage].readAsync { [namespaces, dependencies] db in + try namespaces.reduce(into: [:]) { result, namespace in + result[namespace] = try SnodeReceivedMessageInfo.fetchLastNotExpired( + db, + for: snode, + namespace: namespace, + swarmPublicKey: try authMethod.swarmPublicKey, + using: dependencies + )?.hash + } + } + let request: Network.PreparedRequest = try SnodeAPI.preparedPoll( + namespaces: namespaces, + lastHashes: lastHashes, + refreshingConfigHashes: activeHashes, + from: snode, + authMethod: authMethod, + using: dependencies + ) + let response: SnodeAPI.PollResponse = try await request.send(using: dependencies) - /// Fetch the messages - return dependencies[singleton: .network] - .getSwarm(for: pollerDestination.target) - .tryFlatMapWithRandomSnode(drainBehaviour: _pollerDrainBehaviour, using: dependencies) { [pollerDestination, customAuthMethod, namespaces, dependencies] snode -> AnyPublisher<(LibSession.Snode, Network.PreparedRequest), Error> in - dependencies[singleton: .storage].readPublisher { db -> (LibSession.Snode, Network.PreparedRequest) in - let authMethod: AuthenticationMethod = try (customAuthMethod ?? Authentication.with( - db, - swarmPublicKey: pollerDestination.target, - using: dependencies - )) + /// Get all of the messages and sort them by their required `processingOrder` + typealias MessageData = (namespace: SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?) + let sortedMessages: [MessageData] = response + .compactMap { namespace, result -> MessageData? in + (result.data?.messages).map { (namespace, $0, result.data?.lastHash) } + } + .sorted { lhs, rhs in lhs.namespace.processingOrder < rhs.namespace.processingOrder } + let rawMessageCount: Int = sortedMessages.map { $0.messages.count }.reduce(0, +) + + /// No need to do anything if there are no messages + guard rawMessageCount > 0 else { + return PollResult([]) + } + + /// Process the response + let processedResponse: (configMessageJobs: [Job], standardMessageJobs: [Job], pollResult: PollResult) = try await dependencies[singleton: .storage].writeAsync { [destination, shouldStoreMessages, dependencies] db in + SwarmPoller.processPollResponse( + db, + cat: .poller, + source: .snode(snode), + swarmPublicKey: destination.target, + shouldStoreMessages: shouldStoreMessages, + ignoreDedupeFiles: false, + forceSynchronousProcessing: forceSynchronousProcessing, + sortedMessages: sortedMessages, + using: dependencies + ) + } + + /// If we don't want to forcible process the response synchronously then just finish immediately + guard forceSynchronousProcessing else { return processedResponse.pollResult } + + /// We want to try to handle the receive jobs immediately in the background + await withThrowingTaskGroup { [dependencies] group in + processedResponse.configMessageJobs.forEach { job in + group.addTask { [dependencies] in + /// **Note:** In the background we just want jobs to fail silently + let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) - return ( - snode, - try SnodeAPI.preparedPoll( - db, - namespaces: namespaces, - refreshingConfigHashes: activeHashes, - from: snode, - authMethod: authMethod, - using: dependencies - ) + // FIXME: Rework this once jobs are async/await + ConfigMessageReceiveJob.run( + job, + scheduler: Threading.pollerQueue, + success: { _, _ in semaphore.signal() }, + failure: { _, _, _ in semaphore.signal() }, + deferred: { _ in semaphore.signal() }, + using: dependencies ) } } - .flatMap { [dependencies] snode, request in - request.send(using: dependencies) - .map { _, response in (snode, response) } - } - .flatMapStorageWritePublisher(using: dependencies, updates: { [pollerDestination, shouldStoreMessages, forceSynchronousProcessing, dependencies] db, info -> ([Job], [Job], PollResult) in - let (snode, namespacedResults): (LibSession.Snode, SnodeAPI.PollResponse) = info - - /// Get all of the messages and sort them by their required `processingOrder` - typealias MessageData = (namespace: SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?) - let sortedMessages: [MessageData] = namespacedResults - .compactMap { namespace, result -> MessageData? in - (result.data?.messages).map { (namespace, $0, result.data?.lastHash) } - } - .sorted { lhs, rhs in lhs.namespace.processingOrder < rhs.namespace.processingOrder } - let rawMessageCount: Int = sortedMessages.map { $0.messages.count }.reduce(0, +) - - /// No need to do anything if there are no messages - guard rawMessageCount > 0 else { - return ([], [], ([], 0, 0, false)) - } - - return SwarmPoller.processPollResponse( - db, - cat: .poller, - source: .snode(snode), - swarmPublicKey: pollerDestination.target, - shouldStoreMessages: shouldStoreMessages, - ignoreDedupeFiles: false, - forceSynchronousProcessing: forceSynchronousProcessing, - sortedMessages: sortedMessages, - using: dependencies - ) - }) - .flatMap { [dependencies] (configMessageJobs, standardMessageJobs, pollResult) -> AnyPublisher in - // If we don't want to forcible process the response synchronously then just finish immediately - guard forceSynchronousProcessing else { - return Just(pollResult) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - - // We want to try to handle the receive jobs immediately in the background - return Publishers - .MergeMany( - configMessageJobs.map { job -> AnyPublisher in - Deferred { - Future { resolver in - // Note: In the background we just want jobs to fail silently - ConfigMessageReceiveJob.run( - job, - scheduler: pollerQueue, - success: { _, _ in resolver(Result.success(())) }, - failure: { _, _, _ in resolver(Result.success(())) }, - deferred: { _ in resolver(Result.success(())) }, - using: dependencies - ) - } - } - .eraseToAnyPublisher() - } + } + await withThrowingTaskGroup { [dependencies] group in + processedResponse.standardMessageJobs.forEach { job in + group.addTask { [dependencies] in + /// **Note:** In the background we just want jobs to fail silently + let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) + + // FIXME: Rework this once jobs are async/await + MessageReceiveJob.run( + job, + scheduler: Threading.pollerQueue, + success: { _, _ in semaphore.signal() }, + failure: { _, _, _ in semaphore.signal() }, + deferred: { _ in semaphore.signal() }, + using: dependencies ) - .collect() - .flatMap { _ in - Publishers - .MergeMany( - standardMessageJobs.map { job -> AnyPublisher in - Deferred { - Future { resolver in - // Note: In the background we just want jobs to fail silently - MessageReceiveJob.run( - job, - scheduler: pollerQueue, - success: { _, _ in resolver(Result.success(())) }, - failure: { _, _, _ in resolver(Result.success(())) }, - deferred: { _ in resolver(Result.success(())) }, - using: dependencies - ) - } - } - .eraseToAnyPublisher() - } - ) - .collect() - } - .map { _ in pollResult } - .eraseToAnyPublisher() - } - .handleEvents( - receiveOutput: { [weak self] (pollResult: PollResult) in - /// Notify any observers that we got a result - self?.receivedPollResponseSubject.send(pollResult.response) } - ) - .eraseToAnyPublisher() + } + } + + return processedResponse.pollResult + } +} + +public enum SwarmPoller { + public typealias PollResponse = [ProcessedMessage] + + public enum PollSource: Equatable { + case snode(LibSession.Snode) + case pushNotification } @discardableResult public static func processPollResponse( @@ -235,12 +186,12 @@ public class SwarmPoller: SwarmPollerType & PollerType { forceSynchronousProcessing: Bool, sortedMessages: [(namespace: SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?)], using dependencies: Dependencies - ) -> ([Job], [Job], PollResult) { + ) -> ([Job], [Job], PollResult) { /// No need to do anything if there are no messages let rawMessageCount: Int = sortedMessages.map { $0.messages.count }.reduce(0, +) guard rawMessageCount > 0 else { - return ([], [], ([], 0, 0, false)) + return ([], [], PollResult([])) } /// Otherwise process the messages and add them to the queue for handling @@ -275,7 +226,7 @@ public class SwarmPoller: SwarmPollerType & PollerType { }() guard lastHashes.isEmpty || Set(lastHashes) == lastHashesAfterFetch else { - return ([], [], ([], 0, 0, false)) + return ([], [], PollResult([])) } /// Since the hashes are still accurate we can now process the messages @@ -397,7 +348,7 @@ public class SwarmPoller: SwarmPollerType & PollerType { guard shouldStoreMessages && !forceSynchronousProcessing else { messageCount += allProcessedMessages.count finalProcessedMessages += allProcessedMessages - return ([], [], (finalProcessedMessages, rawMessageCount, messageCount, hadValidHashUpdate)) + return ([], [], PollResult(finalProcessedMessages, rawMessageCount, messageCount, hadValidHashUpdate)) } /// Add a job to process the config messages first @@ -490,6 +441,6 @@ public class SwarmPoller: SwarmPollerType & PollerType { catch { Log.error(cat, "Failed to handle potential invalid/deleted hashes due to error: \(error).") } } - return (configMessageJobs, standardMessageJobs, (finalProcessedMessages, rawMessageCount, messageCount, hadValidHashUpdate)) + return (configMessageJobs, standardMessageJobs, PollResult(finalProcessedMessages, rawMessageCount, messageCount, hadValidHashUpdate)) } } diff --git a/SessionMessagingKit/Utilities/AppSetup.swift b/SessionMessagingKit/Utilities/AppSetup.swift new file mode 100644 index 0000000000..4535c28cd4 --- /dev/null +++ b/SessionMessagingKit/Utilities/AppSetup.swift @@ -0,0 +1,109 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUIKit +import SessionMessagingKit +import SessionUtilitiesKit + +public enum AppSetup { + public static func performSetup( + migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, + using dependencies: Dependencies + ) async { + var backgroundTask: SessionBackgroundTask? = SessionBackgroundTask(label: #function, using: dependencies) + + /// Order matters here. + /// + /// All of these "singletons" should have any dependencies used in their + /// initializers injected. + dependencies[singleton: .backgroundTaskManager].startObservingNotifications() + + /// Attachments can be stored to NSTemporaryDirectory() + /// If you receive a media message while the device is locked, the download will fail if + /// the temporary directory is NSFileProtectionComplete + try? dependencies[singleton: .fileManager].protectFileOrFolder( + at: NSTemporaryDirectory(), + fileProtectionType: .completeUntilFirstUserAuthentication + ) + + SessionEnvironment.shared = SessionEnvironment( + audioSession: OWSAudioSession(), + proximityMonitoringManager: OWSProximityMonitoringManagerImpl(using: dependencies), + windowManager: OWSWindowManager(default: ()) + ) + + try await dependencies[singleton: .storage].perform( + migrations: SNMessagingKit.migrations, + onProgressUpdate: { [weak self] progress, minEstimatedTotalTime in + self?.loadingViewController?.updateProgress( + progress: progress, + minEstimatedTotalTime: minEstimatedTotalTime + ) + } + ) + } + + public static func setup( + backgroundTask: SessionBackgroundTask? = nil, + migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, + using dependencies: Dependencies + ) async { + + } + + public static func postMigrationSetup(using dependencies: Dependencies) async throws { + /// Now that the migrations are complete there are a few more states which need to be setup + typealias UserInfo = ( + sessionId: SessionId, + ed25519SecretKey: [UInt8], + dumpSessionIds: Set, + unreadCount: Int? + ) + let userInfo: UserInfo? = try await dependencies[singleton: .storage].readAsync { db -> UserInfo? in + guard let ed25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else { + return nil + } + + /// Cache the users session id so we don't need to fetch it from the database every time + dependencies.mutate(cache: .general) { + $0.setSecretKey(ed25519SecretKey: ed25519KeyPair.secretKey) + } + + /// Load the `libSession` state into memory + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let cache: LibSession.Cache = LibSession.Cache( + userSessionId: userSessionId, + using: dependencies + ) + cache.loadState(db) + dependencies.set(cache: .libSession, to: cache) + + return ( + userSessionId, + ed25519KeyPair.secretKey, + cache.allDumpSessionIds, + try? Interaction.fetchAppBadgeUnreadCount(db, using: dependencies) + ) + } + + /// Save the `UserMetadata` and replicate `ConfigDump` data if needed + if let userInfo: UserInfo = userInfo { + try? dependencies[singleton: .extensionHelper].saveUserMetadata( + sessionId: userInfo.sessionId, + ed25519SecretKey: userInfo.ed25519SecretKey, + unreadCount: userInfo.unreadCount + ) + + Task.detached(priority: .medium) { + dependencies[singleton: .extensionHelper].replicateAllConfigDumpsIfNeeded( + userSessionId: userInfo.sessionId, + allDumpSessionIds: userInfo.dumpSessionIds + ) + } + } + + /// Ensure any recurring jobs are properly scheduled + dependencies[singleton: .jobRunner].scheduleRecurringJobsIfNeeded() + } +} diff --git a/SessionNetworkingKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift index 0e98f62fae..6bd97c9f73 100644 --- a/SessionNetworkingKit/LibSession/LibSession+Networking.swift +++ b/SessionNetworkingKit/LibSession/LibSession+Networking.swift @@ -7,105 +7,327 @@ import Combine import SessionUtil import SessionUtilitiesKit -// MARK: - Cache - -public extension Cache { - static let libSessionNetwork: CacheConfig = Dependencies.create( - identifier: "libSessionNetwork", - createInstance: { dependencies in - /// The `libSessionNetwork` cache gets warmed during startup and creates a network instance, populates the snode - /// cache and builds onion requests when created - when running unit tests we don't want to do any of that unless explicitly - /// desired within the test itself so instead we default to a `NoopNetworkCache` when running unit tests - guard !SNUtilitiesKit.isRunningTests else { return LibSession.NoopNetworkCache() } - - return LibSession.NetworkCache(using: dependencies) - }, - mutableInstance: { $0 }, - immutableInstance: { $0 } - ) -} - // MARK: - Log.Category public extension Log.Category { static let network: Log.Category = .create("Network", defaultLevel: .info) } -// MARK: - LibSession.Network +// MARK: - LibSessionNetwork -class LibSessionNetwork: NetworkType { +actor LibSessionNetwork: NetworkType { + fileprivate typealias Response = ( + success: Bool, + timeout: Bool, + statusCode: Int, + headers: [String: String], + data: Data? + ) + + private static var snodeCachePath: String { "\(SessionFileManager.nonInjectedAppSharedDataDirectoryPath)/snodeCache" } + private let dependencies: Dependencies + private let dependenciesPtr: UnsafeMutableRawPointer + private var network: UnsafeMutablePointer? = nil + nonisolated private let internalNetworkStatus: CurrentValueAsyncStream = CurrentValueAsyncStream(.unknown) + + public private(set) var isSuspended: Bool = false + nonisolated public var networkStatus: AsyncStream { internalNetworkStatus.stream } + nonisolated public let syncState: NetworkSyncState = NetworkSyncState() + + @available(*, deprecated, message: "We want to shift from Combine to Async/Await when possible") + private let networkInstance: CurrentValueSubject?, Error> = CurrentValueSubject(nil) + + @available(*, deprecated, message: "This probably isn't needed but in order to isolate the async from sync states I've added it") + nonisolated private let syncDependencies: Dependencies // MARK: - Initialization init(using dependencies: Dependencies) { self.dependencies = dependencies + self.dependenciesPtr = Unmanaged.passRetained(dependencies).toOpaque() + self.syncDependencies = dependencies + + /// Create the network object + Task { [self] in + /// If the app has been set to `forceOffline` then we need to explicitly set the network status to disconnected (because + /// it'll never be set otherwise) + if dependencies[feature: .forceOffline] { + await setNetworkStatus(status: .disconnected) + } + + /// Create the `network` instance so it can do any setup required + _ = try? await getOrCreateNetwork() + } + } + + deinit { + // Send completion events to the observables (so they can resubscribe to a future instance) + Task { [status = internalNetworkStatus] in + await status.send(.disconnected) + await status.finishCurrentStreams() + } + + // Finish the `networkInstance` (since it's a `CurrentValueSubject` we want to ensure it doesn't + // hold the `network` instance since we are about to free it below + self.networkInstance.send(nil) + self.networkInstance.send(completion: .finished) + + // Clear the network changed callbacks (just in case, since we are going to free the + // dependenciesPtr) and then free the network object + switch network { + case .none: break + case .some(let network): + session_network_set_status_changed_callback(network, nil, nil) + session_network_free(network) + } + + // Finally we need to make sure to clean up the unbalanced retain to the dependencies + Unmanaged.fromOpaque(dependenciesPtr).release() } // MARK: - NetworkType - func getSwarm(for swarmPublicKey: String) -> AnyPublisher, Error> { - typealias Output = Result, Error> + func getActivePaths() async throws -> [LibSession.Path] { + let network = try await getOrCreateNetwork() + + var cPathsPtr: UnsafeMutablePointer? + var cPathsLen: Int = 0 + session_network_get_active_paths(network, &cPathsPtr, &cPathsLen) + defer { + if let paths = cPathsPtr { + session_network_paths_free(paths) + } + } + + guard + cPathsLen > 0, + let cPaths: UnsafeMutablePointer = cPathsPtr + else { return [] } - return dependencies - .mutate(cache: .libSessionNetwork) { $0.getOrCreateNetwork_v2() } - .tryMapCallbackContext(type: Output.self) { ctx, network in - let sessionId: SessionId = try SessionId(from: swarmPublicKey) + return (0.. 0, let cNodes: UnsafePointer = cPaths[index].nodes { + nodes = (0.. = cPaths[index].onion_metadata { + category = Network.RequestCategory(onionMeta.get(\.category)) + } + else if let lokinetMeta: UnsafePointer = cPaths[index].lokinet_metadata { + destinationPubkey = lokinetMeta.get(\.destination_pubkey) + destinationAddress = lokinetMeta.get(\.destination_snode_address) + } + + return LibSession.Path( + nodes: nodes, + category: category, + destinationPubkey: destinationPubkey, + destinationSnodeAddress: destinationAddress + ) + } + } + + func getSwarm(for swarmPublicKey: String) async throws -> Set { + typealias Continuation = CheckedContinuation, Error> + + let network = try await getOrCreateNetwork() + let sessionId: SessionId = try SessionId(from: swarmPublicKey) + + guard let cSwarmPublicKey: [CChar] = sessionId.publicKeyString.cString(using: .utf8) else { + throw LibSessionError.invalidCConversion + } + + return try await withCheckedThrowingContinuation { continuation in + let context = LibSessionNetwork.ContinuationBox(continuation).unsafePointer() + + session_network_get_swarm(network, cSwarmPublicKey, { swarmPtr, swarmSize, ctx in + guard let box = LibSessionNetwork.ContinuationBox.from(unsafePointer: ctx) else { + return + } + + guard + swarmSize > 0, + let cSwarm: UnsafeMutablePointer = swarmPtr + else { return box.continuation.resume(throwing: SnodeAPIError.unableToRetrieveSwarm) } - guard let cSwarmPublicKey: [CChar] = sessionId.publicKeyString.cString(using: .utf8) else { - throw LibSessionError.invalidCConversion + var nodes: Set = [] + (0.. Set { + typealias Continuation = CheckedContinuation, Error> + + let network = try await getOrCreateNetwork() + + let nodes: Set = try await withCheckedThrowingContinuation { continuation in + let context = LibSessionNetwork.ContinuationBox(continuation).unsafePointer() + + session_network_get_random_nodes(network, UInt16(count), { nodesPtr, nodesSize, ctx in + guard let box = LibSessionNetwork.ContinuationBox.from(unsafePointer: ctx) else { + return } - session_network_get_swarm(network, cSwarmPublicKey, { swarmPtr, swarmSize, ctx in - guard - swarmSize > 0, - let cSwarm: UnsafeMutablePointer = swarmPtr - else { return CallbackWrapper.run(ctx, .failure(SnodeAPIError.unableToRetrieveSwarm)) } - - var nodes: Set = [] - (0...run(ctx, .success(nodes)) - }, ctx); - } - .tryMap { [dependencies = self.dependencies] result in - dependencies - .mutate(cache: .libSessionNetwork) { - $0.setSnodeNumber( - publicKey: swarmPublicKey, - value: (try? result.get())?.count ?? 0 - ) - } - return try result.get() - } - .eraseToAnyPublisher() + guard + nodesSize > 0, + let cSwarm: UnsafeMutablePointer = nodesPtr + else { return box.continuation.resume(throwing: SnodeAPIError.unableToRetrieveSwarm) } + + var nodes: Set = [] + (0..= count else { + throw SnodeAPIError.unableToRetrieveSwarm + } + + return nodes } - func getRandomNodes(count: Int) -> AnyPublisher, Error> { - typealias Output = Result, Error> + nonisolated func send( + endpoint: (any EndpointType), + destination: Network.Destination, + body: Data?, + category: Network.RequestCategory, + requestTimeout: TimeInterval, + overallTimeout: TimeInterval? + ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { + typealias FinalRequestInfo = ( + network: UnsafeMutablePointer, + body: Data?, + destination: Network.Destination + ) + typealias Output = (success: Bool, timeout: Bool, statusCode: Int, headers: [String: String], data: Data?) - return dependencies - .mutate(cache: .libSessionNetwork) { $0.getOrCreateNetwork() } - .tryMapCallbackContext(type: Output.self) { ctx, network in - network_get_random_nodes(network, UInt16(count), { nodesPtr, nodesSize, ctx in - guard - nodesSize > 0, - let cSwarm: UnsafeMutablePointer = nodesPtr - else { return CallbackWrapper.run(ctx, .failure(SnodeAPIError.unableToRetrieveSwarm)) } - - var nodes: Set = [] - (0...run(ctx, .success(nodes)) - }, ctx); + guard !syncState.isSuspended else { + Log.warn(.network, "Attempted to access suspended network.") + return Fail(error: NetworkError.suspended) + .eraseToAnyPublisher() + } + + guard !syncDependencies[feature: .forceOffline] else { + return Fail(error: NetworkError.serviceUnavailable) + .delay(for: .seconds(1), scheduler: DispatchQueue.global(qos: .userInitiated)) + .eraseToAnyPublisher() + } + + return networkInstance + .compactMap { $0 } + .tryFlatMap { [dependencies] network -> AnyPublisher in + switch destination { + case .snode, .server, .serverUpload, .serverDownload, .cached: + return Just((network, body, destination)) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + + case .randomSnode(let swarmPublicKey): + guard (try? SessionId(from: swarmPublicKey)) != nil else { + throw SessionIdError.invalidSessionId + } + guard body != nil else { + throw NetworkError.invalidPreparedRequest + } + guard let cSwarmPublicKey: [CChar] = swarmPublicKey.cString(using: .utf8) else { + throw LibSessionError.invalidCConversion + } + + return FutureBox> + .create { ctx in + session_network_get_swarm(network, cSwarmPublicKey, { swarmPtr, swarmSize, ctx in + guard + swarmSize > 0, + let cSwarm: UnsafeMutablePointer = swarmPtr + else { + return LibSessionNetwork.FutureBox>.fail( + error: SnodeAPIError.unableToRetrieveSwarm, + ptr: ctx + ) + } + + var nodes: Set = [] + (0..>.resolve( + result: nodes, + ptr: ctx + ) + }, ctx) + } + .tryMap { [dependencies] nodes in + try dependencies.randomElement(nodes) ?? { + throw SnodeAPIError.ranOutOfRandomSnodes(nil) + }() + } + .map { node in + ( + network, + body, + Network.Destination.snode(node, swarmPublicKey: swarmPublicKey) + ) + } + .eraseToAnyPublisher() + } } - .tryMap { result in - switch result { - case .failure(let error): throw error - case .success(let nodes): - guard nodes.count >= count else { throw SnodeAPIError.unableToRetrieveSwarm } + .tryMapCallbackContext(type: Output.self) { ctx, finalRequestInfo in + /// If it's a cached request then just return the cached result immediately + if case .cached(let success, let timeout, let statusCode, let headers, let data) = destination { + return CallbackWrapper.run(ctx, (success, timeout, statusCode, headers, data)) + } + + /// Define the callback to avoid dupolication + typealias ResponseCallback = session_network_response_t + let cCallback: ResponseCallback = { success, timeout, statusCode, cHeaders, cHeadersLen, dataPtr, dataLen, ctx in + let headers: [String: String] = LibSessionNetwork.headers(cHeaders, cHeadersLen) + let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } + CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) + } + let request: LibSessionNetwork.Request = LibSessionNetwork.Request( + endpoint: endpoint, + body: finalRequestInfo.body, + category: category, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout + ) + + switch finalRequestInfo.destination { + case .snode(let snode, _): + try LibSessionNetwork.withSnodeRequestParams(request, snode) { paramsPtr in + session_network_send_request(finalRequestInfo.network, paramsPtr, cCallback, ctx) + } + + case .server(let info), .serverUpload(let info, _), .serverDownload(let info): + let uploadFileName: String? = { + switch destination { + case .serverUpload(_, let fileName): return fileName + default: return nil + } + }() + + try LibSessionNetwork.withServerRequestParams(request, info, uploadFileName) { paramsPtr in + session_network_send_request(finalRequestInfo.network, paramsPtr, cCallback, ctx) + } - return nodes + /// Some destinations are for convenience and redirect to "proper" destination types so if one of them gets here + /// then it is invalid + default: throw NetworkError.invalidPreparedRequest } } + .tryMap { [dependencies] success, timeout, statusCode, headers, maybeData -> (any ResponseInfoType, Data) in + let response: Response = (success, timeout, statusCode, headers, maybeData) + try LibSessionNetwork.throwErrorIfNeeded(response, using: dependencies) + + guard let data: Data = maybeData else { throw NetworkError.parsingFailed } + + return (Network.ResponseInfo(code: statusCode, headers: headers), data) + } .eraseToAnyPublisher() } @@ -116,16 +338,10 @@ class LibSessionNetwork: NetworkType { category: Network.RequestCategory, requestTimeout: TimeInterval, overallTimeout: TimeInterval? - ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { -// func send( -// _ body: Data?, -// to destination: Network.Destination, -// requestTimeout: TimeInterval, -// requestAndPathBuildTimeout: TimeInterval? -// ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { + ) async throws -> (info: ResponseInfoType, value: Data?) { switch destination { case .snode, .server, .serverUpload, .serverDownload, .cached: - return sendRequest( + return try await sendRequest( endpoint: endpoint, destination: destination, body: body, @@ -134,104 +350,170 @@ class LibSessionNetwork: NetworkType { overallTimeout: overallTimeout ) - case .randomSnode(let swarmPublicKey, let retryCount): + case .randomSnode(let swarmPublicKey): guard (try? SessionId(from: swarmPublicKey)) != nil else { - return Fail(error: SessionIdError.invalidSessionId).eraseToAnyPublisher() + throw SessionIdError.invalidSessionId } - guard body != nil else { return Fail(error: NetworkError.invalidPreparedRequest).eraseToAnyPublisher() } + guard body != nil else { throw NetworkError.invalidPreparedRequest } - return getSwarm(for: swarmPublicKey) - .tryFlatMapWithRandomSnode(retry: retryCount, using: dependencies) { [weak self] snode in - try self.validOrThrow().sendRequest( - endpoint: endpoint, - destination: .snode(snode, swarmPublicKey: swarmPublicKey), - body: body, - category: category, - requestTimeout: requestTimeout, - overallTimeout: overallTimeout - ) - } + let swarm: Set = try await getSwarm(for: swarmPublicKey) + let swarmDrainer: SwarmDrainer = SwarmDrainer(swarm: swarm, using: dependencies) + let snode: LibSession.Snode = try await swarmDrainer.selectNextNode() - case .randomSnodeLatestNetworkTimeTarget(let swarmPublicKey, let retryCount, let bodyWithUpdatedTimestampMs): - guard (try? SessionId(from: swarmPublicKey)) != nil else { - return Fail(error: SessionIdError.invalidSessionId).eraseToAnyPublisher() - } - guard body != nil else { return Fail(error: NetworkError.invalidPreparedRequest).eraseToAnyPublisher() } - - return getSwarm(for: swarmPublicKey) - .tryFlatMapWithRandomSnode(retry: retryCount, using: dependencies) { [weak self, dependencies] snode in - try SnodeAPI - .preparedGetNetworkTime(from: snode, using: dependencies) - .send(using: dependencies) - .tryFlatMap { _, timestampMs in - guard - let updatedEncodable: Encodable = bodyWithUpdatedTimestampMs(timestampMs, dependencies), - let updatedBody: Data = try? JSONEncoder(using: dependencies).encode(updatedEncodable) - else { throw NetworkError.invalidPreparedRequest } - - return try self.validOrThrow().sendRequest( - endpoint: endpoint, - destination: .snode(snode, swarmPublicKey: swarmPublicKey), - body: updatedBody, - category: category, - requestTimeout: requestTimeout, - overallTimeout: overallTimeout - ) - .map { info, response -> (ResponseInfoType, Data?) in - ( - SnodeAPI.LatestTimestampResponseInfo( - code: info.code, - headers: info.headers, - timestampMs: timestampMs - ), - response - ) - } - } - } + return try await self.sendRequest( + endpoint: endpoint, + destination: .snode(snode, swarmPublicKey: swarmPublicKey), + body: body, + category: category, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout + ) } } - func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error> { - typealias Output = (success: Bool, timeout: Bool, statusCode: Int, headers: [String: String], data: Data?) + func checkClientVersion(ed25519SecretKey: [UInt8]) async throws -> (info: ResponseInfoType, value: AppVersionResponse) { + typealias Continuation = CheckedContinuation + + let network = try await getOrCreateNetwork() + var cEd25519SecretKey: [UInt8] = Array(ed25519SecretKey) + + guard ed25519SecretKey.count == 64 else { throw LibSessionError.invalidCConversion } + let paramsPtr: UnsafeMutablePointer = try session_file_server_get_client_version( + CLIENT_PLATFORM_IOS, + &cEd25519SecretKey, + Int64(floor(Network.defaultTimeout * 1000)), + 0 + ) ?? { throw NetworkError.invalidPreparedRequest }() + defer { session_request_params_free(paramsPtr) } + + let result: Response = try await withCheckedThrowingContinuation { continuation in + let box = LibSessionNetwork.ContinuationBox(continuation) + session_network_send_request(network, paramsPtr, box.cCallback, box.unsafePointer()) + } + + try LibSessionNetwork.throwErrorIfNeeded(result, using: dependencies) + let data: Data = try result.data ?? { throw NetworkError.parsingFailed }() + + return ( + Network.ResponseInfo(code: result.statusCode), + try AppVersionResponse.decoded(from: data, using: dependencies) + ) + } + + public func setNetworkStatus(status: NetworkStatus) async { + guard status == .disconnected || !isSuspended else { + Log.warn(.network, "Attempted to update network status to '\(status)' for suspended network, closing connections again.") + + switch network { + case .none: return + case .some(let network): return session_network_close_connections(network) + } + } + + // Notify any subscribers + Log.info(.network, "Network status changed to: \(status)") + await internalNetworkStatus.send(status) + } + + public func suspendNetworkAccess() async { + Log.info(.network, "Network access suspended.") + isSuspended = true + syncState.update(isSuspended: true) + await setNetworkStatus(status: .disconnected) + + switch network { + case .none: break + case .some(let network): session_network_suspend(network) + } + } + + public func resumeNetworkAccess() async { + isSuspended = false + syncState.update(isSuspended: false) + Log.info(.network, "Network access resumed.") + + switch network { + case .none: break + case .some(let network): session_network_resume(network) + } + } + + public func finishCurrentObservations() async { + await internalNetworkStatus.finishCurrentStreams() + } + + public func clearCache() async { + switch network { + case .none: break + case .some(let network): session_network_clear_cache(network) + } + } + + // MARK: - Internal Functions + + private func getOrCreateNetwork() async throws -> UnsafeMutablePointer { + guard !isSuspended else { + Log.warn(.network, "Attempted to access suspended network.") + throw NetworkError.suspended + } - return dependencies - .mutate(cache: .libSessionNetwork) { $0.getOrCreateNetwork() } - .tryMapCallbackContext(type: Output.self) { ctx, network in - guard ed25519SecretKey.count == 64 else { throw LibSessionError.invalidCConversion } + switch (network, dependencies[feature: .forceOffline]) { + case (_, true): + try await Task.sleep(for: .seconds(1)) + throw NetworkError.serviceUnavailable - var cEd25519SecretKey: [UInt8] = Array(ed25519SecretKey) + case (.some(let existingNetwork), _): return existingNetwork + + case (.none, _): + guard let cCachePath: [CChar] = LibSessionNetwork.snodeCachePath.cString(using: .utf8) else { + Log.error(.network, "Unable to create network object: \(LibSessionError.invalidCConversion)") + throw NetworkError.invalidState + } - network_get_client_version( - network, - CLIENT_PLATFORM_IOS, - &cEd25519SecretKey, - Int64(floor(Network.defaultTimeout * 1000)), - 0, - { success, timeout, statusCode, cHeaders, cHeaderVals, headerLen, dataPtr, dataLen, ctx in - let headers: [String: String] = CallbackWrapper - .headers(cHeaders, cHeaderVals, headerLen) - let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } - CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) - }, - ctx - ) - } - .tryMap { [dependencies] success, timeout, statusCode, headers, maybeData -> (any ResponseInfoType, AppVersionResponse) in - try LibSessionNetwork.throwErrorIfNeeded(success, timeout, statusCode, headers, maybeData, using: dependencies) + var error: [CChar] = [CChar](repeating: 0, count: 256) + var network: UnsafeMutablePointer? + var config: session_network_config = session_network_config_default() + config.cache_refresh_using_legacy_endpoint = true - guard let data: Data = maybeData else { throw NetworkError.parsingFailed } + if dependencies[feature: .serviceNetwork] == .testnet { + config.netid = SESSION_NETWORK_TESTNET + config.enforce_subnet_diversity = false /// On testnet we can't do this as nodes share IPs + } - return ( - Network.ResponseInfo(code: statusCode), - try AppVersionResponse.decoded(from: data, using: dependencies) - ) - } - .eraseToAnyPublisher() + /// If it's not the main app then we want to run in "Single Path Mode" (no use creating extra paths in the extensions) + if !dependencies[singleton: .appContext].isMainApp { + config.onionreq_single_path_mode = true + } + + try cCachePath.withUnsafeBufferPointer { cachePtr in + config.cache_dir = cachePtr.baseAddress + + guard session_network_init(&network, &config, &error) else { + Log.error(.network, "Unable to create network object: \(String(cString: error))") + throw NetworkError.invalidState + } + } + + /// Store the newly created network + self.network = network + self.networkInstance.send(network) + + session_network_set_status_changed_callback(network, { cStatus, ctx in + guard let ctx: UnsafeMutableRawPointer = ctx else { return } + + let status: NetworkStatus = NetworkStatus(status: cStatus) + let dependencies: Dependencies = Unmanaged.fromOpaque(ctx).takeUnretainedValue() + + // Kick off a task so we don't hold up the libSession thread that triggered the update + Task { [network = dependencies[singleton: .network]] in + await network.setNetworkStatus(status: status) + } + }, dependenciesPtr) + + return try network ?? { throw NetworkError.invalidState }() + } } - // MARK: - Internal Functions - private func sendRequest( endpoint: (any EndpointType), destination: Network.Destination, @@ -239,134 +521,76 @@ class LibSessionNetwork: NetworkType { category: Network.RequestCategory, requestTimeout: TimeInterval, overallTimeout: TimeInterval? - ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { - typealias Output = (success: Bool, timeout: Bool, statusCode: Int, headers: [String: String], data: Data?) + ) async throws -> (info: ResponseInfoType, value: Data?) { + typealias Continuation = CheckedContinuation - return dependencies - .mutate(cache: .libSessionNetwork) { $0.getOrCreateNetwork_v2() } - .tryMapCallbackContext(type: Output.self) { ctx, network in - /// If it's a cached request then just return the cached result immediately - if case .cached(let success, let timeout, let statusCode, let headers, let data) = destination { - return CallbackWrapper.run(ctx, (success, timeout, statusCode, headers, data)) - } - - /// Define the callback to avoid dupolication - typealias ResponseCallback = session_network_response_t - let cCallback: ResponseCallback = { success, timeout, statusCode, cHeaders, cHeadersLen, dataPtr, dataLen, ctx in - let headers: [String: String] = CallbackWrapper.headers(cHeaders, cHeadersLen) - let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } - CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) - } - let request: Request = Request( - endpoint: endpoint, - body: body, - category: category, - requestTimeout: requestTimeout, - overallTimeout: overallTimeout - ) - + let network = try await getOrCreateNetwork() + let result: Response = try await withCheckedThrowingContinuation { continuation in + let box = LibSessionNetwork.ContinuationBox(continuation) + + /// If it's a cached request then just return the cached result immediately + if case .cached(let success, let timeout, let statusCode, let headers, let data) = destination { + return box.continuation.resume(returning: (success, timeout, Int(statusCode), headers, data)) + } + + /// Define the callback to avoid dupolication + let context = box.unsafePointer() + let request: Request = Request( + endpoint: endpoint, + body: body, + category: category, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout + ) + + do { switch destination { case .snode(let snode, _): try LibSessionNetwork.withSnodeRequestParams(request, snode) { paramsPtr in - session_network_send_request(network, paramsPtr, cCallback, ctx) + session_network_send_request(network, paramsPtr, box.cCallback, context) } case .server(let info), .serverUpload(let info, _), .serverDownload(let info): - try LibSessionNetwork.withServerRequestParams(request, info) { paramsPtr in - session_network_send_request(network, paramsPtr, cCallback, ctx) + let uploadFileName: String? = { + switch destination { + case .serverUpload(_, let fileName): return fileName + default: return nil + } + }() + + try LibSessionNetwork.withServerRequestParams(request, info, uploadFileName) { paramsPtr in + session_network_send_request(network, paramsPtr, box.cCallback, context) } /// Some destinations are for convenience and redirect to "proper" destination types so if one of them gets here /// then it is invalid default: throw NetworkError.invalidPreparedRequest } - -// /// Prepare the values -// var cSnode: network_service_node? = { -// switch destination { -// case .snode(let snode, _): return snode.cSnode -// default: return nil -// } -// }() -// var serverInfo: Network.Destination.ServerInfo? = { -// switch destination { -// case .server(let info), .serverUpload(let info, _), .serverDownload(let info): -// return info -// -// default: return nil -// } -// }() -// let cBodyBytes: [UInt8] = try { -// switch body { -// case .none: return [] -// case let data as Data: return Array(data) -// case let bytes as [UInt8]: return bytes -// default: -// guard let encodedBody: Data = try? JSONEncoder().encode(body) else { -// throw SnodeAPIError.invalidPayload -// } -// -// return Array(encodedBody) -// } -// }() -// -// /// Construct and send the params -// return try endpoint.path.withCString { cEndpoint in -// try serverInfo.withUnsafePointer { cServerDest in -// // TODO: `fileName?.cString(using: .utf8),` for server upload -// var params = session_request_params() -//// var params = session_request_params( -//// snode_dest: &cSnode, -//// server_dest: cServerDest, -//// endpoint: cEndpoint, -//// body: &cBodyBytes, -//// body_size: cBodyBytes.count, -//// category: category.libSessionValue, -//// request_timeout_ms: Int64(floor(requestTimeout * 1000)), -//// overall_timeout_ms: Int64(floor((overallTimeout ?? 0) * 1000)), -//// request_id: nil -//// ) -// -// session_network_send_request( -// network, -// ¶ms, -// { success, timeout, statusCode, cHeaders, dataPtr, dataLen, ctx in -// let headers: [String: String] = CallbackWrapper.headers(cHeaders) -// let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } -// CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) -// }, -// ctx -// ) -// } -// } - } - .tryMap { [dependencies] success, timeout, statusCode, headers, data -> (any ResponseInfoType, Data?) in - try LibSessionNetwork.throwErrorIfNeeded(success, timeout, statusCode, headers, data, using: dependencies) - return (Network.ResponseInfo(code: statusCode, headers: headers), data) } - .eraseToAnyPublisher() + catch { box.continuation.resume(throwing: error) } + } + + try LibSessionNetwork.throwErrorIfNeeded(result, using: dependencies) + return (Network.ResponseInfo(code: result.statusCode, headers: result.headers), result.data) } - private static func throwErrorIfNeeded( - _ success: Bool, - _ timeout: Bool, - _ statusCode: Int, - _ headers: [String: String], - _ data: Data?, - using dependencies: Dependencies - ) throws { - guard !success || statusCode < 200 || statusCode > 299 else { return } - guard !timeout else { - switch data.map({ String(data: $0, encoding: .ascii) }) { - case .none: throw NetworkError.timeout(error: "\(NetworkError.unknown)", rawData: data) - case .some(let responseString): throw NetworkError.timeout(error: responseString, rawData: data) + private static func throwErrorIfNeeded(_ response: Response, using dependencies: Dependencies) throws { + guard !response.success || response.statusCode < 200 || response.statusCode > 299 else { return } + guard !response.timeout else { + switch response.data.map({ String(data: $0, encoding: .ascii) }) { + case .none: throw NetworkError.timeout(error: "\(NetworkError.unknown)", rawData: response.data) + case .some(let responseString): + throw NetworkError.timeout(error: responseString, rawData: response.data) } } /// Handle status codes with specific meanings - switch (statusCode, data.map { String(data: $0, encoding: .ascii) }) { - case (400, .none): throw NetworkError.badRequest(error: "\(NetworkError.unknown)", rawData: data) - case (400, .some(let responseString)): throw NetworkError.badRequest(error: responseString, rawData: data) + switch (response.statusCode, response.data.map { String(data: $0, encoding: .ascii) }) { + case (400, .none): + throw NetworkError.badRequest(error: "\(NetworkError.unknown)", rawData: response.data) + + case (400, .some(let responseString)): + throw NetworkError.badRequest(error: responseString, rawData: response.data) case (401, _): Log.warn(.network, "Unauthorised (Failed to verify the signature).") @@ -390,19 +614,12 @@ class LibSessionNetwork: NetworkType { throw NetworkError.badGateway } - let nodeHex: String = String(responseString.suffix(64)) - - for path in dependencies[cache: .libSessionNetwork].currentPaths { - if let index: Int = path.firstIndex(where: { $0.ed25519PubkeyHex == nodeHex }) { - throw SnodeAPIError.nodeNotFound(index, nodeHex) - } - } - - throw SnodeAPIError.nodeNotFound(nil, nodeHex) + throw SnodeAPIError.nodeNotFound(String(responseString.suffix(64))) case (504, _): throw NetworkError.gatewayTimeout case (_, .none): throw NetworkError.unknown - case (_, .some(let responseString)): throw NetworkError.requestFailed(error: responseString, rawData: data) + case (_, .some(let responseString)): + throw NetworkError.requestFailed(error: responseString, rawData: response.data) } } } @@ -439,6 +656,76 @@ private extension LibSessionNetwork { } } +private extension LibSessionNetwork { + class ContinuationBox { + let continuation: T + + init(_ continuation: T) { + self.continuation = continuation + } + + // MARK: - Functions + + public func unsafePointer() -> UnsafeMutableRawPointer { Unmanaged.passRetained(self).toOpaque() } + public static func from(unsafePointer: UnsafeMutableRawPointer?) -> ContinuationBox? { + guard let ptr: UnsafeMutableRawPointer = unsafePointer else { return nil } + + return Unmanaged>.fromOpaque(ptr).takeRetainedValue() + } + } + + class FutureBox { + var promise: ((Result) -> Void)? + + static func create(_ closure: @escaping (UnsafeMutableRawPointer) -> Void) -> AnyPublisher { + let box: FutureBox = FutureBox() + + return Future { [box] promise in + box.promise = promise + + let ptr = Unmanaged.passRetained(box).toOpaque() + closure(ptr) + }.eraseToAnyPublisher() + } + + init() {} + + // MARK: - Functions + + public static func resolve(result: T, ptr: UnsafeMutableRawPointer?) { + guard let ptr: UnsafeMutableRawPointer = ptr else { return } + + Unmanaged> + .fromOpaque(ptr) + .takeRetainedValue() + .promise?(.success(result)) + } + + public static func fail(error: Error, ptr: UnsafeMutableRawPointer?) { + guard let ptr: UnsafeMutableRawPointer = ptr else { return } + + Unmanaged> + .fromOpaque(ptr) + .takeRetainedValue() + .promise?(.failure(error)) + } + } +} + +extension LibSessionNetwork.ContinuationBox where T == CheckedContinuation { + var cCallback: session_network_response_t { + return { success, timeout, statusCode, cHeaders, cHeadersLen, dataPtr, dataLen, ctx in + guard let box = LibSessionNetwork.ContinuationBox.from(unsafePointer: ctx) else { + return + } + + let headers: [String: String] = LibSessionNetwork.headers(cHeaders, cHeadersLen) + let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } + box.continuation.resume(returning: (success, timeout, Int(statusCode), headers, data)) + } + } +} + // MARK: - Publisher Convenience fileprivate extension Publisher { @@ -467,17 +754,6 @@ fileprivate extension Publisher { } } -// MARK: - Optional Convenience - -private extension Optional where Wrapped == LibSessionNetwork { - func validOrThrow() throws -> Wrapped { - switch self { - case .none: throw NetworkError.invalidState - case .some(let value): return value - } - } -} - // MARK: - NetworkStatus Convenience private extension NetworkStatus { @@ -493,6 +769,15 @@ private extension NetworkStatus { // MARK: - Snode +extension LibSession { + public struct Path { + public let nodes: [LibSession.Snode] + public let category: Network.RequestCategory? + public let destinationPubkey: String? + public let destinationSnodeAddress: String? + } +} + extension LibSession { public struct Snode: Codable, Hashable, CustomStringConvertible { public let ed25519PubkeyHex: String @@ -619,12 +904,14 @@ private extension LibSessionNetwork { let params: session_request_params = session_request_params( snode_dest: cSnodePtr, server_dest: nil, + remote_addr_dest: nil, endpoint: cEndpoint, body: cBodyPtr, body_size: bodySize, category: request.category.libSessionValue, request_timeout_ms: UInt64(Int64(floor(request.requestTimeout * 1000))), overall_timeout_ms: UInt64(floor((request.overallTimeout ?? 0) * 1000)), + upload_file_name: nil, request_id: nil ) @@ -639,31 +926,45 @@ private extension LibSessionNetwork { static func withServerRequestParams( _ request: Request, _ info: Network.Destination.ServerInfo, + _ uploadFileName: String?, _ callback: (UnsafePointer) -> Result ) throws -> Result { - return try withBodyPointer(request.body) { cBodyPtr, bodySize in - try info.withServerInfoPointer { cServerDestinationPtr in - request.endpoint.path.withCString { cEndpoint in - let params: session_request_params = session_request_params( - snode_dest: nil, - server_dest: cServerDestinationPtr, - endpoint: cEndpoint, - body: cBodyPtr, - body_size: bodySize, - category: request.category.libSessionValue, - request_timeout_ms: UInt64(floor(request.requestTimeout * 1000)), - overall_timeout_ms: UInt64(floor((request.overallTimeout ?? 0) * 1000)), - request_id: nil - ) - - return withUnsafePointer(to: params) { paramsPtr in - callback(paramsPtr) + try info.pathAndParamsString.withCString { cEndpoint in + try withFileNamePtr(uploadFileName) { cUploadFileNamePtr in + try info.withServerInfoPointer { cServerDestinationPtr in + let params: session_request_params = session_request_params( + snode_dest: nil, + server_dest: cServerDestinationPtr, + remote_addr_dest: nil, + endpoint: cEndpoint, + body: cBodyPtr, + body_size: bodySize, + category: request.category.libSessionValue, + request_timeout_ms: UInt64(floor(request.requestTimeout * 1000)), + overall_timeout_ms: UInt64(floor((request.overallTimeout ?? 0) * 1000)), + upload_file_name: cUploadFileNamePtr, + request_id: nil + ) + + return withUnsafePointer(to: params) { paramsPtr in + callback(paramsPtr) + } } } } } } + + private static func withFileNamePtr( + _ uploadFilename: String?, + _ closure: (UnsafePointer?) throws -> Result + ) throws -> Result { + switch uploadFilename { + case .none: return try closure(nil) + case .some(let filename): return try filename.withCString { try closure($0) } + } + } private static func withBodyPointer( _ body: T?, @@ -705,8 +1006,6 @@ private extension Network.Destination.ServerInfo { } let targetScheme: String = (url.scheme ?? "https") - let endpoint: String = url.path - .appending(url.query.map { value in "?\(value)" } ?? "") let port: UInt16 = UInt16(url.port ?? (targetScheme == "https" ? 443 : 80)) let headersArray: [String] = headers.flatMap { [$0.key, $0.value] } @@ -714,23 +1013,20 @@ private extension Network.Destination.ServerInfo { return try method.rawValue.withCString { cMethodPtr in try targetScheme.withCString { cTargetSchemePtr in try host.withCString { cHostPtr in - try endpoint.withCString { cEndpointPtr in - try x25519PublicKey.withCString { cX25519PubkeyPtr in - try headersArray.withUnsafeCStrArray { headersArrayPtr in - let cServerDest = network_v2_server_destination( - method: cMethodPtr, - protocol: cTargetSchemePtr, - host: cHostPtr, - endpoint: cEndpointPtr, // TODO: Ditch this - port: port, - x25519_pubkey_hex: cX25519PubkeyPtr, - headers_kv_pairs: headersArrayPtr.baseAddress, - headers_kv_pairs_len: headersArray.count - ) - - return withUnsafePointer(to: cServerDest) { ptr in - body(ptr) - } + try x25519PublicKey.withCString { cX25519PubkeyPtr in + try headersArray.withUnsafeCStrArray { headersArrayPtr in + let cServerDest = network_v2_server_destination( + method: cMethodPtr, + protocol: cTargetSchemePtr, + host: cHostPtr, + port: port, + x25519_pubkey_hex: cX25519PubkeyPtr, + headers_kv_pairs: headersArrayPtr.baseAddress, + headers_kv_pairs_len: headersArray.count + ) + + return withUnsafePointer(to: cServerDest) { ptr in + body(ptr) } } } @@ -740,19 +1036,7 @@ private extension Network.Destination.ServerInfo { } } -private extension LibSessionNetwork.CallbackWrapper { - static func headers( - _ cHeaders: UnsafePointer?>?, - _ cHeaderVals: UnsafePointer?>?, - _ count: Int - ) -> [String: String] { - let headers: [String] = ([String](cStringArray: cHeaders, count: count) ?? []) - let headerVals: [String] = ([String](cStringArray: cHeaderVals, count: count) ?? []) - - return zip(headers, headerVals) - .reduce(into: [:]) { result, next in result[next.0] = next.1 } - } - +private extension LibSessionNetwork { static func headers(_ cHeaders: UnsafePointer?>?, _ count: Int) -> [String: String] { let headersArray: [String] = ([String](cStringArray: cHeaders, count: count) ?? []) @@ -765,445 +1049,62 @@ private extension LibSessionNetwork.CallbackWrapper { } } -// MARK: - LibSession.NetworkCache - public extension LibSession { - class NetworkCache: NetworkCacheType { - private static var snodeCachePath: String { "\(SessionFileManager.nonInjectedAppSharedDataDirectoryPath)/snodeCache" } - - private let dependencies: Dependencies - private let dependenciesPtr: UnsafeMutableRawPointer - private var network: UnsafeMutablePointer? = nil - private var network_v2: UnsafeMutablePointer? = nil - private let _paths: CurrentValueSubject<[[Snode]], Never> = CurrentValueSubject([]) - private let _networkStatus: CurrentValueSubject = CurrentValueSubject(.unknown) - private let _snodeNumber: CurrentValueSubject<[String: Int], Never> = .init([:]) + actor NoopNetwork: NetworkType { + public let isSuspended: Bool = false + nonisolated public let networkStatus: AsyncStream = .makeStream().stream + nonisolated public let syncState: NetworkSyncState = NetworkSyncState() - public var isSuspended: Bool = false - public var networkStatus: AnyPublisher { _networkStatus.eraseToAnyPublisher() } + public init() {} - public var paths: AnyPublisher<[[Snode]], Never> { _paths.eraseToAnyPublisher() } - public var hasPaths: Bool { !_paths.value.isEmpty } - public var currentPaths: [[Snode]] { _paths.value } - public var pathsDescription: String { _paths.value.prettifiedDescription } - public var snodeNumber: [String: Int] { _snodeNumber.value } + public func getActivePaths() async throws -> [LibSession.Path] { return [] } + public func getSwarm(for swarmPublicKey: String) async throws -> Set { return [] } + public func getRandomNodes(count: Int) async throws -> Set { return [] } - // MARK: - Initialization - - public init(using dependencies: Dependencies) { - self.dependencies = dependencies - self.dependenciesPtr = Unmanaged.passRetained(dependencies).toOpaque() - - // Create the network object - getOrCreateNetwork().sinkUntilComplete() - getOrCreateNetwork_v2().sinkUntilComplete() - - // If the app has been set to 'forceOffline' then we need to explicitly set the network status - // to disconnected (because it'll never be set otherwise) - if dependencies[feature: .forceOffline] { - DispatchQueue.global(qos: .default).async { [dependencies] in - dependencies.mutate(cache: .libSessionNetwork) { $0.setNetworkStatus(status: .disconnected) } - } - } + nonisolated public func send( + endpoint: (any EndpointType), + destination: Network.Destination, + body: Data?, + category: Network.RequestCategory, + requestTimeout: TimeInterval, + overallTimeout: TimeInterval? + ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { + return Fail(error: NetworkError.invalidState).eraseToAnyPublisher() } - deinit { - // Send completion events to the observables (so they can resubscribe to a future instance) - _paths.send(completion: .finished) - _networkStatus.send(completion: .finished) - _snodeNumber.send(completion: .finished) - - // Clear the network changed callbacks (just in case, since we are going to free the - // dependenciesPtr) and then free the network object - switch network { - case .none: break - case .some(let network): - network_set_status_changed_callback(network, nil, nil) - network_set_paths_changed_callback(network, nil, nil) - network_free(network) - } - - // Finally we need to make sure to clean up the unbalanced retain to the dependencies - Unmanaged.fromOpaque(dependenciesPtr).release() + public func send( + endpoint: (any EndpointType), + destination: Network.Destination, + body: Data?, + category: Network.RequestCategory, + requestTimeout: TimeInterval, + overallTimeout: TimeInterval? + ) async throws -> (info: ResponseInfoType, value: Data?) { + return (Network.ResponseInfo(code: -1), nil) } - // MARK: - Functions - - public func suspendNetworkAccess() { - Log.info(.network, "Network access suspended.") - isSuspended = true - - switch network { - case .none: break - case .some(let network): network_suspend(network) - } - } - - public func resumeNetworkAccess() { - isSuspended = false - Log.info(.network, "Network access resumed.") - - switch network { - case .none: break - case .some(let network): network_resume(network) - } - } - - public func getOrCreateNetwork() -> AnyPublisher?, Error> { - return Deferred { - Future?, Error> { promise in } - }.eraseToAnyPublisher() - guard !isSuspended else { - Log.warn(.network, "Attempted to access suspended network.") - return Fail(error: NetworkError.suspended).eraseToAnyPublisher() - } - - switch (network, dependencies[feature: .forceOffline]) { - case (_, true): - return Fail(error: NetworkError.serviceUnavailable) - .delay(for: .seconds(1), scheduler: DispatchQueue.global(qos: .userInitiated)) - .eraseToAnyPublisher() - - case (.some(let existingNetwork), _): - return Just(existingNetwork) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - - case (.none, _): - let useTestnet: Bool = (dependencies[feature: .serviceNetwork] == .testnet) - let isMainApp: Bool = dependencies[singleton: .appContext].isMainApp - var error: [CChar] = [CChar](repeating: 0, count: 256) - var network: UnsafeMutablePointer? - - guard let cCachePath: [CChar] = NetworkCache.snodeCachePath.cString(using: .utf8) else { - Log.error(.network, "Unable to create network object: \(LibSessionError.invalidCConversion)") - return Fail(error: NetworkError.invalidState).eraseToAnyPublisher() - } - - guard network_init(&network, cCachePath, useTestnet, !isMainApp, true, &error) else { - Log.error(.network, "Unable to create network object: \(String(cString: error))") - return Fail(error: NetworkError.invalidState).eraseToAnyPublisher() - } - - // Store the newly created network - self.network = network - - /// Register the callbacks in the next run loop (this needs to happen in a subsequent run loop because it mutates the - /// `libSessionNetwork` cache and this function gets called during init so could end up with weird order-of-execution issues) - /// - /// **Note:** We do it this way because `DispatchQueue.async` can be optimised out if the code is already running in a - /// queue with the same `qos`, this approach ensures the code will run in a subsequent run loop regardless - let concurrentQueue = DispatchQueue(label: "Network.callback.registration", attributes: .concurrent) - concurrentQueue.async(flags: .barrier) { [weak self] in - guard - let network: UnsafeMutablePointer = self?.network, - let dependenciesPtr: UnsafeMutableRawPointer = self?.dependenciesPtr - else { return } - - // Register for network status changes - network_set_status_changed_callback(network, { cStatus, ctx in - guard let ctx: UnsafeMutableRawPointer = ctx else { return } - - let status: NetworkStatus = NetworkStatus(status: cStatus) - let dependencies: Dependencies = Unmanaged.fromOpaque(ctx).takeUnretainedValue() - - // Dispatch async so we don't hold up the libSession thread that triggered the update - // or have a reentrancy issue with the mutable cache - DispatchQueue.global(qos: .default).async { - dependencies.mutate(cache: .libSessionNetwork) { $0.setNetworkStatus(status: status) } - } - }, dependenciesPtr) - - // Register for path changes - network_set_paths_changed_callback(network, { pathsPtr, pathsLen, ctx in - guard let ctx: UnsafeMutableRawPointer = ctx else { return } - - var paths: [[Snode]] = [] - - if let cPathsPtr: UnsafeMutablePointer = pathsPtr { - var cPaths: [onion_request_path] = [] - - (0...fromOpaque(ctx).takeUnretainedValue() - - DispatchQueue.global(qos: .default).async { - dependencies.mutate(cache: .libSessionNetwork) { $0.setPaths(paths: paths) } - } - }, dependenciesPtr) - } - - return Just(network) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - } - - public func getOrCreateNetwork_v2() -> AnyPublisher?, Error> { - guard !isSuspended else { - Log.warn(.network, "Attempted to access suspended network.") - return Fail(error: NetworkError.suspended).eraseToAnyPublisher() - } - - switch (network_v2, dependencies[feature: .forceOffline]) { - case (_, true): - return Fail(error: NetworkError.serviceUnavailable) - .delay(for: .seconds(1), scheduler: DispatchQueue.global(qos: .userInitiated)) - .eraseToAnyPublisher() - - case (.some(let existingNetwork), _): - return Just(existingNetwork) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - - case (.none, _): - guard let cCachePath: [CChar] = NetworkCache.snodeCachePath.cString(using: .utf8) else { - Log.error(.network, "Unable to create network object: \(LibSessionError.invalidCConversion)") - return Fail(error: NetworkError.invalidState).eraseToAnyPublisher() - } - - let useTestnet: Bool = (dependencies[feature: .serviceNetwork] == .testnet) - let isMainApp: Bool = dependencies[singleton: .appContext].isMainApp - var error: [CChar] = [CChar](repeating: 0, count: 256) - var network: UnsafeMutablePointer? - var config: session_network_config = session_network_config_default() - config.cache_refresh_using_legacy_endpoint = true - - if dependencies[feature: .serviceNetwork] == .testnet { - config.netid = SESSION_NETWORK_TESTNET - config.enforce_subnet_diversity = false // On testnet we can't do this as nodes share IPs - } - - let result: Result = cCachePath.withUnsafeBufferPointer { cachePtr in - config.cache_dir = cachePtr.baseAddress - - guard session_network_init(&network, &config, &error) else { - Log.error(.network, "Unable to create network object: \(String(cString: error))") - return .failure(NetworkError.invalidState) - } - - return .success(()) - } - - switch result { - case .success: break - case .failure(let error): return Fail(error: error).eraseToAnyPublisher() - } - - // Store the newly created network - self.network_v2 = network - -// /// Register the callbacks in the next run loop (this needs to happen in a subsequent run loop because it mutates the -// /// `libSessionNetwork` cache and this function gets called during init so could end up with weird order-of-execution issues) -// /// -// /// **Note:** We do it this way because `DispatchQueue.async` can be optimised out if the code is already running in a -// /// queue with the same `qos`, this approach ensures the code will run in a subsequent run loop regardless -// let concurrentQueue = DispatchQueue(label: "Network.callback.registration", attributes: .concurrent) -// concurrentQueue.async(flags: .barrier) { [weak self] in -// guard -// let network: UnsafeMutablePointer = self?.network, -// let dependenciesPtr: UnsafeMutableRawPointer = self?.dependenciesPtr -// else { return } -// -// // Register for network status changes -// network_set_status_changed_callback(network, { cStatus, ctx in -// guard let ctx: UnsafeMutableRawPointer = ctx else { return } -// -// let status: NetworkStatus = NetworkStatus(status: cStatus) -// let dependencies: Dependencies = Unmanaged.fromOpaque(ctx).takeUnretainedValue() -// -// // Dispatch async so we don't hold up the libSession thread that triggered the update -// // or have a reentrancy issue with the mutable cache -// DispatchQueue.global(qos: .default).async { -// dependencies.mutate(cache: .libSessionNetwork) { $0.setNetworkStatus(status: status) } -// } -// }, dependenciesPtr) -// -// // Register for path changes -// network_set_paths_changed_callback(network, { pathsPtr, pathsLen, ctx in -// guard let ctx: UnsafeMutableRawPointer = ctx else { return } -// -// var paths: [[Snode]] = [] -// -// if let cPathsPtr: UnsafeMutablePointer = pathsPtr { -// var cPaths: [onion_request_path] = [] -// -// (0...fromOpaque(ctx).takeUnretainedValue() -// -// DispatchQueue.global(qos: .default).async { -// dependencies.mutate(cache: .libSessionNetwork) { $0.setPaths(paths: paths) } -// } -// }, dependenciesPtr) -// } - - return Just(network) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - } - - public func setNetworkStatus(status: NetworkStatus) { - guard status == .disconnected || !isSuspended else { - Log.warn(.network, "Attempted to update network status to '\(status)' for suspended network, closing connections again.") - - switch network { - case .none: return - case .some(let network): return network_close_connections(network) - } - } - - // Notify any subscribers - Log.info(.network, "Network status changed to: \(status)") - _networkStatus.send(status) - } - - public func setPaths(paths: [[Snode]]) { - // Notify any subscribers - _paths.send(paths) - } - - public func setSnodeNumber(publicKey: String, value: Int) { - var snodeNumber = _snodeNumber.value - snodeNumber[publicKey] = value - _snodeNumber.send(snodeNumber) - } - - public func clearCallbacks() { - switch network { - case .none: break - case .some(let network): - network_set_status_changed_callback(network, nil, nil) - network_set_paths_changed_callback(network, nil, nil) - } - } - - public func clearSnodeCache() { - switch network { - case .none: break - case .some(let network): network_clear_cache(network) - } - } - - public func snodeCacheSize() -> Int { - switch network { - case .none: return 0 - case .some(let network): return network_get_snode_cache_size(network) - } - } - } - - // MARK: - NetworkCacheType - - /// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way - protocol NetworkImmutableCacheType: ImmutableCacheType { - var isSuspended: Bool { get } - var networkStatus: AnyPublisher { get } - - var paths: AnyPublisher<[[Snode]], Never> { get } - var hasPaths: Bool { get } - var currentPaths: [[Snode]] { get } - var pathsDescription: String { get } - var snodeNumber: [String: Int] { get } - } - - protocol NetworkCacheType: NetworkImmutableCacheType, MutableCacheType { - var isSuspended: Bool { get } - var networkStatus: AnyPublisher { get } - - var paths: AnyPublisher<[[Snode]], Never> { get } - var hasPaths: Bool { get } - var currentPaths: [[Snode]] { get } - var pathsDescription: String { get } - var snodeNumber: [String: Int] { get } - - func suspendNetworkAccess() - func resumeNetworkAccess() - func getOrCreateNetwork() -> AnyPublisher?, Error> - func getOrCreateNetwork_v2() -> AnyPublisher?, Error> - func setNetworkStatus(status: NetworkStatus) - func setPaths(paths: [[Snode]]) - func setSnodeNumber(publicKey: String, value: Int) - func clearCallbacks() - func clearSnodeCache() - func snodeCacheSize() -> Int - } - - class NoopNetworkCache: NetworkCacheType, NoopDependency { - public var isSuspended: Bool { return false } - public var networkStatus: AnyPublisher { - Just(NetworkStatus.unknown).eraseToAnyPublisher() - } - - public var paths: AnyPublisher<[[Snode]], Never> { Just([]).eraseToAnyPublisher() } - public var hasPaths: Bool { return false } - public var currentPaths: [[LibSession.Snode]] { [] } - public var pathsDescription: String { "" } - public var snodeNumber: [String: Int] { [:] } - - public func suspendNetworkAccess() {} - public func resumeNetworkAccess() {} - public func getOrCreateNetwork() -> AnyPublisher?, Error> { - return Fail(error: NetworkError.invalidState) - .eraseToAnyPublisher() - } - public func getOrCreateNetwork_v2() -> AnyPublisher?, Error> { - return Fail(error: NetworkError.invalidState) - .eraseToAnyPublisher() + public func checkClientVersion(ed25519SecretKey: [UInt8]) async throws -> (info: ResponseInfoType, value: AppVersionResponse) { + return ( + Network.ResponseInfo(code: -1), + AppVersionResponse( + version: "", + updated: nil, + name: nil, + notes: nil, + assets: nil, + prerelease: nil + ) + ) } - public func setNetworkStatus(status: NetworkStatus) {} - public func setPaths(paths: [[LibSession.Snode]]) {} - public func setSnodeNumber(publicKey: String, value: Int) {} - public func clearCallbacks() {} - public func clearSnodeCache() {} - public func snodeCacheSize() -> Int { 0 } + public func setNetworkStatus(status: NetworkStatus) async {} + public func suspendNetworkAccess() async {} + public func resumeNetworkAccess() async {} + public func finishCurrentObservations() async {} + public func clearCache() async {} } } extension session_network_config: @retroactive CAccessible, @retroactive CMutable {} +extension session_onion_path_metadata: @retroactive CAccessible {} +extension session_lokinet_tunnel_metadata: @retroactive CAccessible {} diff --git a/SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift index ff99bea9d5..7d15ebe4e6 100644 --- a/SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift +++ b/SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift @@ -8,7 +8,7 @@ import SessionUtilitiesKit // MARK: Request - SnodeAPI public extension Request where Endpoint == SnodeAPI.Endpoint { - init(//)( + init( endpoint: SnodeAPI.Endpoint, snode: LibSession.Snode, swarmPublicKey: String? = nil, @@ -16,7 +16,7 @@ public extension Request where Endpoint == SnodeAPI.Endpoint { requestTimeout: TimeInterval = Network.defaultTimeout, overallTimeout: TimeInterval? = nil, retryCount: Int = 0 - ) throws {//where T == SnodeRequest { + ) throws { self = try Request( endpoint: endpoint, destination: .snode( @@ -27,70 +27,24 @@ public extension Request where Endpoint == SnodeAPI.Endpoint { requestTimeout: requestTimeout, overallTimeout: overallTimeout, retryCount: retryCount -// body: SnodeRequest( -// endpoint: endpoint, -// body: body -// ) ) } - init(//)( + init( endpoint: SnodeAPI.Endpoint, swarmPublicKey: String, body: T, requestTimeout: TimeInterval = Network.defaultTimeout, overallTimeout: TimeInterval? = nil, - retryCount: Int = 0, - snodeRetrievalRetryCount: Int = SnodeAPI.maxRetryCount - ) throws {//where T == SnodeRequest { - self = try Request( - endpoint: endpoint, - destination: .randomSnode( - swarmPublicKey: swarmPublicKey, - snodeRetrievalRetryCount: snodeRetrievalRetryCount - ), - body: body, - requestTimeout: requestTimeout, - overallTimeout: overallTimeout, - retryCount: retryCount -// body: SnodeRequest( -// endpoint: endpoint, -// body: body -// ) - ) - } - - init(//)( - endpoint: Endpoint, - swarmPublicKey: String, - requiresLatestNetworkTime: Bool, - body: T,//B, - requestTimeout: TimeInterval = Network.defaultTimeout, - overallTimeout: TimeInterval? = nil, - retryCount: Int = 0, - snodeRetrievalRetryCount: Int = SnodeAPI.maxRetryCount - ) throws where T: UpdatableTimestamp{//where T == SnodeRequest, B: Encodable & UpdatableTimestamp { + retryCount: Int = 0 + ) throws { self = try Request( endpoint: endpoint, - destination: .randomSnodeLatestNetworkTimeTarget( - swarmPublicKey: swarmPublicKey, - snodeRetrievalRetryCount: snodeRetrievalRetryCount, - bodyWithUpdatedTimestampMs: { timestampMs, dependencies in - body.with(timestampMs: timestampMs) -// SnodeRequest( -// endpoint: endpoint, -// body: body.with(timestampMs: timestampMs) -// ) - } - ), + destination: .randomSnode(swarmPublicKey: swarmPublicKey), body: body, requestTimeout: requestTimeout, overallTimeout: overallTimeout, retryCount: retryCount -// body: SnodeRequest( -// endpoint: endpoint, -// body: body -// ) ) } } diff --git a/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift index 02c22ab32e..530d422ba0 100644 --- a/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift +++ b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift @@ -126,7 +126,6 @@ public final class SnodeAPI { requests: [any ErasedPreparedRequest], requireAllBatchResponses: Bool, swarmPublicKey: String, - snodeRetrievalRetryCount: Int, requestTimeout: TimeInterval = Network.defaultTimeout, overallTimeout: TimeInterval? = nil, using dependencies: Dependencies @@ -138,8 +137,7 @@ public final class SnodeAPI { swarmPublicKey: swarmPublicKey, body: Network.BatchRequest(requestsKey: .requests, requests: requests), requestTimeout: requestTimeout, - overallTimeout: overallTimeout, - snodeRetrievalRetryCount: snodeRetrievalRetryCount + overallTimeout: overallTimeout ), responseType: Network.BatchResponse.self, requireAllBatchResponses: requireAllBatchResponses, @@ -215,7 +213,7 @@ public final class SnodeAPI { public static func getSessionID( for onsName: String, using dependencies: Dependencies - ) -> AnyPublisher { + ) async throws -> String { let validationCount = 3 // The name must be lowercased diff --git a/SessionNetworkingKit/Types/Destination.swift b/SessionNetworkingKit/Types/Destination.swift index 9e48a355e4..3037fe7c9c 100644 --- a/SessionNetworkingKit/Types/Destination.swift +++ b/SessionNetworkingKit/Types/Destination.swift @@ -106,12 +106,7 @@ public extension Network { } case snode(LibSession.Snode, swarmPublicKey: String?) - case randomSnode(swarmPublicKey: String, snodeRetrievalRetryCount: Int) - case randomSnodeLatestNetworkTimeTarget( - swarmPublicKey: String, - snodeRetrievalRetryCount: Int, - bodyWithUpdatedTimestampMs: ((UInt64, Dependencies) -> Encodable?) - ) + case randomSnode(swarmPublicKey: String) case server(info: ServerInfo) case serverUpload(info: ServerInfo, fileName: String?) case serverDownload(info: ServerInfo) @@ -129,7 +124,7 @@ public extension Network { public var url: URL? { switch self { case .server(let info), .serverUpload(let info, _), .serverDownload(let info): return try? info.url - case .snode, .randomSnode, .randomSnodeLatestNetworkTimeTarget: return nil + case .snode, .randomSnode: return nil case .cached: return nil } } @@ -139,7 +134,7 @@ public extension Network { case .server(let info), .serverUpload(let info, _), .serverDownload(let info): return info.headers - case .snode, .randomSnode, .randomSnodeLatestNetworkTimeTarget: return [:] + case .snode, .randomSnode: return [:] case .cached(_, _, _, let headers, _): return headers } } @@ -258,17 +253,8 @@ public extension Network { lhsSwarmPublicKey == rhsSwarmPublicKey ) - case (.randomSnode(let lhsSwarmPublicKey, let lhsRetryCount), .randomSnode(let rhsSwarmPublicKey, let rhsRetryCount)): - return ( - lhsSwarmPublicKey == rhsSwarmPublicKey && - lhsRetryCount == rhsRetryCount - ) - - case (.randomSnodeLatestNetworkTimeTarget(let lhsSwarmPublicKey, let lhsRetryCount, _), .randomSnodeLatestNetworkTimeTarget(let rhsSwarmPublicKey, let rhsRetryCount, _)): - return ( - lhsSwarmPublicKey == rhsSwarmPublicKey && - lhsRetryCount == rhsRetryCount - ) + case (.randomSnode(let lhsSwarmPublicKey), .randomSnode(let rhsSwarmPublicKey)): + return (lhsSwarmPublicKey == rhsSwarmPublicKey) case (.server(let lhsInfo), .server(let rhsInfo)): return (lhsInfo == rhsInfo) diff --git a/SessionNetworkingKit/Types/Network.swift b/SessionNetworkingKit/Types/Network.swift index 08cf2916d6..8124825278 100644 --- a/SessionNetworkingKit/Types/Network.swift +++ b/SessionNetworkingKit/Types/Network.swift @@ -18,10 +18,16 @@ public extension Singleton { // MARK: - NetworkType public protocol NetworkType { - func getSwarm(for swarmPublicKey: String) -> AnyPublisher, Error> - func getRandomNodes(count: Int) -> AnyPublisher, Error> + var isSuspended: Bool { get async } + nonisolated var networkStatus: AsyncStream { get } + nonisolated var syncState: NetworkSyncState { get } - func send( + func getActivePaths() async throws -> [LibSession.Path] + func getSwarm(for swarmPublicKey: String) async throws -> Set + func getRandomNodes(count: Int) async throws -> Set + + @available(*, deprecated, message: "We want to shift from Combine to Async/Await when possible") + nonisolated func send( endpoint: (any EndpointType), destination: Network.Destination, body: Data?, @@ -30,7 +36,32 @@ public protocol NetworkType { overallTimeout: TimeInterval? ) -> AnyPublisher<(ResponseInfoType, Data?), Error> - func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error> + func send( + endpoint: (any EndpointType), + destination: Network.Destination, + body: Data?, + category: Network.RequestCategory, + requestTimeout: TimeInterval, + overallTimeout: TimeInterval? + ) async throws -> (info: ResponseInfoType, value: Data?) + + func checkClientVersion(ed25519SecretKey: [UInt8]) async throws -> (info: ResponseInfoType, value: AppVersionResponse) + + func setNetworkStatus(status: NetworkStatus) async + func suspendNetworkAccess() async + func resumeNetworkAccess() async + func finishCurrentObservations() async + func clearCache() async +} + +/// We manually handle thread-safety using the `NSLock` so can ensure this is `Sendable` +public final class NetworkSyncState: @unchecked Sendable { + private let lock = NSLock() + private var _isSuspended: Bool = false + + public var isSuspended: Bool { lock.withLock { _isSuspended } } + + func update(isSuspended: Bool) { lock.withLock { self._isSuspended = isSuspended } } } // MARK: - Network Constants @@ -142,6 +173,7 @@ public extension Network { fileName: nil ), body: data, + category: .upload, requestTimeout: Network.fileUploadTimeout, overallTimeout: overallTimeout ), @@ -162,6 +194,7 @@ public extension Network { x25519PublicKey: FileServer.fileServerPublicKey, fileName: nil ), + category: .download, requestTimeout: Network.fileUploadTimeout ), responseType: Data.self, diff --git a/SessionNetworkingKit/Types/PreparedRequest+Sending.swift b/SessionNetworkingKit/Types/PreparedRequest+Sending.swift index 295407765f..5f11523ecd 100644 --- a/SessionNetworkingKit/Types/PreparedRequest+Sending.swift +++ b/SessionNetworkingKit/Types/PreparedRequest+Sending.swift @@ -18,22 +18,49 @@ public extension Network.PreparedRequest { .decoded(with: self, using: dependencies) .retry(retryCount, using: dependencies) .handleEvents( - receiveSubscription: { _ in self.subscriptionHandler?() }, receiveOutput: self.outputEventHandler, - receiveCompletion: self.completionEventHandler, - receiveCancel: self.cancelEventHandler + receiveCompletion: self.completionEventHandler ) .eraseToAnyPublisher() } -} - -public extension Optional { - func send(using dependencies: Dependencies) -> AnyPublisher<(ResponseInfoType, R), Error> where Wrapped == Network.PreparedRequest { - guard let instance: Wrapped = self else { - return Fail(error: NetworkError.invalidPreparedRequest) - .eraseToAnyPublisher() + + func send(using dependencies: Dependencies) async throws -> (info: ResponseInfoType, value: R) { + /// Need to calculate a `finalRetryCount` to ensure the request is sent at least once + var lastError: Error? + let finalRetryCount: Int = (retryCount < 0 ? 1 : retryCount + 1) + + for _ in 0.. R { + return try await send(using: dependencies).value } } diff --git a/SessionNetworkingKit/Types/PreparedRequest.swift b/SessionNetworkingKit/Types/PreparedRequest.swift index c737132930..1a9597f8f2 100644 --- a/SessionNetworkingKit/Types/PreparedRequest.swift +++ b/SessionNetworkingKit/Types/PreparedRequest.swift @@ -13,6 +13,12 @@ public extension Network { fileprivate let info: ResponseInfoType fileprivate let originalData: Any fileprivate let convertedData: R + + public init(info: ResponseInfoType, originalData: Any, convertedData: R) { + self.info = info + self.originalData = originalData + self.convertedData = convertedData + } } public let endpoint: (any EndpointType) @@ -27,10 +33,8 @@ public extension Network { public let responseType: R.Type public let cachedResponse: CachedResponse? fileprivate let responseConverter: ((ResponseInfoType, Any) throws -> R) - public let subscriptionHandler: (() -> Void)? public let outputEventHandler: (((CachedResponse)) -> Void)? public let completionEventHandler: ((Subscribers.Completion) -> Void)? - public let cancelEventHandler: (() -> Void)? // The following types are needed for `BatchRequest` handling public let method: HTTPMethod @@ -52,9 +56,6 @@ public extension Network { request: Request, responseType: R.Type, requireAllBatchResponses: Bool = true, -// retryCount: Int = 0, -// requestTimeout: TimeInterval = Network.defaultTimeout, -// requestAndPathBuildTimeout: TimeInterval? = nil, using dependencies: Dependencies ) throws where R: Decodable { try self.init( @@ -62,9 +63,6 @@ public extension Network { responseType: responseType, additionalSignatureData: NoSignature.null, requireAllBatchResponses: requireAllBatchResponses, -// retryCount: retryCount, -// requestTimeout: requestTimeout, -// requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) } @@ -74,9 +72,6 @@ public extension Network { responseType: R.Type, additionalSignatureData: S?, requireAllBatchResponses: Bool = true, -// retryCount: Int = 0, -// requestTimeout: TimeInterval = Network.defaultTimeout, -// requestAndPathBuildTimeout: TimeInterval? = nil, using dependencies: Dependencies ) throws where R: Decodable { let batchRequests: [Network.BatchRequest.Child]? = (request.body as? BatchRequestChildRetrievable)?.requests @@ -207,7 +202,6 @@ public extension Network { } } }() - self.subscriptionHandler = nil self.completionEventHandler = { guard let subRequestEventHandlers: [((Subscribers.Completion) -> Void)] = batchRequests? @@ -219,15 +213,6 @@ public extension Network { // individual subRequest results here return { result in subRequestEventHandlers.forEach { $0(result) } } }() - self.cancelEventHandler = { - guard - let subRequestEventHandlers: [(() -> Void)] = batchRequests? - .compactMap({ $0.request.cancelEventHandler }), - !subRequestEventHandlers.isEmpty - else { return nil } - - return { subRequestEventHandlers.forEach { $0() } } - }() // The following data is needed in this type for handling batch requests self.method = request.destination.method @@ -285,10 +270,8 @@ public extension Network { responseType: R.Type, cachedResponse: CachedResponse?, responseConverter: @escaping (ResponseInfoType, Any) throws -> R, - subscriptionHandler: (() -> Void)?, outputEventHandler: ((CachedResponse) -> Void)?, completionEventHandler: ((Subscribers.Completion) -> Void)?, - cancelEventHandler: (() -> Void)?, method: HTTPMethod, endpointName: String, headers: [HTTPHeader: String], @@ -315,10 +298,8 @@ public extension Network { self.responseType = responseType self.cachedResponse = cachedResponse self.responseConverter = responseConverter - self.subscriptionHandler = subscriptionHandler self.outputEventHandler = outputEventHandler self.completionEventHandler = completionEventHandler - self.cancelEventHandler = cancelEventHandler // The following data is needed in this type for handling batch requests self.method = method @@ -349,7 +330,6 @@ public protocol ErasedPreparedRequest { var erasedResponseConverter: ((ResponseInfoType, Any) throws -> Any) { get } var erasedOutputEventHandler: ((ResponseInfoType, Any, Any) -> Void)? { get } var completionEventHandler: ((Subscribers.Completion) -> Void)? { get } - var cancelEventHandler: (() -> Void)? { get } func batchRequestEndpoint(of type: E.Type) -> E? func encodeForBatchRequest(to encoder: Encoder) throws @@ -469,10 +449,8 @@ public extension Network.PreparedRequest { responseType: responseType, cachedResponse: cachedResponse, responseConverter: responseConverter, - subscriptionHandler: subscriptionHandler, outputEventHandler: outputEventHandler, completionEventHandler: completionEventHandler, - cancelEventHandler: cancelEventHandler, method: method, endpointName: endpointName, headers: signedDestination.headers, @@ -526,7 +504,6 @@ public extension Network.PreparedRequest { } }, responseConverter: responseConverter, - subscriptionHandler: subscriptionHandler, outputEventHandler: self.outputEventHandler.map { eventHandler in { data in guard let validResponse: R = try? originalConverter(data.info, data.originalData) else { @@ -541,7 +518,6 @@ public extension Network.PreparedRequest { } }, completionEventHandler: completionEventHandler, - cancelEventHandler: cancelEventHandler, method: method, endpointName: endpointName, headers: headers, @@ -559,23 +535,9 @@ public extension Network.PreparedRequest { } func handleEvents( - receiveSubscription: (() -> Void)? = nil, receiveOutput: (((ResponseInfoType, R)) -> Void)? = nil, - receiveCompletion: ((Subscribers.Completion) -> Void)? = nil, - receiveCancel: (() -> Void)? = nil + receiveCompletion: ((Subscribers.Completion) -> Void)? = nil ) -> Network.PreparedRequest { - let subscriptionHandler: (() -> Void)? = { - switch (self.subscriptionHandler, receiveSubscription) { - case (.none, .none): return nil - case (.some(let eventHandler), .none): return eventHandler - case (.none, .some(let eventHandler)): return eventHandler - case (.some(let originalEventHandler), .some(let eventHandler)): - return { - originalEventHandler() - eventHandler() - } - } - }() let outputEventHandler: ((CachedResponse) -> Void)? = { switch (self.outputEventHandler, receiveOutput) { case (.none, .none): return nil @@ -604,18 +566,6 @@ public extension Network.PreparedRequest { } } }() - let cancelEventHandler: (() -> Void)? = { - switch (self.cancelEventHandler, receiveCancel) { - case (.none, .none): return nil - case (.some(let eventHandler), .none): return eventHandler - case (.none, .some(let eventHandler)): return eventHandler - case (.some(let originalEventHandler), .some(let eventHandler)): - return { - originalEventHandler() - eventHandler() - } - } - }() return Network.PreparedRequest( endpoint: endpoint, @@ -630,10 +580,8 @@ public extension Network.PreparedRequest { responseType: responseType, cachedResponse: cachedResponse, responseConverter: responseConverter, - subscriptionHandler: subscriptionHandler, outputEventHandler: outputEventHandler, completionEventHandler: completionEventHandler, - cancelEventHandler: cancelEventHandler, method: method, endpointName: endpointName, headers: headers, @@ -679,10 +627,8 @@ public extension Network.PreparedRequest { convertedData: cachedResponse ), responseConverter: { _, _ in cachedResponse }, - subscriptionHandler: nil, outputEventHandler: nil, completionEventHandler: nil, - cancelEventHandler: nil, method: .get, endpointName: E.name, headers: [:], @@ -722,6 +668,57 @@ public extension Decodable { } } +public extension Network.PreparedRequest { + func decode( + info: ResponseInfoType, + data: Data?, + using dependencies: Dependencies + ) throws -> (originalData: Any, convertedData: R) { + // Depending on the 'originalType' we need to process the response differently + let targetData: Any = try { + switch originalType { + case let erasedBatchResponse as ErasedBatchResponseMap.Type: + let response: Network.BatchResponse = try Network.BatchResponse.decodingResponses( + from: data, + as: batchResponseTypes, + requireAllResults: requireAllBatchResponses, + using: dependencies + ) + + return try erasedBatchResponse.from( + batchEndpoints: batchEndpoints, + response: response + ) + + case is Network.BatchResponse.Type: + return try Network.BatchResponse.decodingResponses( + from: data, + as: batchResponseTypes, + requireAllResults: requireAllBatchResponses, + using: dependencies + ) + + case is NoResponse.Type: return NoResponse() + case is Optional.Type: return data as Any + case is Data.Type: return try data ?? { throw NetworkError.parsingFailed }() + + case is _OptionalProtocol.Type: + guard let data: Data = data else { return data as Any } + + return try originalType.decoded(from: data, using: dependencies) + + default: + guard let data: Data = data else { throw NetworkError.parsingFailed } + + return try originalType.decoded(from: data, using: dependencies) + } + }() + + // Generate and return the converted data + return (targetData, try responseConverter(info, targetData)) + } +} + public extension Publisher where Output == (ResponseInfoType, Data?), Failure == Error { func decoded( with preparedRequest: Network.PreparedRequest, diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 0fe6785ada..c188855d9a 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -100,7 +100,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension } /// Setup Version Info and Network - dependencies.warmCache(cache: .appVersion) + dependencies.warm(cache: .appVersion) /// Configure the different targets SNUtilitiesKit.configure( diff --git a/SessionShareExtension/ShareNavController.swift b/SessionShareExtension/ShareNavController.swift index b1f420316f..8160588d8c 100644 --- a/SessionShareExtension/ShareNavController.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -41,11 +41,9 @@ final class ShareNavController: UINavigationController { } guard !SNUtilitiesKit.isRunningTests else { return } - - dependencies.warmCache(cache: .appVersion) - AppSetup.setupEnvironment( - appSpecificBlock: { [dependencies] in + Task(priority: .userInitiated) { [weak self, dependencies] in + do { // stringlint:ignore_start if !Log.loggerExists(withPrefix: "SessionShareExtension") { Log.setup(with: Logger( @@ -58,45 +56,37 @@ final class ShareNavController: UINavigationController { } // stringlint:ignore_stop - // Setup LibSession - dependencies.warmCache(cache: .libSessionNetwork) + var backgroundTask: SessionBackgroundTask? = SessionBackgroundTask(label: #function, using: dependencies) + try await AppSetup.performSetup(using: dependencies) + try await AppSetup.performDatabaseMigrations(using: dependencies) + try await AppSetup.postMigrationSetup(using: dependencies) - // Configure the different targets - SNUtilitiesKit.configure( - networkMaxFileSize: Network.maxFileSize, - maxValidImageDimention: ImageDataManager.DataSource.maxValidDimension, - using: dependencies - ) - SNMessagingKit.configure(using: dependencies) - }, - migrationsCompletion: { [weak self, dependencies] result in - switch result { - case .failure: Log.error("Failed to complete migrations") - case .success: - DispatchQueue.main.async { - /// Because the `SessionUIKit` target doesn't depend on the `SessionUtilitiesKit` dependency (it shouldn't - /// need to since it should just be UI) but since the theme settings are stored in the database we need to pass these through - /// to `SessionUIKit` and expose a mechanism to save updated settings - this is done here (once the migrations complete) - SNUIKit.configure( - with: SAESNUIKitConfig(using: dependencies), - themeSettings: dependencies.mutate(cache: .libSession) { cache -> ThemeSettings in - ( - cache.get(.theme), - cache.get(.themePrimaryColor), - cache.get(.themeMatchSystemDayNightCycle) - ) - } + /// The 'if' is only there to prevent the "variable never read" warning from showing + if backgroundTask != nil { backgroundTask = nil } + + let maybeUserMetadata: ExtensionHelper.UserMetadata? = dependencies[singleton: .extensionHelper] + .loadUserMetadata() + + await MainActor.run { [weak self] in + /// `SessionUIKit` is isolated from the other targets (since it should only contain UI code) but the theme settings are + /// stored in `libSession` so we need to pass these through and expose a mechanism to save updated settings - this + /// is done here + SNUIKit.configure( + with: SAESNUIKitConfig(using: dependencies), + themeSettings: dependencies.mutate(cache: .libSession) { cache -> ThemeSettings in + ( + cache.get(.theme), + cache.get(.themePrimaryColor), + cache.get(.themeMatchSystemDayNightCycle) ) - - let maybeUserMetadata: ExtensionHelper.UserMetadata? = dependencies[singleton: .extensionHelper] - .loadUserMetadata() - - self?.versionMigrationsDidComplete(userMetadata: maybeUserMetadata) } + ) + + self?.versionMigrationsDidComplete(userMetadata: maybeUserMetadata) } - }, - using: dependencies - ) + } + catch { Log.error("Failed to complete migrations") } + } // We don't need to use "screen protection" in the SAE. NotificationCenter.default.addObserver( diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index b460349b73..d571b47e1e 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -124,9 +124,11 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView // When the thread picker disappears it means the user has left the screen (this will be called // whether the user has sent the message or cancelled sending) - viewModel.dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() } - viewModel.dependencies[singleton: .storage].suspendDatabaseAccess() - Log.flush() + Task { [dependencies = viewModel.dependencies] in + await dependencies[singleton: .network].suspendNetworkAccess() + dependencies[singleton: .storage].suspendDatabaseAccess() + Log.flush() + } } // MARK: Layout @@ -263,187 +265,182 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView shareNavController?.dismiss(animated: true, completion: nil) - ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "sending".localized()) { [dependencies = viewModel.dependencies] activityIndicator in - dependencies[singleton: .storage].resumeDatabaseAccess() - dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() } - + ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "sending".localized()) { [weak self, dependencies = viewModel.dependencies] activityIndicator in /// When we prepare the message we set the timestamp to be the `dependencies[cache: .snodeAPI].currentOffsetTimestampMs()` /// but won't actually have a value because the share extension won't have talked to a service node yet which can cause /// issues with Disappearing Messages, as a result we need to explicitly `getNetworkTime` in order to ensure it's accurate /// before we create the interaction var sharedInteractionId: Int64? - dependencies[singleton: .network] - .getSwarm(for: swarmPublicKey) - .tryFlatMapWithRandomSnode(using: dependencies) { snode in - try SnodeAPI - .preparedGetNetworkTime(from: snode, using: dependencies) - .send(using: dependencies) - } - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .flatMapStorageWritePublisher(using: dependencies) { db, _ -> (Message, Message.Destination, Int64?, AuthenticationMethod, [Network.PreparedRequest<(attachment: Attachment, fileId: String)>]) in - guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { - throw MessageSenderError.noThread + Task { [weak self] in + dependencies[singleton: .storage].resumeDatabaseAccess() + await dependencies[singleton: .network].resumeNetworkAccess() + + do { + typealias MessageData = ( + message: Message, + destination: Message.Destination, + interactionId: Int64?, + authMethod: AuthenticationMethod, + preparedUploads: [Network.PreparedRequest<(attachment: Attachment, fileId: String)>] + ) + + let swarm: Set = try await dependencies[singleton: .network].getSwarm(for: swarmPublicKey) + guard let randomSnode: LibSession.Snode = dependencies.randomElement(swarm) else { + throw SnodeAPIError.insufficientSnodes } - // Update the thread to be visible (if it isn't already) - if !thread.shouldBeVisible || thread.pinnedPriority == LibSession.hiddenPriority { - try SessionThread.updateVisibility( + let networkTime: UInt64 = try await SnodeAPI + .preparedGetNetworkTime(from: randomSnode, using: dependencies) + .send(using: dependencies) + let data: MessageData = try await dependencies[singleton: .storage].writeAsync { db in + guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { + throw MessageSenderError.noThread + } + + // Update the thread to be visible (if it isn't already) + if !thread.shouldBeVisible || thread.pinnedPriority == LibSession.hiddenPriority { + try SessionThread.updateVisibility( + db, + threadId: threadId, + isVisible: true, + additionalChanges: [SessionThread.Columns.isDraft.set(to: false)], + using: dependencies + ) + } + + // Create the interaction + let sentTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + let destinationDisappearingMessagesConfiguration: DisappearingMessagesConfiguration? = try? DisappearingMessagesConfiguration + .filter(id: threadId) + .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) + .fetchOne(db) + let interaction: Interaction = try Interaction( + threadId: threadId, + threadVariant: threadVariant, + authorId: userSessionId.hexString, + variant: .standardOutgoing, + body: body, + timestampMs: sentTimestampMs, + hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body, using: dependencies), + expiresInSeconds: destinationDisappearingMessagesConfiguration?.expiresInSeconds(), + expiresStartedAtMs: destinationDisappearingMessagesConfiguration?.initialExpiresStartedAtMs( + sentTimestampMs: Double(sentTimestampMs) + ), + linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil), + using: dependencies + ).inserted(db) + sharedInteractionId = interaction.id + + guard let interactionId: Int64 = interaction.id else { + throw StorageError.failedToSave + } + + // If the user is sharing a Url, there is a LinkPreview and it doesn't match an existing + // one then add it now + if + isSharingUrl, + let linkPreviewDraft: LinkPreviewDraft = attachments.first?.linkPreviewDraft, + (try? interaction.linkPreview.isEmpty(db)) == true + { + try LinkPreview( + url: linkPreviewDraft.urlString, + title: linkPreviewDraft.title, + attachmentId: LinkPreview + .generateAttachmentIfPossible( + imageData: linkPreviewDraft.jpegImageData, + type: .jpeg, + using: dependencies + )? + .inserted(db) + .id, + using: dependencies + ).insert(db) + } + + // Process any attachments + try AttachmentUploader.process( + db, + attachments: AttachmentUploader.prepare( + attachments: finalAttachments, + using: dependencies + ), + for: interactionId + ) + + // Using the same logic as the `MessageSendJob` retrieve + let authMethod: AuthenticationMethod = try Authentication.with( db, threadId: threadId, - isVisible: true, - additionalChanges: [SessionThread.Columns.isDraft.set(to: false)], + threadVariant: threadVariant, using: dependencies ) - } - - // Create the interaction - let sentTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() - let destinationDisappearingMessagesConfiguration: DisappearingMessagesConfiguration? = try? DisappearingMessagesConfiguration - .filter(id: threadId) - .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) - .fetchOne(db) - let interaction: Interaction = try Interaction( - threadId: threadId, - threadVariant: threadVariant, - authorId: userSessionId.hexString, - variant: .standardOutgoing, - body: body, - timestampMs: sentTimestampMs, - hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body, using: dependencies), - expiresInSeconds: destinationDisappearingMessagesConfiguration?.expiresInSeconds(), - expiresStartedAtMs: destinationDisappearingMessagesConfiguration?.initialExpiresStartedAtMs( - sentTimestampMs: Double(sentTimestampMs) - ), - linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil), - using: dependencies - ).inserted(db) - sharedInteractionId = interaction.id - - guard let interactionId: Int64 = interaction.id else { - throw StorageError.failedToSave - } - - // If the user is sharing a Url, there is a LinkPreview and it doesn't match an existing - // one then add it now - if - isSharingUrl, - let linkPreviewDraft: LinkPreviewDraft = attachments.first?.linkPreviewDraft, - (try? interaction.linkPreview.isEmpty(db)) == true - { - try LinkPreview( - url: linkPreviewDraft.urlString, - title: linkPreviewDraft.title, - attachmentId: LinkPreview - .generateAttachmentIfPossible( - imageData: linkPreviewDraft.jpegImageData, - type: .jpeg, + let attachmentState: MessageSendJob.AttachmentState = try MessageSendJob + .fetchAttachmentState(db, interactionId: interactionId) + let preparedUploads: [Network.PreparedRequest<(attachment: Attachment, fileId: String)>] = try Attachment + .filter(ids: attachmentState.allAttachmentIds) + .fetchAll(db) + .map { attachment in + try AttachmentUploader.preparedUpload( + attachment: attachment, + logCategory: nil, + authMethod: authMethod, using: dependencies - )? - .inserted(db) - .id, - using: dependencies - ).insert(db) + ) + } + let visibleMessage: VisibleMessage = VisibleMessage.from(db, interaction: interaction) + let destination: Message.Destination = try Message.Destination.from( + db, + threadId: threadId, + threadVariant: threadVariant + ) + + return (visibleMessage, destination, interaction.id, authMethod, preparedUploads) } - // Process any attachments - try AttachmentUploader.process( - db, - attachments: AttachmentUploader.prepare( - attachments: finalAttachments, - using: dependencies - ), - for: interactionId - ) + /// Perform any uploads + var uploadResults: [(Attachment, String)] = [] + for uploadRequest in data.preparedUploads { + uploadResults.append(try await uploadRequest.send(using: dependencies)) + + } - // Using the same logic as the `MessageSendJob` retrieve - let authMethod: AuthenticationMethod = try Authentication.with( - db, - threadId: threadId, - threadVariant: threadVariant, + /// Send the message + let sentMessage: Message = try await MessageSender.preparedSend( + message: data.message, + to: data.destination, + namespace: data.destination.defaultNamespace, + interactionId: data.interactionId, + attachments: uploadResults, + authMethod: data.authMethod, + onEvent: MessageSender.standardEventHandling(using: dependencies), using: dependencies ) - let attachmentState: MessageSendJob.AttachmentState = try MessageSendJob - .fetchAttachmentState(db, interactionId: interactionId) - let preparedUploads: [Network.PreparedRequest<(attachment: Attachment, fileId: String)>] = try Attachment - .filter(ids: attachmentState.allAttachmentIds) - .fetchAll(db) - .map { attachment in - try AttachmentUploader.preparedUpload( - attachment: attachment, - logCategory: nil, - authMethod: authMethod, - using: dependencies - ) - } - let visibleMessage: VisibleMessage = VisibleMessage.from(db, interaction: interaction) - let destination: Message.Destination = try Message.Destination.from( - db, - threadId: threadId, - threadVariant: threadVariant - ) + .send(using: dependencies) - return (visibleMessage, destination, interaction.id, authMethod, preparedUploads) - } - .flatMap { (message: Message, destination: Message.Destination, interactionId: Int64?, authMethod: AuthenticationMethod, preparedUploads: [Network.PreparedRequest<(attachment: Attachment, fileId: String)>]) -> AnyPublisher<(Message, Message.Destination, Int64?, AuthenticationMethod, [(Attachment, String)]), Error> in - guard !preparedUploads.isEmpty else { - return Just((message, destination, interactionId, authMethod, [])) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - - return Publishers - .MergeMany(preparedUploads.map { $0.send(using: dependencies) }) - .collect() - .map { results in (message, destination, interactionId, authMethod, results.map { _, value in value }) } - .eraseToAnyPublisher() - } - .tryFlatMap { message, destination, interactionId, authMethod, attachments -> AnyPublisher<(Message, [Attachment]), Error> in - try MessageSender - .preparedSend( - message: message, - to: destination, - namespace: destination.defaultNamespace, - interactionId: interactionId, - attachments: attachments, - authMethod: authMethod, - onEvent: MessageSender.standardEventHandling(using: dependencies), - using: dependencies - ) - .send(using: dependencies) - .map { _, message in - (message, attachments.map { attachment, _ in attachment }) - } - .eraseToAnyPublisher() - } - .handleEvents( - receiveOutput: { _, attachments in - guard !attachments.isEmpty else { return } - - /// Need to actually save the uploaded attachments now that we are done - dependencies[singleton: .storage].write { db in - attachments.forEach { attachment in + /// Need to actually save the uploaded attachments now that we are done + if !uploadResults.isEmpty { + try? await dependencies[singleton: .storage].writeAsync { db in + uploadResults.forEach { attachment, _ in try? attachment.upsert(db) } } } - ) - .receive(on: DispatchQueue.main) - .sinkUntilComplete( - receiveCompletion: { [weak self] result in - dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() } - dependencies[singleton: .storage].suspendDatabaseAccess() - Log.flush() - activityIndicator.dismiss { } - - switch result { - case .finished: self?.shareNavController?.shareViewWasCompleted( - threadId: threadId, - interactionId: sharedInteractionId - ) - case .failure(let error): self?.shareNavController?.shareViewFailed(error: error) - } - } - ) + + self?.shareNavController?.shareViewWasCompleted( + threadId: threadId, + interactionId: sharedInteractionId + ) + } + catch { + self?.shareNavController?.shareViewFailed(error: error) + } + + /// Shut everything down again + await dependencies[singleton: .network].suspendNetworkAccess() + dependencies[singleton: .storage].suspendDatabaseAccess() + Log.flush() + activityIndicator.dismiss { } + } } } diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 6d1c899bdf..fffc71c648 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -255,15 +255,12 @@ open class Storage { public func perform( migrations: [Migration.Type], - async: Bool = true, - onProgressUpdate: ((CGFloat, TimeInterval) -> ())?, - onComplete: @escaping (Result) -> () - ) { + onProgressUpdate: ((CGFloat, TimeInterval) -> ())? + ) async throws { guard isValid, let dbWriter: DatabaseWriter = dbWriter else { let error: Error = (startupError ?? StorageError.startupFailed) Log.error(.storage, "Startup failed with error: \(error)") - onComplete(.failure(error)) - return + throw error } // Setup and run any required migrations @@ -277,7 +274,7 @@ open class Storage { // Determine which migrations need to be performed and gather the relevant settings needed to // inform the app of progress/states - let completedMigrations: [String] = (try? dbWriter.read { db in try migrator.completedMigrations(db) }) + let completedMigrations: [String] = (try? await dbWriter.read { [migrator] db in try migrator.completedMigrations(db) }) .defaulting(to: []) let unperformedMigrations: [Migration.Type] = migrations .reduce(into: []) { result, next in @@ -304,57 +301,15 @@ open class Storage { ) let totalProgress: CGFloat = (completedExpectedDuration / totalMinExpectedDuration) - DispatchQueue.main.async { + Task { @MainActor in onProgressUpdate?(totalProgress, totalMinExpectedDuration) } } - let migrationCompleted: (Result) -> () = { [weak self, migrator, dbWriter, dependencies] result in - // Make sure to transition the progress updater to 100% for the final migration (just - // in case the migration itself didn't update to 100% itself) - if let lastMigrationIdentifier: String = unperformedMigrations.last?.identifier { - MigrationExecution.current?.progressUpdater(lastMigrationIdentifier, 1) - } - - self?.hasCompletedMigrations = true - - // Output any events tracked during the migration and trigger any `postCommitActions` which - // should occur - if let events: [ObservedEvent] = MigrationExecution.current?.observedEvents { - dependencies.notifyAsync(events: events) - } - - if let actions: [String: () -> Void] = MigrationExecution.current?.postCommitActions { - actions.values.forEach { $0() } - } - - // Don't log anything in the case of a 'success' or if the database is suspended (the - // latter will happen if the user happens to return to the background too quickly on - // launch so is unnecessarily alarming, it also gets caught and logged separately by - // the 'write' functions anyway) - switch result { - case .success: break - case .failure(DatabaseError.SQLITE_ABORT): break - case .failure(let error): - let completedMigrations: [String] = (try? dbWriter - .read { db in try migrator.completedMigrations(db) }) - .defaulting(to: []) - let failedMigrationName: String = migrator.migrations - .filter { !completedMigrations.contains($0) } - .first - .defaulting(to: "Unknown") - Log.critical(.migration, "Migration '\(failedMigrationName)' failed with error: \(error)") - } - - onComplete(result) - } // if there aren't any migrations to run then just complete immediately (this way the migrator // doesn't try to execute on the DBWrite thread so returning from the background can't get blocked // due to some weird endless process running) - guard !unperformedMigrations.isEmpty else { - migrationCompleted(.success(())) - return - } + guard !unperformedMigrations.isEmpty else { return } // Create the `MigrationContext` let migrationContext: MigrationExecution.Context = MigrationExecution.Context(progressUpdater: progressUpdater) @@ -364,22 +319,58 @@ open class Storage { migrationContext.progressUpdater(firstMigrationIdentifier, 0) } - MigrationExecution.$current.withValue(migrationContext) { - // Note: The non-async migration should only be used for unit tests - guard async else { return migrationCompleted(Result(catching: { try migrator.migrate(dbWriter) })) } - - migrator.asyncMigrate(dbWriter) { [dependencies] result in - let finalResult: Result = { + return try await withCheckedThrowingContinuation { [weak self, migrator, dependencies] continuation in + MigrationExecution.$current.withValue(migrationContext) { [weak self, migrator, dependencies] in + migrator.asyncMigrate(dbWriter) { [weak self, migrator, dependencies] result in + let finalResult: Result = { + switch result { + case .failure(let error): return .failure(error) + case .success: return .success(()) + } + }() + + // Make sure to transition the progress updater to 100% for the final migration (just + // in case the migration itself didn't update to 100% itself) + if let lastMigrationIdentifier: String = unperformedMigrations.last?.identifier { + MigrationExecution.current?.progressUpdater(lastMigrationIdentifier, 1) + } + + self?.hasCompletedMigrations = true + + // Output any events tracked during the migration and trigger any `postCommitActions` which + // should occur + if let events: [ObservedEvent] = MigrationExecution.current?.observedEvents { + dependencies.notifyAsync(events: events) + } + + if let actions: [String: () -> Void] = MigrationExecution.current?.postCommitActions { + actions.values.forEach { $0() } + } + + /// Don't log anything in the case of a `success` or if the database is suspended (the latter will happen if the + /// user happens to return to the background too quickly on launch so is unnecessarily alarming, it also gets + /// caught and logged separately by the `write` functions anyway) switch result { - case .failure(let error): return .failure(error) - case .success: return .success(()) + case .success: break + case .failure(DatabaseError.SQLITE_ABORT): break + case .failure(let error): + Task { [migrator] in + let completedMigrations: [String] = (try? await dbWriter + .read { [migrator] db in try migrator.completedMigrations(db) }) + .defaulting(to: []) + let failedMigrationName: String = migrator.migrations + .filter { !completedMigrations.contains($0) } + .first + .defaulting(to: "Unknown") + Log.critical(.migration, "Migration '\(failedMigrationName)' failed with error: \(error)") + } + } + + /// Resume the continuation + switch result { + case .failure(let error): continuation.resume(throwing: error) + case .success: continuation.resume() } - }() - - // Note: We need to dispatch this to the next run toop to prevent blocking if the callback - // performs subsequent database operations - DispatchQueue.global(qos: .userInitiated).async(using: dependencies) { - migrationCompleted(finalResult) } } } diff --git a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift index 4419366ba0..67560796fb 100644 --- a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift +++ b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift @@ -125,11 +125,11 @@ public class Dependencies { // MARK: - Instance management - public func warmSingleton(singleton: SingletonConfig) { + public func warm(singleton: SingletonConfig) { _ = getOrCreate(singleton) } - public func warmCache(cache: CacheConfig) { + public func warm(cache: CacheConfig) { _ = getOrCreate(cache) } @@ -142,6 +142,10 @@ public class Dependencies { setValue(value, typedStorage: .cache(value), key: cache.identifier) } + public func remove(singleton: SingletonConfig) { + removeValue(singleton.identifier, of: .singleton) + } + public func remove(cache: CacheConfig) { removeValue(cache.identifier, of: .cache) } diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index e3549638e3..a8c5afa608 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -904,6 +904,8 @@ public final class JobRunner: JobRunnerType { } public func scheduleRecurringJobsIfNeeded() { + guard dependencies[singleton: .appContext].isMainApp else { return } + let scheduleInfo: [ScheduleInfo] = registeredRecurringJobs let variants: Set = Set(scheduleInfo.map { $0.variant }) let maybeExistingJobs: [Job]? = dependencies[singleton: .storage].read { db in diff --git a/SessionUtilitiesKit/Types/CancellationAwareAsyncStream.swift b/SessionUtilitiesKit/Types/CancellationAwareAsyncStream.swift new file mode 100644 index 0000000000..42a56bace5 --- /dev/null +++ b/SessionUtilitiesKit/Types/CancellationAwareAsyncStream.swift @@ -0,0 +1,72 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +// MARK: - CancellationAwareAsyncStream + +public actor CancellationAwareAsyncStream: CancellationAwareStreamType { + private let lifecycleManager: StreamLifecycleManager = StreamLifecycleManager() + + // MARK: - Initialization + + public init() {} + + // MARK: - Functions + + public func send(_ newValue: Element) async { + lifecycleManager.send(newValue) + } + + public func finishCurrentStreams() async { + lifecycleManager.finishCurrentStreams() + } + + public func beforeYield(to continuation: AsyncStream.Continuation) async { + // No-op - no initial value + } + + public func makeTrackedStream() -> AsyncStream { + lifecycleManager.makeTrackedStream().stream + } +} + +// MARK: - CancellationAwareStreamType + +public protocol CancellationAwareStreamType: Actor { + associatedtype Element: Sendable + + func send(_ newValue: Element) async + func finishCurrentStreams() async + + /// This function gets called when a stream is initially created but before the inner stream is created, it shouldn't be called directly + func beforeYield(to continuation: AsyncStream.Continuation) async + + /// This is an internal function which shouldn't be called directly + func makeTrackedStream() async -> AsyncStream +} + +public extension CancellationAwareStreamType { + /// Every time `stream` is accessed it will create a **new** stream + /// + /// **Note:** This is non-isolated so it can be exposed via protocols without `async`, this is safe because `AsyncStream` is + /// thread-safe internally and `Element` is `Sendable` so it's verified to be safe to send concurrently + nonisolated var stream: AsyncStream { + AsyncStream { continuation in + let bridgingTask = Task { + await self.beforeYield(to: continuation) + + let internalStream: AsyncStream = await self.makeTrackedStream() + + for await element in internalStream { + continuation.yield(element) + } + + continuation.finish() + } + + continuation.onTermination = { @Sendable _ in + bridgingTask.cancel() + } + } + } +} diff --git a/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift b/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift index cd4e8b7a10..b2f9f13e1a 100644 --- a/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift +++ b/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift @@ -2,24 +2,12 @@ import Foundation -public actor CurrentValueAsyncStream { +public actor CurrentValueAsyncStream: CancellationAwareStreamType { private let lifecycleManager: StreamLifecycleManager = StreamLifecycleManager() /// This is the most recently emitted value public private(set) var currentValue: Element - /// Every time `stream` is accessed it will create a **new** stream - /// - /// **Note:** This is non-isolated so it can be exposed via protocols without `async`, this is safe because `AsyncStream` is - /// thread-safe internally and `Element` is `Sendable` so it's verified to be safe to send concurrently - nonisolated public var stream: AsyncStream { - AsyncStream { continuation in - Task { - await self.add(continuation: continuation) - } - } - } - // MARK: - Initialization public init(_ initialValue: Element) { @@ -28,25 +16,20 @@ public actor CurrentValueAsyncStream { // MARK: - Functions - public func send(_ newValue: Element) { + public func send(_ newValue: Element) async { currentValue = newValue lifecycleManager.send(newValue) } - public func finish() { - lifecycleManager.finish() + public func finishCurrentStreams() async { + lifecycleManager.finishCurrentStreams() } - // MARK: - Internal Functions - - private func add(continuation: AsyncStream.Continuation) { - let id: UUID = lifecycleManager.track(continuation) - - continuation.onTermination = { @Sendable [lifecycleManager] _ in - lifecycleManager.untrack(id: id) - } - - /// Since we've added a new subscriber we need to yield the current value to them + public func beforeYield(to continuation: AsyncStream.Continuation) async { continuation.yield(currentValue) } + + public func makeTrackedStream() -> AsyncStream { + lifecycleManager.makeTrackedStream().stream + } } diff --git a/SessionUtilitiesKit/Types/StreamLifecycleManager.swift b/SessionUtilitiesKit/Types/StreamLifecycleManager.swift index 4a9f426e51..444e431525 100644 --- a/SessionUtilitiesKit/Types/StreamLifecycleManager.swift +++ b/SessionUtilitiesKit/Types/StreamLifecycleManager.swift @@ -2,7 +2,7 @@ import Foundation -final class StreamLifecycleManager: @unchecked Sendable { +public final class StreamLifecycleManager: @unchecked Sendable { private let lock: NSLock = NSLock() private var continuations: [UUID: AsyncStream.Continuation] = [:] @@ -11,21 +11,22 @@ final class StreamLifecycleManager: @unchecked Sendable { public init() {} deinit { - finish() + finishCurrentStreams() } // MARK: - Functions - func track(_ continuation: AsyncStream.Continuation) -> UUID { + func makeTrackedStream() -> (stream: AsyncStream, id: UUID) { + let (stream, continuation) = AsyncStream.makeStream(of: Element.self) let id: UUID = UUID() lock.withLock { continuations[id] = continuation } + + continuation.onTermination = { @Sendable [self] _ in + self.finishStream(id: id) + } - return id - } - - func untrack(id: UUID) { - _ = lock.withLock { continuations.removeValue(forKey: id) } + return (stream, id) } func send(_ value: Element) { @@ -38,7 +39,15 @@ final class StreamLifecycleManager: @unchecked Sendable { } } - func finish() { + func finishStream(id: UUID) { + lock.withLock { + if let continuation: AsyncStream.Continuation = continuations.removeValue(forKey: id) { + continuation.finish() + } + } + } + + func finishCurrentStreams() { let currentContinuations: [UUID: AsyncStream.Continuation] = lock.withLock { let continuationsToFinish: [UUID: AsyncStream.Continuation] = continuations continuations.removeAll() diff --git a/SignalUtilitiesKit/Utilities/AppSetup.swift b/SignalUtilitiesKit/Utilities/AppSetup.swift deleted file mode 100644 index 1a3f2f7558..0000000000 --- a/SignalUtilitiesKit/Utilities/AppSetup.swift +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import GRDB -import SessionUIKit -import SessionNetworkingKit -import SessionMessagingKit -import SessionUtilitiesKit - -public enum AppSetup { - public static func setupEnvironment( - requestId: String? = nil, - appSpecificBlock: (() -> ())? = nil, - migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, - migrationsCompletion: @escaping (Result) -> (), - using dependencies: Dependencies - ) { - var backgroundTask: SessionBackgroundTask? = SessionBackgroundTask(label: #function, using: dependencies) - - DispatchQueue.global(qos: .userInitiated).async { - // Order matters here. - // - // All of these "singletons" should have any dependencies used in their - // initializers injected. - dependencies[singleton: .backgroundTaskManager].startObservingNotifications() - - // Attachments can be stored to NSTemporaryDirectory() - // If you receive a media message while the device is locked, the download will fail if - // the temporary directory is NSFileProtectionComplete - try? dependencies[singleton: .fileManager].protectFileOrFolder( - at: NSTemporaryDirectory(), - fileProtectionType: .completeUntilFirstUserAuthentication - ) - - SessionEnvironment.shared = SessionEnvironment( - audioSession: OWSAudioSession(), - proximityMonitoringManager: OWSProximityMonitoringManagerImpl(using: dependencies), - windowManager: OWSWindowManager(default: ()) - ) - appSpecificBlock?() - - runPostSetupMigrations( - requestId: requestId, - backgroundTask: backgroundTask, - migrationProgressChanged: migrationProgressChanged, - migrationsCompletion: migrationsCompletion, - using: dependencies - ) - - // The 'if' is only there to prevent the "variable never read" warning from showing - if backgroundTask != nil { backgroundTask = nil } - } - } - - public static func runPostSetupMigrations( - requestId: String? = nil, - backgroundTask: SessionBackgroundTask? = nil, - migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, - migrationsCompletion: @escaping (Result) -> (), - using dependencies: Dependencies - ) { - var backgroundTask: SessionBackgroundTask? = (backgroundTask ?? SessionBackgroundTask(label: #function, using: dependencies)) - - dependencies[singleton: .storage].perform( - migrations: SNMessagingKit.migrations, - onProgressUpdate: migrationProgressChanged, - onComplete: { originalResult in - // Now that the migrations are complete there are a few more states which need - // to be setup - typealias UserInfo = ( - sessionId: SessionId, - ed25519SecretKey: [UInt8], - dumpSessionIds: Set, - unreadCount: Int? - ) - dependencies[singleton: .storage].readAsync( - retrieve: { db -> UserInfo? in - guard let ed25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else { - return nil - } - - /// Cache the users session id so we don't need to fetch it from the database every time - dependencies.mutate(cache: .general) { - $0.setSecretKey(ed25519SecretKey: ed25519KeyPair.secretKey) - } - - /// Load the `libSession` state into memory - let userSessionId: SessionId = dependencies[cache: .general].sessionId - let cache: LibSession.Cache = LibSession.Cache( - userSessionId: userSessionId, - using: dependencies - ) - cache.loadState(db, requestId: requestId) - dependencies.set(cache: .libSession, to: cache) - - return ( - userSessionId, - ed25519KeyPair.secretKey, - cache.allDumpSessionIds, - try? Interaction.fetchAppBadgeUnreadCount(db, using: dependencies) - ) - }, - completion: { result in - switch result { - case .failure, .success(.none): break - case .success(.some(let userInfo)): - /// Save the `UserMetadata` and replicate `ConfigDump` data if needed - try? dependencies[singleton: .extensionHelper].saveUserMetadata( - sessionId: userInfo.sessionId, - ed25519SecretKey: userInfo.ed25519SecretKey, - unreadCount: userInfo.unreadCount - ) - - Task.detached(priority: .medium) { - dependencies[singleton: .extensionHelper].replicateAllConfigDumpsIfNeeded( - userSessionId: userInfo.sessionId, - allDumpSessionIds: userInfo.dumpSessionIds - ) - } - } - - /// Ensure any recurring jobs are properly scheduled - dependencies[singleton: .jobRunner].scheduleRecurringJobsIfNeeded() - - /// Callback that the migrations have completed - migrationsCompletion(originalResult) - - /// The 'if' is only there to prevent the "variable never read" warning from showing - if backgroundTask != nil { backgroundTask = nil } - } - ) - } - ) - } -} From baee6a341d6da1d7b509db3e4f080b1e256c690a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 10:09:15 +1000 Subject: [PATCH 13/59] Updated the SessionNetworkScreen to use async/await and ObservationManager --- .../SessionNetworkScreen+ViewModel.swift | 222 +++++++++++++----- .../SessionNetworkAPI+Network.swift | 86 ++++--- .../SessionNetworkAPI/SessionNetworkAPI.swift | 4 - 3 files changed, 212 insertions(+), 100 deletions(-) diff --git a/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift b/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift index d7795ea74f..7b2b033f57 100644 --- a/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift +++ b/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift @@ -11,84 +11,190 @@ import SessionMessagingKit extension SessionNetworkScreenContent { public class ViewModel: ObservableObject, ViewModelType { - @Published public var dataModel: DataModel + @Published public var state: State @Published public var isRefreshing: Bool = false @Published public var lastRefreshWasSuccessful: Bool = false @Published public var errorString: String? = nil @Published public var lastUpdatedTimeString: String? = nil - private var observationCancellable: AnyCancellable? - private var dependencies: Dependencies - - private var disposables = Set() + private let dependencies: Dependencies + private var observationTask: Task? + private var getInfoTask: Task? private var timer: Timer? = nil + public struct ObservableState: ObservableKeyProvider { + public let state: State + + public var observedKeys: Set = [ + .keyValue(.contractAddress), + .keyValue(.tokenUsd), + .keyValue(.priceTimestampMs), + .keyValue(.stakingRequirement), + .keyValue(.networkSize), + .keyValue(.networkStakedUSD), + .keyValue(.stakingRewardPool), + .keyValue(.marketCapUsd), + .keyValue(.lastUpdatedTimestampMs), + .conversationCreated, + .anyConversationDeleted + ] + } + + init(dependencies: Dependencies) { self.dependencies = dependencies - self.dataModel = DataModel() + self.state = State( + snodesInCurrentSwarm: 0, + snodesInTotal: 0, + contractAddress: nil, + tokenUSD: nil, + priceTimestampMs: 0, + stakingRequirement: 0, + networkSize: 0, + networkStakedTokens: 0, + networkStakedUSD: 0, + stakingRewardPool: nil, + marketCapUSD: nil, + totalTargetConversations: 0, + lastUpdatedTimestampMs: nil + ) - let userSessionId: SessionId = dependencies[cache: .general].sessionId - self.observationCancellable = ObservationBuilderOld - .databaseObservation(dependencies) { [dependencies] db in - let swarmNodesCount: Int = dependencies[cache: .libSessionNetwork].snodeNumber[userSessionId.hexString] ?? 0 - let snodeInTotal: Int = { - let pathsCount: Int = dependencies[cache: .libSessionNetwork].currentPaths.count - let validThreadVariants: [SessionThread.Variant] = [.contact, .group, .legacyGroup] - let convosInTotal: Int = ( - try? SessionThread - .filter(validThreadVariants.contains(SessionThread.Columns.variant)) - .fetchAll(db) - ) - .defaulting(to: []) - .count - let calculatedSnodeInTotal = swarmNodesCount + pathsCount * 3 + convosInTotal * 6 - if let networkSize = db[.networkSize] { - return min(networkSize, calculatedSnodeInTotal) - } - return calculatedSnodeInTotal - }() + self.observationTask = ObservationBuilder + .initialValue(ObservableState(state: state)) + .debounce(for: .milliseconds(250)) + .using(dependencies: dependencies) + .query(ViewModel.queryState) + .assign { [weak self] updatedState in + self?.state = updatedState.state + self?.updateLastUpdatedTimeString() + } + } + + deinit { + getInfoTask?.cancel() + observationTask?.cancel() + } + + @Sendable private static func queryState( + previousState: ObservableState, + events: [ObservedEvent], + isInitialQuery: Bool, + using dependencies: Dependencies + ) async -> ObservableState { + var contractAddress: String? = previousState.state.contractAddress + var tokenUSD: Double? = previousState.state.tokenUSD + var priceTimestampMs: Int64 = previousState.state.priceTimestampMs + var stakingRequirement: Double = previousState.state.stakingRequirement + var networkSize: Int = previousState.state.networkSize + var networkStakedTokens: Double = previousState.state.networkStakedTokens + var networkStakedUSD: Double = previousState.state.networkStakedUSD + var stakingRewardPool: Double? = previousState.state.stakingRewardPool + var marketCapUSD: Double? = previousState.state.marketCapUSD + var lastUpdatedTimestampMs: Int64? = previousState.state.lastUpdatedTimestampMs + var totalTargetConversations: Int = previousState.state.totalTargetConversations + let validThreadVariants: [SessionThread.Variant] = [.contact, .group, .legacyGroup] + + /// On the first query we want to load the state from the database + if isInitialQuery { + try? await dependencies[singleton: .storage].readAsync { db in + contractAddress = (db[.contractAddress] ?? contractAddress) + tokenUSD = (db[.tokenUsd] ?? tokenUSD) + priceTimestampMs = (db[.priceTimestampMs] ?? priceTimestampMs) + stakingRequirement = (db[.stakingRequirement] ?? stakingRequirement) + networkSize = (db[.networkSize] ?? networkSize) + networkStakedTokens = (db[.networkStakedTokens] ?? networkStakedTokens) + networkStakedUSD = (db[.networkStakedUSD] ?? networkStakedUSD) + stakingRewardPool = (db[.stakingRewardPool] ?? stakingRewardPool) + marketCapUSD = (db[.marketCapUsd] ?? marketCapUSD) + lastUpdatedTimestampMs = (db[.lastUpdatedTimestampMs] ?? lastUpdatedTimestampMs) - return DataModel( - snodesInCurrentSwarm: swarmNodesCount, - snodesInTotal: snodeInTotal, - contractAddress: db[.contractAddress], - tokenUSD: db[.tokenUsd], - priceTimestampMs: db[.priceTimestampMs] ?? 0, - stakingRequirement: db[.stakingRequirement] ?? 0, - networkSize: db[.networkSize] ?? 0, - networkStakedTokens: db[.networkStakedTokens] ?? 0, - networkStakedUSD: db[.networkStakedUSD] ?? 0, - stakingRewardPool: db[.stakingRewardPool], - marketCapUSD: db[.marketCapUsd], - lastUpdatedTimestampMs: db[.lastUpdatedTimestampMs] - ) + totalTargetConversations = (try? SessionThread + .filter(validThreadVariants.contains(SessionThread.Columns.variant)) + .fetchCount(db)) + .defaulting(to: 0) } - .sink( - receiveCompletion: { _ in /* ignore error */ }, - receiveValue: { [weak self] dataModel in - self?.dataModel = dataModel - self?.updateLastUpdatedTimeString() - } + } + + /// Re-fetch the total conversation count if needed + if events.contains(where: { $0.key == .conversationCreated || $0.key == .anyConversationDeleted }) { + try? await dependencies[singleton: .storage].readAsync { db in + totalTargetConversations = (try? SessionThread + .filter(validThreadVariants.contains(SessionThread.Columns.variant)) + .fetchCount(db)) + .defaulting(to: 0) + } + } + + /// Extract data changes from events + events.forEach { event in + switch (event.key, event.value) { + case (.keyValue(.contractAddress), let value as String): contractAddress = value + case (.keyValue(.tokenUsd), let value as Double): tokenUSD = value + case (.keyValue(.priceTimestampMs), let value as Int64): priceTimestampMs = value + case (.keyValue(.stakingRequirement), let value as Double): stakingRequirement = value + case (.keyValue(.networkSize), let value as Int): networkSize = value + case (.keyValue(.networkStakedTokens), let value as Double): networkStakedTokens = value + case (.keyValue(.networkStakedUSD), let value as Double): networkStakedUSD = value + case (.keyValue(.networkStakedTokens), let value as Double): networkStakedTokens = value + case (.keyValue(.stakingRewardPool), let value as Double?): stakingRewardPool = value + case (.keyValue(.marketCapUsd), let value as Double?): marketCapUSD = value + case (.keyValue(.lastUpdatedTimestampMs), let value as Int64?): lastUpdatedTimestampMs = value + case (.conversationCreated, _), (.anyConversationDeleted, _): break + default: + Log.warn("[SessionNetworkScreen] Received update event with unknown key: \(event.key)") + break + } + } + + /// Retrieve the latest state from the network + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let snodesInCurrentSwarm: Int = ((try? await dependencies[singleton: .network] + .getSwarm(for: userSessionId.hexString) + .count) ?? 0) + let pathsCount: Int = ((try? await dependencies[singleton: .network].getActivePaths().count) ?? 0) + let calculatedSnodeInTotal: Int = (snodesInCurrentSwarm + pathsCount * 3 + totalTargetConversations * 6) + let snodesInTotal: Int = min(networkSize, calculatedSnodeInTotal) + + return ObservableState( + state: State( + snodesInCurrentSwarm: snodesInCurrentSwarm, + snodesInTotal: snodesInTotal, + contractAddress: contractAddress, + tokenUSD: tokenUSD, + priceTimestampMs: priceTimestampMs, + stakingRequirement: stakingRequirement, + networkSize: networkSize, + networkStakedTokens: networkStakedTokens, + networkStakedUSD: networkStakedUSD, + stakingRewardPool: stakingRewardPool, + marketCapUSD: marketCapUSD, + totalTargetConversations: totalTargetConversations, + lastUpdatedTimestampMs: lastUpdatedTimestampMs ) + ) } public func fetchDataFromNetwork() { guard !self.isRefreshing else { return } + self.isRefreshing.toggle() self.lastRefreshWasSuccessful = false - SessionNetworkAPI.client.getInfo(using: dependencies) - .subscribe(on: SessionNetworkAPI.workQueue, using: dependencies) - .receive(on: DispatchQueue.main) - .sink( - receiveCompletion: { _ in }, - receiveValue: { [weak self] didRefreshSuccessfully in - self?.lastRefreshWasSuccessful = didRefreshSuccessfully - self?.isRefreshing.toggle() + getInfoTask = Task { [weak self, client = dependencies[singleton: .sessionNetworkApiClient]] in + do { + _ = try await client.getInfo() + await MainActor.run { [weak self] in + self?.lastRefreshWasSuccessful = true } - ) - .store(in: &disposables) + } catch { + await MainActor.run { [weak self] in + self?.lastRefreshWasSuccessful = false + } + } + + self?.isRefreshing.toggle() + } } public func openURL(_ url: URL) { @@ -97,7 +203,7 @@ extension SessionNetworkScreenContent { private func updateLastUpdatedTimeString() { self.lastUpdatedTimeString = { - guard let lastUpdatedTimestampMs = dataModel.lastUpdatedTimestampMs else { return nil } + guard let lastUpdatedTimestampMs = state.lastUpdatedTimestampMs else { return nil } return String.formattedRelativeTime( lastUpdatedTimestampMs, minimumUnit: .minute @@ -106,7 +212,7 @@ extension SessionNetworkScreenContent { self.timer?.invalidate() self.timer = Timer.scheduledTimerOnMainThread(withTimeInterval: 60, repeats: true, using: dependencies) { [weak self] _ in self?.lastUpdatedTimeString = { - guard let lastUpdatedTimestampMs = self?.dataModel.lastUpdatedTimestampMs else { return nil } + guard let lastUpdatedTimestampMs = self?.state.lastUpdatedTimestampMs else { return nil } return String.formattedRelativeTime( lastUpdatedTimestampMs, minimumUnit: .minute diff --git a/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift index 79feb74a36..b5117fad27 100644 --- a/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift +++ b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift @@ -7,48 +7,60 @@ import Combine import GRDB import SessionUtilitiesKit +// MARK: - Singleton + +public extension Singleton { + static let sessionNetworkApiClient: SingletonConfig = Dependencies.create( + identifier: "sessionNetworkApiClient", + createInstance: { dependencies in SessionNetworkAPI.HTTPClient(using: dependencies) } + ) +} + // MARK: - Log.Category public extension Log.Category { static let sessionNetwork: Log.Category = .create("SessionNetwork", defaultLevel: .info) } +// MARK: - SessionNetworkAPI.HTTPClient + extension SessionNetworkAPI { - public final class HTTPClient { - private var cancellable: AnyCancellable? - private var dependencies: Dependencies? + public actor HTTPClient { + private var getInfoTask: Task? + private var dependencies: Dependencies - public func initialize(using dependencies: Dependencies) { + public init(using dependencies: Dependencies) { self.dependencies = dependencies - cancellable = getInfo(using: dependencies) - .subscribe(on: SessionNetworkAPI.workQueue, using: dependencies) - .receive(on: SessionNetworkAPI.workQueue) - .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) } - public func getInfo(using dependencies: Dependencies) -> AnyPublisher { - cancellable?.cancel() + public func fetchInfoInBackground() { + getInfoTask = Task { + _ = try? await getInfo() + } + } + + public func getInfo() async throws -> Bool { + getInfoTask?.cancel() + + let staleTimestampMs: Int64 = (try? await dependencies[singleton: .storage] + .readAsync { db in db[.staleTimestampMs] }) + .defaulting(to: 0) - let staleTimestampMs: Int64 = dependencies[singleton: .storage].read { db in db[.staleTimestampMs] }.defaulting(to: 0) guard staleTimestampMs < dependencies[cache: .snodeAPI].currentOffsetTimestampMs() else { - return Just(()) - .delay(for: .milliseconds(500), scheduler: SessionNetworkAPI.workQueue) - .setFailureType(to: Error.self) - .flatMapStorageWritePublisher(using: dependencies) { [dependencies] db, info -> Bool in - db[.lastUpdatedTimestampMs] = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() - return true - } - .eraseToAnyPublisher() + try? await Task.sleep(for: .milliseconds(500)) + try await dependencies[singleton: .storage].writeAsync { [dependencies] db in + db[.lastUpdatedTimestampMs] = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + } + + return true } - return Result { - try SessionNetworkAPI + do { + let info: SessionNetworkAPI.Info = try await SessionNetworkAPI .prepareInfo(using: dependencies) - } - .publisher - .flatMap { [dependencies] in $0.send(using: dependencies) } - .map { _, info in info } - .flatMapStorageWritePublisher(using: dependencies) { [dependencies] db, info -> Bool in + .send(using: dependencies) + + try await dependencies[singleton: .storage].writeAsync { [dependencies] db in // Token info db[.lastUpdatedTimestampMs] = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() db[.tokenUsd] = info.price?.tokenUsd @@ -70,21 +82,19 @@ extension SessionNetworkAPI { db[.networkSize] = info.network?.networkSize db[.networkStakedTokens] = info.network?.networkStakedTokens db[.networkStakedUSD] = info.network?.networkStakedUSD - - return true } - .catch { error -> AnyPublisher in - Log.error(.sessionNetwork, "Failed to fetch token info due to error: \(error).") - return self.cleanUpSessionNetworkPageData(using: dependencies) - .map { _ in false } - .eraseToAnyPublisher() - - } - .eraseToAnyPublisher() + + return true + } + catch { + Log.error(.sessionNetwork, "Failed to fetch token info due to error: \(error).") + try? await cleanUpSessionNetworkPageData() + return false + } } - private func cleanUpSessionNetworkPageData(using dependencies: Dependencies) -> AnyPublisher { - dependencies[singleton: .storage].writePublisher { db in + private func cleanUpSessionNetworkPageData() async throws { + try await dependencies[singleton: .storage].writeAsync { db in // Token info db[.lastUpdatedTimestampMs] = nil db[.tokenUsd] = nil diff --git a/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift index a6b68ac989..d922740dcd 100644 --- a/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift +++ b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift @@ -7,15 +7,11 @@ import Combine import SessionUtilitiesKit public enum SessionNetworkAPI { - public static let workQueue = DispatchQueue(label: "SessionNetworkAPI.workQueue", qos: .userInitiated) - public static let client = HTTPClient() - // MARK: - Info /// General token info. This endpoint combines the `/price` and `/token` endpoint information. /// /// `GET/info` - public static func prepareInfo( using dependencies: Dependencies ) throws -> Network.PreparedRequest { From ecc472b69069b9ee8b05f5cb1658b41ade9f27bf Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 10:10:08 +1000 Subject: [PATCH 14/59] Updated path status observations to use async/await --- Session/Path/PathStatusView.swift | 15 +++++------ Session/Path/PathVC.swift | 26 ++++++++----------- .../Utilities/Threading+SMK.swift | 2 -- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/Session/Path/PathStatusView.swift b/Session/Path/PathStatusView.swift index 8fe4d81ce5..64b6aa0714 100644 --- a/Session/Path/PathStatusView.swift +++ b/Session/Path/PathStatusView.swift @@ -78,22 +78,21 @@ final class PathStatusView: UIView { private func startObservingNetwork() { statusObservationTask?.cancel() - statusObservationTask = Task.detached(priority: .background) { [weak self, dependencies] in + statusObservationTask = Task.detached(priority: .userInitiated) { [weak self, dependencies] in var specificNetworkObservationTask: Task? for await network in dependencies.stream(singleton: .network) { specificNetworkObservationTask?.cancel() specificNetworkObservationTask = Task { - do { - for await status in network.networkStatus { - try Task.checkCancellation() - - await self?.setStatus(to: status) - } + for await status in network.networkStatus { + await self?.setStatus(to: status) } - catch { Log.info("PathStatusView networkStatus observation ended, restarting.") } + + Log.info("PathStatusView networkStatus observation ended, restarting.") } } + + specificNetworkObservationTask?.cancel() } } diff --git a/Session/Path/PathVC.swift b/Session/Path/PathVC.swift index f82a7f0b20..7cd1ad3994 100644 --- a/Session/Path/PathVC.swift +++ b/Session/Path/PathVC.swift @@ -144,14 +144,11 @@ final class PathVC: BaseVC { for await network in dependencies.stream(singleton: .network) { specificNetworkObservationTask?.cancel() specificNetworkObservationTask = Task { - do { - for await _ in network.networkStatus { - try Task.checkCancellation() - - await self?.loadPathsAsync() - } + for await _ in network.networkStatus { + await self?.loadPathsAsync() } - catch { Log.info("PathStatusView networkStatus observation ended, restarting.") } + + Log.info("PathVC networkStatus observation ended, restarting.") } } } @@ -410,22 +407,21 @@ private final class LineView: UIView { private func startObservingNetwork() { statusObservationTask?.cancel() - statusObservationTask = Task { [weak self, dependencies] in + statusObservationTask = Task.detached(priority: .userInitiated) { [weak self, dependencies] in var specificNetworkObservationTask: Task? for await network in dependencies.stream(singleton: .network) { specificNetworkObservationTask?.cancel() specificNetworkObservationTask = Task { - do { - for await status in network.networkStatus { - try Task.checkCancellation() - - self?.setStatus(to: status) - } + for await status in network.networkStatus { + await self?.setStatus(to: status) } - catch { Log.info("PathStatusView networkStatus observation ended, restarting.") } + + Log.info("LineView networkStatus observation ended, restarting.") } } + + specificNetworkObservationTask?.cancel() } } diff --git a/SessionMessagingKit/Utilities/Threading+SMK.swift b/SessionMessagingKit/Utilities/Threading+SMK.swift index 56f0f67cbd..41c26e5ee8 100644 --- a/SessionMessagingKit/Utilities/Threading+SMK.swift +++ b/SessionMessagingKit/Utilities/Threading+SMK.swift @@ -3,6 +3,4 @@ import SessionUtilitiesKit public extension Threading { static let pollerQueue = DispatchQueue(label: "SessionMessagingKit.pollerQueue") - static let groupPollerQueue = DispatchQueue(label: "SessionMessagingKit.groupPollerQueue") - static let communityPollerQueue = DispatchQueue(label: "SessionMessagingKit.communityPollerQueue") } From ae9e35b82ff50c6494ddf5ef928307070eb61d22 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 10:10:50 +1000 Subject: [PATCH 15/59] Added some missing SessionNetworkScreen changes --- .../SessionNetworkScreen+Models.swift | 66 +++++++++++-------- .../SessionNetworkScreen.swift | 53 +++++++-------- 2 files changed, 66 insertions(+), 53 deletions(-) diff --git a/SessionUIKit/Screens/Settings/SessionNetworkScreen/SessionNetworkScreen+Models.swift b/SessionUIKit/Screens/Settings/SessionNetworkScreen/SessionNetworkScreen+Models.swift index c37eea2674..e1c55320c3 100644 --- a/SessionUIKit/Screens/Settings/SessionNetworkScreen/SessionNetworkScreen+Models.swift +++ b/SessionUIKit/Screens/Settings/SessionNetworkScreen/SessionNetworkScreen+Models.swift @@ -5,8 +5,10 @@ import Foundation public enum SessionNetworkScreenContent {} public extension SessionNetworkScreenContent { + static let defaultPriceString: String = "$-USD" // stringlint:disabled + protocol ViewModelType: ObservableObject { - var dataModel: DataModel { get set } + var state: State { get set } var isRefreshing: Bool { get set } var lastRefreshWasSuccessful: Bool { get set } var errorString: String? { get set } @@ -16,9 +18,7 @@ public extension SessionNetworkScreenContent { func openURL(_ url: URL) } - final class DataModel: Equatable { - public static let defaultPriceString: String = "$-USD" // stringlint:disabled - + final class State: Sendable, Equatable { // Snode Data public let snodesInCurrentSwarm: Int public let snodesInTotal: Int @@ -69,15 +69,13 @@ public extension SessionNetworkScreenContent { } public let networkStakedUSD: Double public var networkStakedUSDString: String { - guard networkStakedUSD > 0 else { - return DataModel.defaultPriceString - } + guard networkStakedUSD > 0 else { return defaultPriceString } + return "$\(networkStakedUSD.formatted(format: .currency(decimal: false))) USD" } public var networkStakedUSDAbbreviatedString: String { - guard networkStakedUSD > 0 else { - return DataModel.defaultPriceString - } + guard networkStakedUSD > 0 else { return defaultPriceString } + return "$\(networkStakedUSD.formatted(format: .abbreviatedCurrency(decimalPlaces: 1))) USD" } public let stakingRewardPool: Double? @@ -100,23 +98,27 @@ public extension SessionNetworkScreenContent { } return "$\(marketCap.formatted(format: .abbreviatedCurrency(decimalPlaces: 1))) USD" } + public let totalTargetConversations: Int // Last update time public let lastUpdatedTimestampMs: Int64? + // MARK: - Initialization + public init( - snodesInCurrentSwarm: Int = 0, - snodesInTotal: Int = 0, - contractAddress: String? = nil, - tokenUSD: Double? = nil, - priceTimestampMs: Int64 = 0, - stakingRequirement: Double = 0, - networkSize: Int = 0, - networkStakedTokens: Double = 0, - networkStakedUSD: Double = 0, - stakingRewardPool: Double? = nil, - marketCapUSD: Double? = nil, - lastUpdatedTimestampMs: Int64? = nil + snodesInCurrentSwarm: Int, + snodesInTotal: Int, + contractAddress: String?, + tokenUSD: Double?, + priceTimestampMs: Int64, + stakingRequirement: Double, + networkSize: Int, + networkStakedTokens: Double, + networkStakedUSD: Double, + stakingRewardPool: Double?, + marketCapUSD: Double?, + totalTargetConversations: Int, + lastUpdatedTimestampMs: Int64? ) { self.snodesInCurrentSwarm = snodesInCurrentSwarm self.snodesInTotal = snodesInTotal @@ -129,10 +131,11 @@ public extension SessionNetworkScreenContent { self.networkStakedUSD = networkStakedUSD self.stakingRewardPool = stakingRewardPool self.marketCapUSD = marketCapUSD + self.totalTargetConversations = totalTargetConversations self.lastUpdatedTimestampMs = lastUpdatedTimestampMs } - public static func == (lhs: DataModel, rhs: DataModel) -> Bool { + public static func == (lhs: State, rhs: State) -> Bool { let isSnodeInfoEqual: Bool = ( lhs.snodesInCurrentSwarm == rhs.snodesInCurrentSwarm && lhs.snodesInTotal == rhs.snodesInTotal @@ -153,16 +156,23 @@ public extension SessionNetworkScreenContent { lhs.marketCapUSD == rhs.marketCapUSD ) + let convoCountEqual: Bool = lhs.totalTargetConversations == rhs.totalTargetConversations let isUpdateTimeEqual: Bool = lhs.lastUpdatedTimestampMs == rhs.lastUpdatedTimestampMs - return isSnodeInfoEqual && isTokenInfoDataEqual && isNetworkInfoDataEqual && isUpdateTimeEqual + return ( + isSnodeInfoEqual && + isTokenInfoDataEqual && + isNetworkInfoDataEqual && + convoCountEqual && + isUpdateTimeEqual + ) } } } // MARK: - Convenience -extension SessionNetworkScreenContent.DataModel { +extension SessionNetworkScreenContent.State { public func with( snodesInCurrentSwarm: Int? = nil, snodesInTotal: Int? = nil, @@ -175,9 +185,10 @@ extension SessionNetworkScreenContent.DataModel { networkStakedUSD: Double? = nil, stakingRewardPool: Double? = nil, marketCapUSD: Double? = nil, + totalTargetConversations: Int? = nil, lastUpdatedTimestampMs: Int64? = nil - ) -> SessionNetworkScreenContent.DataModel { - return SessionNetworkScreenContent.DataModel( + ) -> SessionNetworkScreenContent.State { + return SessionNetworkScreenContent.State( snodesInCurrentSwarm: snodesInCurrentSwarm ?? self.snodesInCurrentSwarm, snodesInTotal: snodesInTotal ?? self.snodesInTotal, contractAddress: contractAddress ?? self.contractAddress, @@ -189,6 +200,7 @@ extension SessionNetworkScreenContent.DataModel { networkStakedUSD: networkStakedUSD ?? self.networkStakedUSD, stakingRewardPool: stakingRewardPool ?? self.stakingRewardPool, marketCapUSD: marketCapUSD ?? self.marketCapUSD, + totalTargetConversations: totalTargetConversations ?? self.totalTargetConversations, lastUpdatedTimestampMs: lastUpdatedTimestampMs ?? self.lastUpdatedTimestampMs ) } diff --git a/SessionUIKit/Screens/Settings/SessionNetworkScreen/SessionNetworkScreen.swift b/SessionUIKit/Screens/Settings/SessionNetworkScreen/SessionNetworkScreen.swift index fee96e74f8..46382aa18c 100644 --- a/SessionUIKit/Screens/Settings/SessionNetworkScreen/SessionNetworkScreen.swift +++ b/SessionUIKit/Screens/Settings/SessionNetworkScreen/SessionNetworkScreen.swift @@ -34,7 +34,7 @@ public struct SessionNetworkScreen 0 { - Image("connection_\(dataModel.snodesInCurrentSwarm)") + } else if state.snodesInCurrentSwarm > 0 { + Image("connection_\(state.snodesInCurrentSwarm)") .renderingMode(.template) .foregroundColor(themeColor: .textPrimary) - Image("snodes_\(dataModel.snodesInCurrentSwarm)") + Image("snodes_\(state.snodesInCurrentSwarm)") .renderingMode(.template) .foregroundColor(themeColor: .primary) .shadow(themeColor: .settings_glowingBackground, radius: 10) @@ -271,9 +271,9 @@ extension SessionNetworkScreen { AdaptiveText( textOptions: [ - dataModel.tokenUSDString, - dataModel.tokenUSDNoCentsString, - dataModel.tokenUSDAbbreviatedString + state.tokenUSDString, + state.tokenUSDNoCentsString, + state.tokenUSDAbbreviatedString ], isLoading: isRefreshing ) @@ -332,7 +332,7 @@ extension SessionNetworkScreen { if isRefreshing || !lastRefreshWasSuccessful { ProgressView() } else { - Text("\(dataModel.snodesInCurrentSwarm)") + Text("\(state.snodesInCurrentSwarm)") .font(.Headings.H3) .foregroundColor(themeColor: .sessionButton_text) .lineLimit(1) @@ -374,9 +374,9 @@ extension SessionNetworkScreen { AdaptiveText( textOptions: [ - dataModel.snodesInTotalString, - dataModel.snodesInTotalAbbreviatedString, - dataModel.snodesInTotalAbbreviatedNoDecimalString + state.snodesInTotalString, + state.snodesInTotalAbbreviatedString, + state.snodesInTotalAbbreviatedNoDecimalString ], isLoading: isRefreshing || !lastRefreshWasSuccessful ) @@ -411,7 +411,7 @@ extension SessionNetworkScreen { ) .minimumScaleFactor(0.5) - Text(isRefreshing ? "loading".localized() : dataModel.networkStakedTokensString) + Text(isRefreshing ? "loading".localized() : state.networkStakedTokensString) .font(.Headings.H5) .foregroundColor(themeColor: .sessionButton_text) .lineLimit(1) @@ -423,8 +423,8 @@ extension SessionNetworkScreen { AdaptiveText( textOptions: [ - dataModel.networkStakedUSDString, - dataModel.networkStakedUSDAbbreviatedString + state.networkStakedUSDString, + state.networkStakedUSDAbbreviatedString ], isLoading: isRefreshing ) @@ -433,7 +433,7 @@ extension SessionNetworkScreen { uiKit: Fonts.Body.custom(Values.smallFontSize) ) .foregroundColor(themeColor: .textSecondary) - .loadingStyle(.text(SessionNetworkScreenContent.DataModel.defaultPriceString)) + .loadingStyle(.text(SessionNetworkScreenContent.defaultPriceString)) .fixedSize() } .padding(.horizontal, Values.mediumSmallSpacing) @@ -457,7 +457,7 @@ extension SessionNetworkScreen { ZStack { Text( Constants.session_network_data_price - .put(key: "date_time", value: dataModel.priceTimeString) // stringlint:ignore + .put(key: "date_time", value: state.priceTimeString) // stringlint:ignore .localized() ) .font(.Body.smallRegular) @@ -496,7 +496,7 @@ extension SessionNetworkScreen { extension SessionNetworkScreen { struct SessionTokenSection: View { - @Binding var dataModel: SessionNetworkScreenContent.DataModel + @Binding var state: SessionNetworkScreenContent.State @Binding var isRefreshing: Bool var linkOutAction: () -> () @@ -549,7 +549,7 @@ extension SessionNetworkScreen { alignment: .leading, spacing: Values.veryLargeSpacing ) { - Text(isRefreshing ? "loading".localized() : dataModel.stakingRewardPoolString) + Text(isRefreshing ? "loading".localized() : state.stakingRewardPoolString) .font(.Body.largeRegular) .foregroundColor(themeColor: .textPrimary) .lineLimit(1) @@ -560,8 +560,8 @@ extension SessionNetworkScreen { AdaptiveText( textOptions: [ - dataModel.marketCapString, - dataModel.marketCapAbbreviatedString + state.marketCapString, + state.marketCapAbbreviatedString ], isLoading: isRefreshing ) @@ -613,20 +613,20 @@ extension SessionNetworkScreen { #if DEBUG extension SessionNetworkScreenContent { class PreviewViewModel: ViewModelType { - var dataModel: DataModel + var state: State var isRefreshing: Bool var lastRefreshWasSuccessful: Bool var errorString: String? var lastUpdatedTimeString: String? init( - dataModel: DataModel, + state: State, isRefreshing: Bool, lastRefreshWasSuccessful: Bool, errorString: String? = nil, lastUpdatedTimeString: String? = nil ) { - self.dataModel = dataModel + self.state = state self.isRefreshing = isRefreshing self.lastRefreshWasSuccessful = lastRefreshWasSuccessful self.errorString = errorString @@ -644,7 +644,7 @@ extension SessionNetworkScreenContent { #Preview { SessionNetworkScreen( viewModel: SessionNetworkScreenContent.PreviewViewModel( - dataModel: SessionNetworkScreenContent.DataModel( + state: SessionNetworkScreenContent.State( snodesInCurrentSwarm: 6, snodesInTotal: 2254, contractAddress: "0x7D7fD4E91834A96cD9Fb2369E7f4EB72383bbdEd", @@ -656,6 +656,7 @@ extension SessionNetworkScreenContent { networkStakedUSD: 34_278_323_684.940723, stakingRewardPool: 40_010_040, marketCapUSD: 216_442_438_046.91196, + totalTargetConversations: 6, lastUpdatedTimestampMs: 1745817920000 ), isRefreshing: false, From c3862237f6d3cd6c9918a75ebdef4f050e669f3c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 10:14:29 +1000 Subject: [PATCH 16/59] Refactored 'SwarmDrainBehaviour' into 'SwarmDrainer' --- SessionNetworkingKit/SnodeAPI/SnodeAPI.swift | 82 ---------- .../Types/SwarmDrainBehaviour.swift | 88 ----------- SessionNetworkingKit/Types/SwarmDrainer.swift | 141 ++++++++++++++++++ 3 files changed, 141 insertions(+), 170 deletions(-) delete mode 100644 SessionNetworkingKit/Types/SwarmDrainBehaviour.swift create mode 100644 SessionNetworkingKit/Types/SwarmDrainer.swift diff --git a/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift index 530d422ba0..f744253806 100644 --- a/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift +++ b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift @@ -698,88 +698,6 @@ public final class SnodeAPI { } } -// MARK: - Publisher Convenience - -public extension Publisher where Output == Set { - func tryMapWithRandomSnode( - using dependencies: Dependencies, - _ transform: @escaping (LibSession.Snode) throws -> T - ) -> AnyPublisher { - return self - .tryMap { swarm -> T in - var remainingSnodes: Set = swarm - let snode: LibSession.Snode = try dependencies.popRandomElement(&remainingSnodes) ?? { - throw SnodeAPIError.insufficientSnodes - }() - - return try transform(snode) - } - .eraseToAnyPublisher() - } - - func tryFlatMapWithRandomSnode( - maxPublishers: Subscribers.Demand = .unlimited, - retry retries: Int = 0, - drainBehaviour: ThreadSafeObject = .alwaysRandom, - using dependencies: Dependencies, - _ transform: @escaping (LibSession.Snode) throws -> P - ) -> AnyPublisher where T == P.Output, P: Publisher, P.Failure == Error { - return self - .mapError { $0 } - .flatMap(maxPublishers: maxPublishers) { swarm -> AnyPublisher in - // If we don't want to reuse a specific snode multiple times then just grab a - // random one from the swarm every time - var remainingSnodes: Set = drainBehaviour.performUpdateAndMap { behaviour in - switch behaviour { - case .alwaysRandom: return (behaviour, swarm) - case .limitedReuse(_, let targetSnode, _, let usedSnodes, let swarmHash): - // If we've used all of the snodes or the swarm has changed then reset the used list - guard swarmHash == swarm.hashValue && (targetSnode != nil || usedSnodes != swarm) else { - return (behaviour.reset(), swarm) - } - - return (behaviour, swarm.subtracting(usedSnodes)) - } - } - var lastError: Error? - - return Just(()) - .setFailureType(to: Error.self) - .tryFlatMap(maxPublishers: maxPublishers) { _ -> AnyPublisher in - let snode: LibSession.Snode = try drainBehaviour.performUpdateAndMap { behaviour in - switch behaviour { - case .limitedReuse(_, .some(let targetSnode), _, _, _): - return (behaviour.use(snode: targetSnode, from: swarm), targetSnode) - default: break - } - - // Select the next snode - let result: LibSession.Snode = try dependencies.popRandomElement(&remainingSnodes) ?? { - throw SnodeAPIError.ranOutOfRandomSnodes(lastError) - }() - - return (behaviour.use(snode: result, from: swarm), result) - } - - return try transform(snode) - .eraseToAnyPublisher() - } - .mapError { error in - // Prevent nesting the 'ranOutOfRandomSnodes' errors - switch error { - case SnodeAPIError.ranOutOfRandomSnodes: break - default: lastError = error - } - - return error - } - .retry(retries) - .eraseToAnyPublisher() - } - .eraseToAnyPublisher() - } -} - // MARK: - SnodeAPI Cache public extension SnodeAPI { diff --git a/SessionNetworkingKit/Types/SwarmDrainBehaviour.swift b/SessionNetworkingKit/Types/SwarmDrainBehaviour.swift deleted file mode 100644 index 89466fb0c9..0000000000 --- a/SessionNetworkingKit/Types/SwarmDrainBehaviour.swift +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import SessionUtilitiesKit - -public enum SwarmDrainBehaviour { - case alwaysRandom - case limitedReuse( - count: UInt, - targetSnode: LibSession.Snode?, - targetUseCount: Int, - usedSnodes: Set, - swarmHash: Int - ) - - public static func limitedReuse(count: UInt) -> SwarmDrainBehaviour { - guard count > 1 else { return .alwaysRandom } - - return .limitedReuse(count: count, targetSnode: nil, targetUseCount: 0, usedSnodes: [], swarmHash: 0) - } - - // MARK: - Convenience - - func use(snode: LibSession.Snode, from swarm: Set) -> SwarmDrainBehaviour { - switch self { - case .alwaysRandom: return .alwaysRandom - case .limitedReuse(let count, let targetSnode, let targetUseCount, let usedSnodes, _): - // If we are using a new snode then reset everything - guard targetSnode == snode else { - return .limitedReuse( - count: count, - targetSnode: snode, - targetUseCount: 1, - usedSnodes: usedSnodes.inserting(snode), - swarmHash: swarm.hashValue - ) - } - - // Increment the use count and clear the target if it's been used too many times - let updatedUseCount: Int = (targetUseCount + 1) - - return .limitedReuse( - count: count, - targetSnode: (updatedUseCount < count ? snode : nil), - targetUseCount: updatedUseCount, - usedSnodes: usedSnodes, - swarmHash: swarm.hashValue - ) - } - } - - public func clearTargetSnode() -> SwarmDrainBehaviour { - switch self { - case .alwaysRandom: return .alwaysRandom - case .limitedReuse(let count, _, _, let usedSnodes, let swarmHash): - return .limitedReuse( - count: count, - targetSnode: nil, - targetUseCount: 0, - usedSnodes: usedSnodes, - swarmHash: swarmHash - ) - } - } - - public func reset() -> SwarmDrainBehaviour { - switch self { - case .alwaysRandom: return .alwaysRandom - case .limitedReuse(let count, _, _, _, _): - return .limitedReuse( - count: count, - targetSnode: nil, - targetUseCount: 0, - usedSnodes: [], - swarmHash: 0 - ) - } - } -} - -// MARK: - Convenience - -public extension ThreadSafeObject where Value == SwarmDrainBehaviour { - static var alwaysRandom: ThreadSafeObject { ThreadSafeObject(.alwaysRandom) } - static func limitedReuse(count: UInt) -> ThreadSafeObject { - return ThreadSafeObject(.limitedReuse(count: count)) - } -} diff --git a/SessionNetworkingKit/Types/SwarmDrainer.swift b/SessionNetworkingKit/Types/SwarmDrainer.swift new file mode 100644 index 0000000000..1faca9d98c --- /dev/null +++ b/SessionNetworkingKit/Types/SwarmDrainer.swift @@ -0,0 +1,141 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +public actor SwarmDrainer { + public enum Strategy: Sendable { + /// Select a new random node for each retry attempt + case alwaysRandom + + /// Reuse the same node a number of times before picking a new one + case limitedReuse(count: UInt) + } + + /// The behaviour that should occur when attempting to retrieve the next snode after the swarm has been drained + /// according to the `Stragegy` + public enum AfterDrain: Sendable { + case throwError + case resetState + } + + public struct LogDetails { + let cat: Log.Category? + let name: String? + + public init(cat: Log.Category?, name: String?) { + self.cat = cat + self.name = name + } + + fileprivate func log(_ message: String) { + switch cat { + case .some(let cat): Log.info(cat, "\(name.map { "\($0) " } ?? "")\(message)") + case .none: Log.info("\(name.map { "\($0) " } ?? "")\(message)") + } + } + } + + private let dependencies: Dependencies + private let strategy: Strategy + private let nextRetrievalAfterDrain: AfterDrain + private let logDetails: LogDetails? + + private var swarm: Set + private var remainingSnodes: Set + private var swarmHash: Int + private var targetSnode: LibSession.Snode? + private var targetUseCount: Int + + // MARK: - Initialization + + public init( + swarm: Set = [], + strategy: Strategy = .alwaysRandom, + nextRetrievalAfterDrain: AfterDrain = .throwError, + logDetails: LogDetails? = nil, + using dependencies: Dependencies + ) { + self.dependencies = dependencies + self.strategy = strategy + self.nextRetrievalAfterDrain = nextRetrievalAfterDrain + self.logDetails = logDetails + + self.swarm = swarm + self.remainingSnodes = swarm + self.swarmHash = swarm.hashValue + self.targetSnode = nil + self.targetUseCount = 0 + } + + // MARK: - Functions + + public func updateSwarmIfNeeded(_ swarm: Set) { + guard swarmHash != swarm.hashValue else { return } + + self.swarm = swarm + self.remainingSnodes = swarm + self.swarmHash = swarm.hashValue + self.targetSnode = nil + self.targetUseCount = 0 + } + + public func selectNextNode() throws -> LibSession.Snode { + /// If the swarm was changed then reset the state + if self.swarmHash != swarm.hashValue { + self.resetState() + } + + /// If we have already drained the swarm then we need to behave as per the specified `nextRetrievalAfterDrain` behaviour + if self.remainingSnodes.isEmpty { + switch nextRetrievalAfterDrain { + case .throwError: throw SnodeAPIError.ranOutOfRandomSnodes(nil) + case .resetState: + logDetails?.log("drained the swarm, resetting state.") + self.resetState() + } + } + + switch self.strategy { + case .alwaysRandom: + /// Just pop a random element + guard let snode: LibSession.Snode = dependencies.popRandomElement(&self.remainingSnodes) else { + throw SnodeAPIError.ranOutOfRandomSnodes(nil) + } + + return snode + + case .limitedReuse(let maxUseCount): + if let target: LibSession.Snode = self.targetSnode { + /// If we have more retries then just keep the same target + if self.targetUseCount < maxUseCount { + self.targetUseCount += 1 + return target + } + + /// Otherwise log that we are switching + logDetails?.log("switching from \(target) to next snode.") + } + + self.targetSnode = nil + self.targetUseCount = 0 + + /// Select the next node + guard let newTarget: LibSession.Snode = dependencies.popRandomElement(&self.remainingSnodes) else { + throw SnodeAPIError.ranOutOfRandomSnodes(nil) + } + + self.targetSnode = newTarget + self.targetUseCount = 1 + + return newTarget + } + } + + private func resetState() { + self.remainingSnodes = swarm + self.swarmHash = swarm.hashValue + self.targetSnode = nil + self.targetUseCount = 0 + } +} From bb9bfb148a687ce5a5f28e61cfb8afb0a3b72507 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 10:16:10 +1000 Subject: [PATCH 17/59] Cleaned up the SnodeAPI, updated getSessionId to async/await --- SessionNetworkingKit/SnodeAPI/SnodeAPI.swift | 122 ++++++------------- 1 file changed, 39 insertions(+), 83 deletions(-) diff --git a/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift index f744253806..e44e17ccb0 100644 --- a/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift +++ b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift @@ -3,7 +3,6 @@ // stringlint:disable import Foundation -import Combine import Punycode import SessionUtilitiesKit @@ -224,55 +223,47 @@ public final class SnodeAPI { let nameHash = dependencies[singleton: .crypto].generate( .hash(message: Array(onsName.utf8)) ) - else { - return Fail(error: SnodeAPIError.onsHashingFailed) - .eraseToAnyPublisher() - } + else { throw SnodeAPIError.onsHashingFailed } // Ask 3 different snodes for the Session ID associated with the given name hash let base64EncodedNameHash = nameHash.toBase64() - - return dependencies[singleton: .network] + let nodes: Set = try await dependencies[singleton: .network] .getRandomNodes(count: validationCount) - .tryFlatMap { nodes in - Publishers.MergeMany( - try nodes.map { snode in - try SnodeAPI - .prepareRequest( - request: Request( - endpoint: .oxenDaemonRPCCall, - snode: snode, - body: OxenDaemonRPCRequest( - endpoint: .daemonOnsResolve, - body: ONSResolveRequest( - type: 0, // type 0 means Session - base64EncodedNameHash: base64EncodedNameHash - ) - ) - ), - responseType: ONSResolveResponse.self, - using: dependencies - ) - .tryMap { _, response -> String in - try dependencies[singleton: .crypto].tryGenerate( - .sessionId(name: onsName, response: response) + let results: [String] = try await withThrowingTaskGroup { [dependencies] group in + for node in nodes { + group.addTask { [dependencies] in + let request: Network.PreparedRequest = try SnodeAPI.prepareRequest( + request: Request( + endpoint: .oxenDaemonRPCCall, + snode: node, + body: OxenDaemonRPCRequest( + endpoint: .daemonOnsResolve, + body: ONSResolveRequest( + type: 0, // type 0 means Session + base64EncodedNameHash: base64EncodedNameHash ) - } - .send(using: dependencies) - .map { _, sessionId in sessionId } - .eraseToAnyPublisher() - } - ) - } - .collect() - .tryMap { results -> String in - guard results.count == validationCount, Set(results).count == 1 else { - throw SnodeAPIError.onsValidationFailed + ) + ), + responseType: ONSResolveResponse.self, + using: dependencies + ) + + let response: ONSResolveResponse = try await request.send(using: dependencies) + + return try dependencies[singleton: .crypto].tryGenerate( + .sessionId(name: onsName, response: response) + ) } - - return results[0] } - .eraseToAnyPublisher() + + return try await group.reduce(into: []) { result, next in result.append(next) } + } + + guard results.count == validationCount, Set(results).count == 1 else { + throw SnodeAPIError.onsValidationFailed + } + + return results[0] } public static func preparedGetExpiries( @@ -315,8 +306,7 @@ public final class SnodeAPI { message: message, namespace: namespace ), - overallTimeout: Network.defaultTimeout, - snodeRetrievalRetryCount: 0 // The SendMessageJob already has a retry mechanism + overallTimeout: Network.defaultTimeout ), responseType: SendMessagesResponse.self, using: dependencies @@ -333,8 +323,7 @@ public final class SnodeAPI { authMethod: authMethod, timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() ), - overallTimeout: Network.defaultTimeout, - snodeRetrievalRetryCount: 0 // The SendMessageJob already has a retry mechanism + overallTimeout: Network.defaultTimeout ), responseType: SendMessagesResponse.self, using: dependencies @@ -545,6 +534,7 @@ public final class SnodeAPI { /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation. public static func preparedDeleteAllMessages( namespace: SnodeAPI.Namespace, + snode: LibSession.Snode, requestTimeout: TimeInterval = Network.defaultTimeout, overallTimeout: TimeInterval? = nil, authMethod: AuthenticationMethod, @@ -554,16 +544,15 @@ public final class SnodeAPI { .prepareRequest( request: Request( endpoint: .deleteAll, + snode: snode, swarmPublicKey: try authMethod.swarmPublicKey, - requiresLatestNetworkTime: true, body: DeleteAllMessagesRequest( namespace: namespace, authMethod: authMethod, timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() ), requestTimeout: requestTimeout, - overallTimeout: overallTimeout, - snodeRetrievalRetryCount: 0 + overallTimeout: overallTimeout ), responseType: DeleteAllMessagesResponse.self, using: dependencies @@ -581,39 +570,6 @@ public final class SnodeAPI { } } - /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation. - public static func preparedDeleteAllMessages( - beforeMs: UInt64, - namespace: SnodeAPI.Namespace, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest<[String: Bool]> { - return try SnodeAPI - .prepareRequest( - request: Request( - endpoint: .deleteAllBefore, - swarmPublicKey: try authMethod.swarmPublicKey, - requiresLatestNetworkTime: true, - body: DeleteAllBeforeRequest( - beforeMs: beforeMs, - namespace: namespace, - authMethod: authMethod, - timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() - ), - retryCount: maxRetryCount, - ), - responseType: DeleteAllMessagesResponse.self, - using: dependencies - ) - .tryMap { _, response -> [String: Bool] in - try response.validResultMap( - swarmPublicKey: try authMethod.swarmPublicKey, - validationData: beforeMs, - using: dependencies - ) - } - } - // MARK: - Internal API public static func preparedGetNetworkTime( From 266d1426369b5a2a3221669df272b6004dbf0d58 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 10:17:38 +1000 Subject: [PATCH 18/59] Refactored the PushNotificationAPI to be async/await --- Session/Notifications/SyncPushTokensJob.swift | 250 +++++++------- .../_036_GroupsRebuildChanges.swift | 21 +- .../Database/Models/ClosedGroup.swift | 25 +- .../MessageSender+Groups.swift | 41 +-- .../Notifications/PushNotificationAPI.swift | 323 ++++++++---------- 5 files changed, 309 insertions(+), 351 deletions(-) diff --git a/Session/Notifications/SyncPushTokensJob.swift b/Session/Notifications/SyncPushTokensJob.swift index 7cf9efb5a4..684d27f463 100644 --- a/Session/Notifications/SyncPushTokensJob.swift +++ b/Session/Notifications/SyncPushTokensJob.swift @@ -66,45 +66,44 @@ public enum SyncPushTokensJob: JobExecutor { // If the job is running and 'Fast Mode' is disabled then we should try to unregister the existing // token guard isUsingFullAPNs else { - dependencies[singleton: .storage] - .readPublisher { db in db[.lastRecordedPushToken] } - .flatMap { lastRecordedPushToken -> AnyPublisher in - // Tell the device to unregister for remote notifications (essentially try to invalidate - // the token if needed - we do this first to avoid wrid race conditions which could be - // triggered by the user immediately re-registering) - DispatchQueue.main.sync { UIApplication.shared.unregisterForRemoteNotifications() } - - // Clear the old token - dependencies[singleton: .storage].write { db in - db[.lastRecordedPushToken] = nil + Task { + // Get the last token we subscribed with + let lastRecordedPushToken: String? = try? await dependencies[singleton: .storage].readAsync { db in + db[.lastRecordedPushToken] + } + + // Tell the device to unregister for remote notifications (essentially try to invalidate + // the token if needed - we do this first to avoid wrid race conditions which could be + // triggered by the user immediately re-registering) + await UIApplication.shared.unregisterForRemoteNotifications() + + // Clear the old token + try? await dependencies[singleton: .storage].writeAsync { db in + db[.lastRecordedPushToken] = nil + } + + // Unregister from our server + if let existingToken: String = lastRecordedPushToken { + Log.info(.syncPushTokensJob, "Unregister using last recorded push token: \(redact(existingToken))") + do { + try await PushNotificationAPI.unsubscribeAll( + token: Data(hex: existingToken), + using: dependencies + ) + Log.info(.syncPushTokensJob, "Unregister Completed") } - - // Unregister from our server - if let existingToken: String = lastRecordedPushToken { - Log.info(.syncPushTokensJob, "Unregister using last recorded push token: \(redact(existingToken))") - return PushNotificationAPI - .unsubscribeAll(token: Data(hex: existingToken), using: dependencies) - .map { _ in () } - .eraseToAnyPublisher() + catch { + Log.error(.syncPushTokensJob, "Unregister Failed with error: \(error)") } - + return + } + else { Log.info(.syncPushTokensJob, "No previous token stored just triggering device unregister") - return Just(()) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() } - .subscribe(on: scheduler, using: dependencies) - .sinkUntilComplete( - receiveCompletion: { result in - switch result { - case .finished: Log.info(.syncPushTokensJob, "Unregister Completed") - case .failure: Log.error(.syncPushTokensJob, "Unregister Failed") - } - - // We want to complete this job regardless of success or failure - success(job, false) - } - ) + + // We want to complete this job regardless of success or failure + success(job, false) + } return } @@ -113,101 +112,104 @@ public enum SyncPushTokensJob: JobExecutor { /// **Note:** Apple's documentation states that we should re-register for notifications on every launch: /// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/HandlingRemoteNotifications.html#//apple_ref/doc/uid/TP40008194-CH6-SW1 Log.info(.syncPushTokensJob, "Re-registering for remote notifications") + + // FIXME: We should refactor this entire process to be async/await dependencies[singleton: .pushRegistrationManager].requestPushTokens() - .flatMap { (pushToken: String, voipToken: String) -> AnyPublisher<(String, String)?, Error> in - Log.info(.syncPushTokensJob, "Received push and voip tokens, waiting for paths to build") - - return dependencies[cache: .libSessionNetwork].paths - .filter { !$0.isEmpty } - .first() // Only listen for the first callback - .map { _ in (pushToken, voipToken) } - .setFailureType(to: Error.self) - .timeout( - .seconds(5), // Give the paths a chance to build on launch - scheduler: scheduler, - customError: { NetworkError.timeout(error: "", rawData: nil) } - ) - .catch { error -> AnyPublisher<(String, String)?, Error> in - switch error { - case NetworkError.timeout: - Log.info(.syncPushTokensJob, "OS subscription completed, skipping server subscription due to path build timeout") - return Just(nil).setFailureType(to: Error.self).eraseToAnyPublisher() + .sinkUntilComplete( + receiveCompletion: { result in + /// Just succeed on failure + switch result { + case .finished: break + case .failure: success(job, false) + } + }, + receiveValue: { (pushToken: String, voipToken: String) in + Task { + let hasConnection: Bool = await withThrowingTaskGroup { group in + group.addTask { + _ = await dependencies[singleton: .network].networkStatus.first(where: { + $0 == .connected + }) + } + group.addTask { + /// Give the paths a chance to build on launch/ + try await Task.sleep(for: .seconds(5)) + throw NetworkError.timeout(error: "", rawData: nil) + } - default: return Fail(error: error).eraseToAnyPublisher() + let output: Result? = await group.nextResult() + group.cancelAll() + + switch output { + case .failure, .none: return false + case .success: return true + } } - } - .eraseToAnyPublisher() - } - .flatMapStorageReadPublisher(using: dependencies) { db, tokenInfo -> (String?, (String, String)?) in - (db[.lastRecordedPushToken], tokenInfo) - } - .flatMap { (lastRecordedPushToken: String?, tokenInfo: (String, String)?) -> AnyPublisher in - guard let (pushToken, voipToken): (String, String) = tokenInfo else { - return Just(()) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - - /// For our `subscribe` endpoint we only want to call it if: - /// • It's been longer than `SyncPushTokensJob.maxFrequency` since the last successful subscription; - /// • The token has changed; or - /// • We want to force an update - let timeSinceLastSuccessfulUpload: TimeInterval = dependencies.dateNow - .timeIntervalSince( - Date(timeIntervalSince1970: dependencies[defaults: .standard, key: .lastDeviceTokenUpload]) - ) - let uploadOnlyIfStale: Bool? = { - guard - let detailsData: Data = job.details, - let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData) - else { return nil } - - return details.uploadOnlyIfStale - }() - - guard - timeSinceLastSuccessfulUpload >= SyncPushTokensJob.maxFrequency || - lastRecordedPushToken != pushToken || - uploadOnlyIfStale == false - else { - Log.info(.syncPushTokensJob, "OS subscription completed, skipping server subscription due to frequency") - return Just(()) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - - Log.info(.syncPushTokensJob, "Sending push token to PN server") - return PushNotificationAPI - .subscribeAll( - token: Data(hex: pushToken), - isForcedUpdate: true, - using: dependencies - ) - .retry(3, using: dependencies) - .handleEvents( - receiveCompletion: { result in - switch result { - case .failure(let error): - Log.error(.syncPushTokensJob, "Failed to register due to error: \(error)") + + /// Just log and succeed on failure + guard hasConnection else { + Log.info(.syncPushTokensJob, "OS subscription completed, skipping server subscription due to path build timeout") + return success(job, false) + } + + /// Get the last token we subscribed with + let lastRecordedPushToken: String? = try? await dependencies[singleton: .storage].readAsync { db in + db[.lastRecordedPushToken] + } + + /// For our `subscribe` endpoint we only want to call it if: + /// • It's been longer than `SyncPushTokensJob.maxFrequency` since the last successful subscription; + /// • The token has changed; or + /// • We want to force an update + let timeSinceLastSuccessfulUpload: TimeInterval = dependencies.dateNow + .timeIntervalSince( + Date(timeIntervalSince1970: dependencies[defaults: .standard, key: .lastDeviceTokenUpload]) + ) + let uploadOnlyIfStale: Bool? = { + guard + let detailsData: Data = job.details, + let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData) + else { return nil } + + return details.uploadOnlyIfStale + }() + + /// No need to re-subscribe on the push server + guard + timeSinceLastSuccessfulUpload >= SyncPushTokensJob.maxFrequency || + lastRecordedPushToken != pushToken || + uploadOnlyIfStale == false + else { + Log.info(.syncPushTokensJob, "OS subscription completed, skipping server subscription due to frequency") + return success(job, false) + } + + Log.info(.syncPushTokensJob, "Sending push token to PN server") + + /// Retry up to 3 times + for _ in 0..<3 { + do { + try await PushNotificationAPI.subscribeAll( + token: Data(hex: pushToken), + isForcedUpdate: true, + using: dependencies + ) + Log.debug(.syncPushTokensJob, "Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))") + Log.info(.syncPushTokensJob, "Completed") - case .finished: - Log.debug(.syncPushTokensJob, "Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))") - Log.info(.syncPushTokensJob, "Completed") - - dependencies[singleton: .storage].write { db in - db[.lastRecordedPushToken] = pushToken - db[.lastRecordedVoipToken] = voipToken - } + dependencies[singleton: .storage].write { db in + db[.lastRecordedPushToken] = pushToken + db[.lastRecordedVoipToken] = voipToken + } + break } + catch { Log.error(.syncPushTokensJob, "Failed to register due to error: \(error)") } } - ) - .map { _ in () } - .eraseToAnyPublisher() - } - .subscribe(on: scheduler, using: dependencies) - .sinkUntilComplete( - // We want to complete this job regardless of success or failure - receiveCompletion: { _ in success(job, false) } + + /// We want to complete this job regardless of success or failure + return success(job, false) + } + } ) } diff --git a/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift b/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift index 87081585e3..6bd3de624c 100644 --- a/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift @@ -145,18 +145,15 @@ enum _036_GroupsRebuildChanges: Migration { /// If the group isn't in the invited state then make sure to subscribe for PNs once the migrations are done if !group.invited, let token: String = dependencies[defaults: .standard, key: .deviceToken] { db.afterCommit { - dependencies[singleton: .storage] - .readPublisher { db in - try PushNotificationAPI.preparedSubscribe( - db, - token: Data(hex: token), - sessionIds: [SessionId(.group, hex: group.groupSessionId)], - using: dependencies - ) - } - .flatMap { $0.send(using: dependencies) } - .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) - .sinkUntilComplete() + Task.detached(priority: .userInitiated) { + try? await PushNotificationAPI.subscribe( + token: Data(hex: token), + swarmAuthentication: [ + try? Authentication.with(swarmPublicKey: group.groupSessionId, using: dependencies) + ].compactMap { $0 }, + using: dependencies + ) + } } } } diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index 7ad3fbfc69..dac7977bdd 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -226,16 +226,15 @@ public extension ClosedGroup { /// Subscribe for group push notifications if let token: String = dependencies[defaults: .standard, key: .deviceToken] { - try? PushNotificationAPI - .preparedSubscribe( - db, + Task.detached(priority: .userInitiated) { + try? await PushNotificationAPI.subscribe( token: Data(hex: token), - sessionIds: [SessionId(.group, hex: group.id)], + swarmAuthentication: [ + try? Authentication.with(swarmPublicKey: group.id, using: dependencies) + ].compactMap { $0 }, using: dependencies ) - .send(using: dependencies) - .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) - .sinkUntilComplete() + } } } @@ -307,17 +306,15 @@ public extension ClosedGroup { /// Bulk unsubscripe from updated groups being removed if dataToRemove.contains(.pushNotifications) && threadVariants.contains(where: { $0.variant == .group }) { if let token: String = dependencies[defaults: .standard, key: .deviceToken] { - try? PushNotificationAPI - .preparedUnsubscribe( - db, + Task.detached(priority: .userInitiated) { [dependencies] in + try? await PushNotificationAPI.unsubscribe( token: Data(hex: token), - sessionIds: threadVariants + swarmAuthentication: threadVariants .filter { $0.variant == .group } - .map { SessionId(.group, hex: $0.id) }, + .compactMap { try? Authentication.with(swarmPublicKey: $0.id, using: dependencies) }, using: dependencies ) - .send(using: dependencies) - .sinkUntilComplete() + } } } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift index 401ec56db4..c9ce139e11 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift @@ -12,8 +12,7 @@ extension MessageSender { groupState: [ConfigDump.Variant: LibSession.Config], thread: SessionThread, group: ClosedGroup, - members: [GroupMember], - preparedNotificationsSubscription: Network.PreparedRequest? + members: [GroupMember] ) public static func createGroup( @@ -118,26 +117,12 @@ extension MessageSender { canStartJob: false ) - // Prepare the notification subscription - var preparedNotificationSubscription: Network.PreparedRequest? - - if let token: String = dependencies[defaults: .standard, key: .deviceToken] { - preparedNotificationSubscription = try? PushNotificationAPI - .preparedSubscribe( - db, - token: Data(hex: token), - sessionIds: [createdInfo.groupSessionId], - using: dependencies - ) - } - return ( createdInfo.groupSessionId, createdInfo.groupState, thread, createdInfo.group, - createdInfo.members, - preparedNotificationSubscription + createdInfo.members ) } } @@ -187,7 +172,7 @@ extension MessageSender { .eraseToAnyPublisher() } .handleEvents( - receiveOutput: { groupSessionId, _, thread, group, groupMembers, preparedNotificationSubscription in + receiveOutput: { groupSessionId, _, thread, group, groupMembers in let userSessionId: SessionId = dependencies[cache: .general].sessionId // Start polling @@ -196,10 +181,20 @@ extension MessageSender { } // Subscribe for push notifications (if PNs are enabled) - preparedNotificationSubscription? - .send(using: dependencies) - .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) - .sinkUntilComplete() + if let token: String = dependencies[defaults: .standard, key: .deviceToken] { + Task.detached(priority: .userInitiated) { [dependencies] in + try? await PushNotificationAPI.subscribe( + token: Data(hex: token), + swarmAuthentication: [ + try? Authentication.with( + swarmPublicKey: groupSessionId.hexString, + using: dependencies + ) + ].compactMap { $0 }, + using: dependencies + ) + } + } dependencies[singleton: .storage].writeAsync { db in // Save jobs for sending group member invitations @@ -239,7 +234,7 @@ extension MessageSender { } } ) - .map { _, _, thread, _, _, _ in thread } + .map { _, _, thread, _, _ in thread } .eraseToAnyPublisher() } diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index e559754c80..92897c570f 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -34,102 +34,74 @@ public enum PushNotificationAPI { token: Data, isForcedUpdate: Bool, using dependencies: Dependencies - ) -> AnyPublisher { + ) async throws { let hexEncodedToken: String = token.toHexString() let oldToken: String? = dependencies[defaults: .standard, key: .deviceToken] let lastUploadTime: Double = dependencies[defaults: .standard, key: .lastDeviceTokenUpload] let now: TimeInterval = dependencies.dateNow.timeIntervalSince1970 guard isForcedUpdate || hexEncodedToken != oldToken || now - lastUploadTime > tokenExpirationInterval else { - Log.info(.cat, "Device token hasn't changed or expired; no need to re-upload.") - return Just(()) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() + return Log.info(.cat, "Device token hasn't changed or expired; no need to re-upload.") } - return dependencies[singleton: .storage] - .readPublisher { db -> Network.PreparedRequest in - let userSessionId: SessionId = dependencies[cache: .general].sessionId - - return try PushNotificationAPI - .preparedSubscribe( - db, - token: token, - sessionIds: [userSessionId] - .appending(contentsOf: try ClosedGroup - .select(.threadId) - .filter( - ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString - ) - .filter(ClosedGroup.Columns.shouldPoll) - .asRequest(of: String.self) - .fetchSet(db) - .map { SessionId(.group, hex: $0) } - ), - using: dependencies - ) - .handleEvents( - receiveOutput: { _, response in - guard response.subResponses.first?.success == true else { return } - - dependencies[defaults: .standard, key: .deviceToken] = hexEncodedToken - dependencies[defaults: .standard, key: .lastDeviceTokenUpload] = now - dependencies[defaults: .standard, key: .isUsingFullAPNs] = true - } - ) - } - .flatMap { $0.send(using: dependencies) } - .map { _ in () } - .eraseToAnyPublisher() + let swarmAuthentication: [AuthenticationMethod] = try await retrieveAllSwarmAuth(using: dependencies) + let response: SubscribeResponse = try await PushNotificationAPI.subscribe( + token: token, + swarmAuthentication: swarmAuthentication, + using: dependencies + ) + + /// Only cache the token data If we successfully subscribed for user PNs + if response.subResponses.first?.success == true { + dependencies[defaults: .standard, key: .deviceToken] = hexEncodedToken + dependencies[defaults: .standard, key: .lastDeviceTokenUpload] = now + dependencies[defaults: .standard, key: .isUsingFullAPNs] = true + } } public static func unsubscribeAll( token: Data, using dependencies: Dependencies - ) -> AnyPublisher { - return dependencies[singleton: .storage] - .readPublisher { db -> Network.PreparedRequest in - let userSessionId: SessionId = dependencies[cache: .general].sessionId - - return try PushNotificationAPI - .preparedUnsubscribe( - db, - token: token, - sessionIds: [userSessionId] - .appending(contentsOf: (try? ClosedGroup - .select(.threadId) - .filter( - ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString - ) - .asRequest(of: String.self) - .fetchSet(db)) - .defaulting(to: []) - .map { SessionId(.group, hex: $0) }), - using: dependencies - ) - .handleEvents( - receiveOutput: { _, response in - guard response.subResponses.first?.success == true else { return } - - dependencies[defaults: .standard, key: .deviceToken] = nil - } - ) - } - .flatMap { $0.send(using: dependencies) } - .map { _ in () } - .eraseToAnyPublisher() + ) async throws { + let swarmAuthentication: [AuthenticationMethod] = try await retrieveAllSwarmAuth(using: dependencies) + let response: UnsubscribeResponse = try await PushNotificationAPI.unsubscribe( + token: token, + swarmAuthentication: swarmAuthentication, + using: dependencies + ) + + /// If we successfully unsubscribed for user PNs then remove the cached token + if response.subResponses.first?.success == true { + dependencies[defaults: .standard, key: .deviceToken] = nil + } + } + + private static func retrieveAllSwarmAuth(using dependencies: Dependencies) async throws -> [AuthenticationMethod] { + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let groupIds: Set = try await dependencies[singleton: .storage].readAsync { db in + try ClosedGroup + .select(.threadId) + .filter( + ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && + ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString + ) + .asRequest(of: String.self) + .fetchSet(db) + } + + return try ([userSessionId.hexString] + groupIds).map { + try Authentication.with(swarmPublicKey: $0, using: dependencies) + } } // MARK: - Prepared Requests - public static func preparedSubscribe( - _ db: ObservingDatabase, + public static func subscribe( token: Data, - sessionIds: [SessionId], + swarmAuthentication: [AuthenticationMethod], using dependencies: Dependencies - ) throws -> Network.PreparedRequest { + ) async throws -> SubscribeResponse { + guard !swarmAuthentication.isEmpty else { return SubscribeResponse(subResponses: []) } guard dependencies[defaults: .standard, key: .isUsingFullAPNs] else { throw NetworkError.invalidPreparedRequest } @@ -145,115 +117,110 @@ public enum PushNotificationAPI { throw KeychainStorageError.keySpecInvalid } - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: Endpoint.subscribe, - body: SubscribeRequest( - subscriptions: sessionIds.map { sessionId -> SubscribeRequest.Subscription in - SubscribeRequest.Subscription( - namespaces: { - switch sessionId.prefix { - case .group: return [ - .groupMessages, - .configGroupKeys, - .configGroupInfo, - .configGroupMembers, - .revokedRetrievableGroupMessages - ] - default: return [ - .default, - .configUserProfile, - .configContacts, - .configConvoInfoVolatile, - .configUserGroups - ] - } - }(), - /// Note: Unfortunately we always need the message content because without the content - /// control messages can't be distinguished from visible messages which results in the - /// 'generic' notification being shown when receiving things like typing indicator updates - includeMessageData: true, - serviceInfo: ServiceInfo( - token: token.toHexString() - ), - notificationsEncryptionKey: notificationsEncryptionKey, - authMethod: try Authentication.with( - db, - swarmPublicKey: sessionId.hexString, - using: dependencies - ), - timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) // Seconds - ) - } + do { + let request: Network.PreparedRequest = try Network.PreparedRequest( + request: Request( + method: .post, + endpoint: Endpoint.subscribe, + body: SubscribeRequest( + subscriptions: swarmAuthentication.map { authMethod -> SubscribeRequest.Subscription in + SubscribeRequest.Subscription( + namespaces: { + switch try? SessionId.Prefix(from: try? authMethod.swarmPublicKey) { + case .group: return [ + .groupMessages, + .configGroupKeys, + .configGroupInfo, + .configGroupMembers, + .revokedRetrievableGroupMessages + ] + default: return [ + .default, + .configUserProfile, + .configContacts, + .configConvoInfoVolatile, + .configUserGroups + ] + } + }(), + /// Note: Unfortunately we always need the message content because without the content + /// control messages can't be distinguished from visible messages which results in the + /// 'generic' notification being shown when receiving things like typing indicator updates + includeMessageData: true, + serviceInfo: ServiceInfo( + token: token.toHexString() + ), + notificationsEncryptionKey: notificationsEncryptionKey, + authMethod: authMethod, + timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) // Seconds + ) + } + ), + retryCount: PushNotificationAPI.maxRetryCount ), - retryCount: PushNotificationAPI.maxRetryCount - ), - responseType: SubscribeResponse.self, - using: dependencies - ) - .handleEvents( - receiveOutput: { _, response in - zip(response.subResponses, sessionIds).forEach { subResponse, sessionId in - guard subResponse.success != true else { return } - - Log.error(.cat, "Couldn't subscribe for push notifications for: \(sessionId) due to error (\(subResponse.error ?? -1)): \(subResponse.message ?? "nil").") - } - }, - receiveCompletion: { result in - switch result { - case .finished: break - case .failure(let error): Log.error(.cat, "Couldn't subscribe for push notifications due to error: \(error).") - } + responseType: SubscribeResponse.self, + using: dependencies + ) + let response: SubscribeResponse = try await request.send(using: dependencies) + + zip(response.subResponses, swarmAuthentication).forEach { subResponse, authMethod in + guard subResponse.success != true else { return } + + let swarmPublicKey: String = ((try? authMethod.swarmPublicKey) ?? "INVALID") + Log.error(.cat, "Couldn't subscribe for push notifications for: \(swarmPublicKey) due to error (\(subResponse.error ?? -1)): \(subResponse.message ?? "nil").") } - ) + + return response + } + catch { + Log.error(.cat, "Couldn't subscribe for push notifications due to error: \(error).") + throw error + } } - public static func preparedUnsubscribe( - _ db: ObservingDatabase, + public static func unsubscribe( token: Data, - sessionIds: [SessionId], + swarmAuthentication: [AuthenticationMethod], using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: Endpoint.unsubscribe, - body: UnsubscribeRequest( - subscriptions: sessionIds.map { sessionId -> UnsubscribeRequest.Subscription in - UnsubscribeRequest.Subscription( - serviceInfo: ServiceInfo( - token: token.toHexString() - ), - authMethod: try Authentication.with( - db, - swarmPublicKey: sessionId.hexString, - using: dependencies - ), - timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) // Seconds - ) - } + ) async throws -> UnsubscribeResponse { + guard !swarmAuthentication.isEmpty else { return UnsubscribeResponse(subResponses: []) } + + do { + let request: Network.PreparedRequest = try Network.PreparedRequest( + request: Request( + method: .post, + endpoint: Endpoint.unsubscribe, + body: UnsubscribeRequest( + subscriptions: swarmAuthentication.map { authMethod -> UnsubscribeRequest.Subscription in + UnsubscribeRequest.Subscription( + serviceInfo: ServiceInfo( + token: token.toHexString() + ), + authMethod: authMethod, + timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) // Seconds + ) + } + ), + retryCount: PushNotificationAPI.maxRetryCount ), - retryCount: PushNotificationAPI.maxRetryCount - ), - responseType: UnsubscribeResponse.self, - using: dependencies - ) - .handleEvents( - receiveOutput: { _, response in - zip(response.subResponses, sessionIds).forEach { subResponse, sessionId in - guard subResponse.success != true else { return } - - Log.error(.cat, "Couldn't unsubscribe for push notifications for: \(sessionId) due to error (\(subResponse.error ?? -1)): \(subResponse.message ?? "nil").") - } - }, - receiveCompletion: { result in - switch result { - case .finished: break - case .failure(let error): Log.error(.cat, "Couldn't unsubscribe for push notifications due to error: \(error).") - } + responseType: UnsubscribeResponse.self, + using: dependencies + ) + let response: UnsubscribeResponse = try await request.send(using: dependencies) + + zip(response.subResponses, swarmAuthentication).forEach { subResponse, authMethod in + guard subResponse.success != true else { return } + + let swarmPublicKey: String = ((try? authMethod.swarmPublicKey) ?? "INVALID") + Log.error(.cat, "Couldn't unsubscribe for push notifications for: \(swarmPublicKey) due to error (\(subResponse.error ?? -1)): \(subResponse.message ?? "nil").") } - ) + + return response + } + catch { + Log.error(.cat, "Couldn't unsubscribe for push notifications due to error: \(error).") + throw error + } } // MARK: - Notification Handling From fb9c224e50f305d7a7fd25479dee82213e3cf552 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 10:18:04 +1000 Subject: [PATCH 19/59] Updated NukeDataModel to do some async/await --- Session/Settings/NukeDataModal.swift | 176 +++++++++++++++------------ 1 file changed, 100 insertions(+), 76 deletions(-) diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index 3561029208..0a8e1f2e8b 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -177,81 +177,80 @@ final class NukeDataModal: Modal { ModalActivityIndicatorViewController .present(fromViewController: presentedViewController, canCancel: false) { [weak self, dependencies] _ in - dependencies[singleton: .storage] - .readPublisher { db -> [AuthenticationMethod] in - try OpenGroup - .filter(OpenGroup.Columns.isActive == true) - .select(.server) - .distinct() - .asRequest(of: String.self) - .fetchSet(db) - .map { try Authentication.with(db, server: $0, using: dependencies) } - } - .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) - .tryFlatMap { (communityAuth: [AuthenticationMethod]) -> AnyPublisher<(AuthenticationMethod, [String]), Error> in + Task(priority: .userInitiated) { [weak self, dependencies] in + do { + let communityAuth: [AuthenticationMethod] = try await dependencies[singleton: .storage].readAsync { db in + try OpenGroup + .filter(OpenGroup.Columns.isActive == true) + .select(.server) + .distinct() + .asRequest(of: String.self) + .fetchSet(db) + .map { try Authentication.with(db, server: $0, using: dependencies) } + } + + /// Clear the inbox of any known communities in case the user had sent messages to them + let clearedServers: [String] = try await withThrowingTaskGroup { group in + for authMethod in communityAuth { + guard case .community(let server, _, _, _, _) = authMethod.info else { continue } + + group.addTask { + _ = try await OpenGroupAPI + .preparedClearInbox( + overallTimeout: Network.defaultTimeout, + authMethod: authMethod, + using: dependencies + ) + .send(using: dependencies) + .value + + return server + } + } + + var result: [String] = [] + while !group.isEmpty { + guard let value: String = try await group.next() else { + throw NetworkError.invalidResponse + } + + result.append(value) + } + + return result + } + + /// Get the latest network time before deleting all messages (to reduce the chance that the call will fail + /// due to the network being out of sync) + let swarm: Set = try await dependencies[singleton: .network] + .getSwarm(for: dependencies[cache: .general].sessionId.hexString) + let snode: LibSession.Snode = try await SwarmDrainer(swarm: swarm, using: dependencies) + .selectNextNode() + let networkTimeRequest: Network.PreparedRequest = try SnodeAPI.preparedGetNetworkTime( + from: snode, + using: dependencies + ) + let timestampMs: UInt64 = try await networkTimeRequest.send(using: dependencies) + + /// Clear the users swarm let userAuth: AuthenticationMethod = try Authentication.with( swarmPublicKey: dependencies[cache: .general].sessionId.hexString, using: dependencies ) - - return Publishers - .MergeMany( - try communityAuth.compactMap { authMethod in - switch authMethod.info { - case .community(let server, _, _, _, _): - return try OpenGroupAPI.preparedClearInbox( - overallTimeout: Network.defaultTimeout, - authMethod: authMethod, - using: dependencies - ) - .map { _, _ in server } - .send(using: dependencies) - - default: return nil - } - } - ) - .collect() - .map { response in (userAuth, response.map { $0.1 }) } - .eraseToAnyPublisher() - } - .tryFlatMap { authMethod, clearedServers in - try SnodeAPI + var confirmations: [String: Bool] = try await SnodeAPI .preparedDeleteAllMessages( namespace: .all, + snode: snode, overallTimeout: Network.defaultTimeout, - authMethod: authMethod, + authMethod: userAuth, using: dependencies ) .send(using: dependencies) - .map { _, data in - clearedServers.reduce(into: data) { result, next in result[next] = true } - } - } - .receive(on: DispatchQueue.main, using: dependencies) - .sinkUntilComplete( - receiveCompletion: { result in - switch result { - case .finished: break - case .failure: - self?.dismiss(animated: true, completion: nil) // Dismiss the loader - - let modal: ConfirmationModal = ConfirmationModal( - targetView: self?.view, - info: ConfirmationModal.Info( - title: "clearDataAll".localized(), - body: .text("clearDataErrorDescriptionGeneric".localized()), - confirmTitle: "clearDevice".localized(), - confirmStyle: .danger, - cancelStyle: .alert_text - ) { [weak self] _ in - self?.clearDeviceOnly() - } - ) - self?.present(modal, animated: true) - } - }, - receiveValue: { confirmations in + + /// Add the cleared Community servers so we have a full list + clearedServers.forEach { confirmations[$0] = true } + + await MainActor.run { self?.dismiss(animated: true, completion: nil) // Dismiss the loader // Get a list of nodes which failed to delete the data @@ -279,7 +278,27 @@ final class NukeDataModal: Modal { ) self?.present(modal, animated: true) } - ) + } + catch { + await MainActor.run { + self?.dismiss(animated: true, completion: nil) // Dismiss the loader + + let modal: ConfirmationModal = ConfirmationModal( + targetView: self?.view, + info: ConfirmationModal.Info( + title: "clearDataAll".localized(), + body: .text("clearDataErrorDescriptionGeneric".localized()), + confirmTitle: "clearDevice".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text + ) { [weak self] _ in + self?.clearDeviceOnly() + } + ) + self?.present(modal, animated: true) + } + } + } } } @@ -294,9 +313,12 @@ final class NukeDataModal: Modal { UIApplication.shared.unregisterForRemoteNotifications() if let deviceToken: String = maybeDeviceToken, dependencies[singleton: .storage].isValid { - PushNotificationAPI - .unsubscribeAll(token: Data(hex: deviceToken), using: dependencies) - .sinkUntilComplete() + Task.detached(priority: .userInitiated) { + try? await PushNotificationAPI.unsubscribeAll( + token: Data(hex: deviceToken), + using: dependencies + ) + } } } @@ -318,13 +340,15 @@ final class NukeDataModal: Modal { let wasUnlinked: Bool = dependencies[defaults: .standard, key: .wasUnlinked] let serviceNetwork: ServiceNetwork = dependencies[feature: .serviceNetwork] - dependencies[singleton: .app].resetData { [dependencies] in - // Resetting the data clears the old user defaults. We need to restore the unlink default. - dependencies[defaults: .standard, key: .wasUnlinked] = wasUnlinked - - // We also want to keep the `ServiceNetwork` setting (so someone testing can delete and restore - // accounts on Testnet without issue - dependencies.set(feature: .serviceNetwork, to: serviceNetwork) + Task { + await dependencies[singleton: .app].resetData { [dependencies] in + // Resetting the data clears the old user defaults. We need to restore the unlink default. + dependencies[defaults: .standard, key: .wasUnlinked] = wasUnlinked + + // We also want to keep the `ServiceNetwork` setting (so someone testing can delete and restore + // accounts on Testnet without issue + dependencies.set(feature: .serviceNetwork, to: serviceNetwork) + } } } } From 99f5b88526a7028a590acce0d18bcd37ca643b29 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 10:22:28 +1000 Subject: [PATCH 20/59] Initial dev settings screen updates --- .../DeveloperSettingsViewModel+Testing.swift | 8 +- .../Settings/DeveloperSettingsViewModel.swift | 313 +++++++++--------- 2 files changed, 160 insertions(+), 161 deletions(-) diff --git a/Session/Settings/DeveloperSettingsViewModel+Testing.swift b/Session/Settings/DeveloperSettingsViewModel+Testing.swift index 185528af9c..6cac61c9f0 100644 --- a/Session/Settings/DeveloperSettingsViewModel+Testing.swift +++ b/Session/Settings/DeveloperSettingsViewModel+Testing.swift @@ -27,7 +27,7 @@ extension DeveloperSettingsViewModel { /// ``` /// /// **Note:** All values need to be provided as strings (eg. booleans) - static func processUnitTestEnvVariablesIfNeeded(using dependencies: Dependencies) { + static func processUnitTestEnvVariablesIfNeeded(using dependencies: Dependencies) async { #if targetEnvironment(simulator) enum EnvironmentVariable: String { /// Disables animations for the app (where possible) @@ -62,7 +62,7 @@ extension DeveloperSettingsViewModel { case debugDisappearingMessageDurations } - ProcessInfo.processInfo.environment.forEach { key, value in + for (key, value) in ProcessInfo.processInfo.environment { guard let variable: EnvironmentVariable = EnvironmentVariable(rawValue: key) else { return } switch variable { @@ -71,7 +71,7 @@ extension DeveloperSettingsViewModel { guard value == "false" else { return } - UIView.setAnimationsEnabled(false) + await UIView.setAnimationsEnabled(false) case .showStringKeys: dependencies.set(feature: .showStringKeys, to: (value == "true")) @@ -87,7 +87,7 @@ extension DeveloperSettingsViewModel { default: network = .mainnet } - DeveloperSettingsViewModel.updateServiceNetwork(to: network, using: dependencies) + await DeveloperSettingsViewModel.updateServiceNetwork(to: network, using: dependencies) case .forceOffline: dependencies.set(feature: .forceOffline, to: (value == "true")) diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index 436d6f7a80..47f15b8d6a 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -1090,8 +1090,10 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, } private func updateServiceNetwork(to updatedNetwork: ServiceNetwork?) { - DeveloperSettingsViewModel.updateServiceNetwork(to: updatedNetwork, using: dependencies) - forceRefresh(type: .databaseQuery) + Task { + await DeveloperSettingsViewModel.updateServiceNetwork(to: updatedNetwork, using: dependencies) + await MainActor.run { forceRefresh(type: .databaseQuery) } + } } private func updatePushNotificationService(to updatedService: PushNotificationAPI.Service?) { @@ -1123,7 +1125,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, internal static func updateServiceNetwork( to updatedNetwork: ServiceNetwork?, using dependencies: Dependencies - ) { + ) async { struct IdentityData { let ed25519KeyPair: KeyPair let x25519KeyPair: KeyPair @@ -1132,7 +1134,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, /// Make sure we are actually changing the network before clearing all of the data guard updatedNetwork != dependencies[feature: .serviceNetwork], - let identityData: IdentityData = dependencies[singleton: .storage].read({ db in + let identityData: IdentityData = try? await dependencies[singleton: .storage].readAsync(value: { db in IdentityData( ed25519KeyPair: KeyPair( publicKey: Array(try Identity @@ -1167,25 +1169,23 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, /// Reset the network /// - /// **Note:** We need to store the current `libSessionNetwork` cache until after we swap the `serviceNetwork` - /// and start warming the new cache, otherwise it's destruction can result in automatic recreation due to path and network - /// status observers - dependencies.mutate(cache: .libSessionNetwork) { - $0.setPaths(paths: []) - $0.setNetworkStatus(status: .unknown) - $0.suspendNetworkAccess() - $0.clearSnodeCache() - $0.clearCallbacks() - } - var oldNetworkCache: LibSession.NetworkImmutableCacheType? = dependencies[cache: .libSessionNetwork] - dependencies.remove(cache: .libSessionNetwork) + /// **Note:** We need to set this to a `NoopNetwork` because a number of objects observe the `networkStatus` which + /// would result in automatic re-creation of the network with it's current config (since the `serviceNetwork` hasn't been updated + /// yet) + await dependencies[singleton: .network].suspendNetworkAccess() + await dependencies[singleton: .network].finishCurrentObservations() + await dependencies[singleton: .network].clearCache() + dependencies.set(singleton: .network, to: LibSession.NoopNetwork()) /// Unsubscribe from push notifications (do this after resetting the network as they are server requests so aren't dependant on a service /// layer and we don't want these to be cancelled) - if let existingToken: String = dependencies[singleton: .storage].read({ db in db[.lastRecordedPushToken] }) { - PushNotificationAPI - .unsubscribeAll(token: Data(hex: existingToken), using: dependencies) - .sinkUntilComplete() + if let existingToken: String = try? await dependencies[singleton: .storage].readAsync(value: { db in db[.lastRecordedPushToken] }) { + Task.detached(priority: .userInitiated) { + try? await PushNotificationAPI.unsubscribeAll( + token: Data(hex: existingToken), + using: dependencies + ) + } } /// Clear the snodeAPI caches @@ -1196,7 +1196,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, dependencies.remove(cache: .libSession) /// Remove any network-specific data - dependencies[singleton: .storage].write { [dependencies] db in + try? await dependencies[singleton: .storage].writeAsync { [dependencies] db in let userSessionId: SessionId = dependencies[cache: .general].sessionId _ = try SnodeReceivedMessageInfo.deleteAll(db) @@ -1224,12 +1224,10 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, /// Update to the new `ServiceNetwork` dependencies.set(feature: .serviceNetwork, to: updatedNetwork) - /// Start the new network cache and clear out the old one + /// Remove the temporary NoopNetwork and warm a new instance now that the `serviceNetwork` has been updated + dependencies.remove(singleton: .network) dependencies.warm(singleton: .network) - /// Free the `oldNetworkCache` so it can be destroyed(the 'if' is only there to prevent the "variable never read" warning) - if oldNetworkCache != nil { oldNetworkCache = nil } - /// Run the onboarding process as if we are recovering an account (will setup the device in it's proper state) Onboarding.Cache( ed25519KeyPair: identityData.ed25519KeyPair, @@ -1243,8 +1241,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, dependencies.setAsync(.developerModeEnabled, true) /// Restart the current user poller (there won't be any other pollers though) - Task { @MainActor [currentUserPoller = dependencies[singleton: .currentUserPoller]] in - currentUserPoller.startIfNeeded() + Task { @MainActor [poller = dependencies[singleton: .currentUserPoller]] in + await poller.startIfNeeded() } /// Re-sync the push tokens (if there are any) @@ -1274,11 +1272,10 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, updateFlag(for: .forceOffline, to: !current) // Reset the network cache - dependencies.mutate(cache: .libSessionNetwork) { - $0.setPaths(paths: []) - $0.setNetworkStatus(status: current ? .unknown : .disconnected) + Task { + await dependencies[singleton: .network].setNetworkStatus(status: current ? .unknown : .disconnected) + dependencies.remove(singleton: .network) } - dependencies.remove(cache: .libSessionNetwork) } private func resetServiceNodeCache() { @@ -1296,7 +1293,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, dependencies.remove(cache: .snodeAPI) /// Clear the snode cache - dependencies.mutate(cache: .libSessionNetwork) { $0.clearSnodeCache() } + Task { await dependencies[singleton: .network].clearCache() } } ) ), @@ -1729,142 +1726,144 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, guard let url: URL = url else { return } let viewController: UIViewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self, password = self.databaseKeyEncryptionPassword, dependencies = self.dependencies] modalActivityIndicator in - do { - let tmpUnencryptPath: String = "\(dependencies[singleton: .fileManager].temporaryDirectory)/new_session.bak" - let (paths, additionalFilePaths): ([String], [String]) = try DirectoryArchiver.unarchiveDirectory( - archivePath: url.path, - destinationPath: tmpUnencryptPath, - password: password, - progressChanged: { filesSaved, totalFiles, fileProgress, fileSize in - let percentage: Int = { - guard fileSize > 0 else { return 0 } + Task { + do { + let tmpUnencryptPath: String = "\(dependencies[singleton: .fileManager].temporaryDirectory)/new_session.bak" + let (paths, additionalFilePaths): ([String], [String]) = try DirectoryArchiver.unarchiveDirectory( + archivePath: url.path, + destinationPath: tmpUnencryptPath, + password: password, + progressChanged: { filesSaved, totalFiles, fileProgress, fileSize in + let percentage: Int = { + guard fileSize > 0 else { return 0 } + + return Int((Double(fileProgress) / Double(fileSize)) * 100) + }() - return Int((Double(fileProgress) / Double(fileSize)) * 100) - }() - - DispatchQueue.main.async { - modalActivityIndicator.setMessage([ - "Decryption progress: \(percentage)%", - "Files imported: \(filesSaved)/\(totalFiles)" - ].compactMap { $0 }.joined(separator: "\n")) + DispatchQueue.main.async { + modalActivityIndicator.setMessage([ + "Decryption progress: \(percentage)%", + "Files imported: \(filesSaved)/\(totalFiles)" + ].compactMap { $0 }.joined(separator: "\n")) + } } - } - ) - - /// Test that we actually have valid access to the database - guard - let encKeyPath: String = additionalFilePaths - .first(where: { $0.hasSuffix(Storage.encKeyFilename) }), - let databasePath: String = paths - .first(where: { $0.hasSuffix(Storage.dbFileName) }) - else { throw ArchiveError.unableToFindDatabaseKey } - - DispatchQueue.main.async { - modalActivityIndicator.setMessage( - "Checking for valid database..." ) - } - - let testStorage: Storage = try Storage( - testAccessTo: databasePath, - encryptedKeyPath: encKeyPath, - encryptedKeyPassword: password, - using: dependencies - ) - - guard testStorage.isValid else { - throw ArchiveError.decryptionFailed(ArchiveError.unarchiveFailed) - } - - /// Now that we have confirmed access to the replacement database we need to - /// stop the current account from doing anything - DispatchQueue.main.async { - modalActivityIndicator.setMessage( - "Clearing current account data..." + + /// Test that we actually have valid access to the database + guard + let encKeyPath: String = additionalFilePaths + .first(where: { $0.hasSuffix(Storage.encKeyFilename) }), + let databasePath: String = paths + .first(where: { $0.hasSuffix(Storage.dbFileName) }) + else { throw ArchiveError.unableToFindDatabaseKey } + + DispatchQueue.main.async { + modalActivityIndicator.setMessage( + "Checking for valid database..." + ) + } + + let testStorage: Storage = try Storage( + testAccessTo: databasePath, + encryptedKeyPath: encKeyPath, + encryptedKeyPassword: password, + using: dependencies ) - (UIApplication.shared.delegate as? AppDelegate)?.stopPollers() - } - - /// Need to shut everything down before the swap out the data to prevent crashes - dependencies[singleton: .jobRunner].stopAndClearPendingJobs() - dependencies.remove(cache: .libSession) - dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() } - dependencies[singleton: .storage].suspendDatabaseAccess() - try dependencies[singleton: .storage].closeDatabase() - LibSession.clearLoggers() - - let deleteEnumerator: FileManager.DirectoryEnumerator? = FileManager.default.enumerator( - at: URL( - fileURLWithPath: dependencies[singleton: .fileManager].appSharedDataDirectoryPath - ), - includingPropertiesForKeys: [.isRegularFileKey, .isHiddenKey] - ) - let fileUrls: [URL] = (deleteEnumerator?.allObjects - .compactMap { $0 as? URL } - .filter { url -> Bool in - guard let resourceValues = try? url.resourceValues(forKeys: [.isHiddenKey]) else { - return true - } + guard testStorage.isValid else { + throw ArchiveError.decryptionFailed(ArchiveError.unarchiveFailed) + } + + /// Now that we have confirmed access to the replacement database we need to + /// stop the current account from doing anything + DispatchQueue.main.async { + modalActivityIndicator.setMessage( + "Clearing current account data..." + ) - return (resourceValues.isHidden != true) - }) - .defaulting(to: []) - try fileUrls.forEach { url in - /// The database `wal` and `shm` files might not exist anymore at this point - /// so we should only remove files which exist to prevent errors - guard FileManager.default.fileExists(atPath: url.path) else { return } + (UIApplication.shared.delegate as? AppDelegate)?.stopPollers() + } - try FileManager.default.removeItem(atPath: url.path) - } - - /// Current account data has been removed, we now need to copy over the - /// newly imported data - DispatchQueue.main.async { - modalActivityIndicator.setMessage( - "Moving imported data..." - ) - } - - try paths.forEach { path in - /// Need to ensure the destination directry - let targetPath: String = [ - dependencies[singleton: .fileManager].appSharedDataDirectoryPath, - path.replacingOccurrences(of: tmpUnencryptPath, with: "") - ].joined() // Already has '/' after 'appSharedDataDirectoryPath' + /// Need to shut everything down before the swap out the data to prevent crashes + dependencies[singleton: .jobRunner].stopAndClearPendingJobs() + dependencies.remove(cache: .libSession) + await dependencies[singleton: .network].suspendNetworkAccess() + dependencies[singleton: .storage].suspendDatabaseAccess() + try dependencies[singleton: .storage].closeDatabase() + LibSession.clearLoggers() - try FileManager.default.createDirectory( - atPath: URL(fileURLWithPath: targetPath) - .deletingLastPathComponent() - .path, - withIntermediateDirectories: true - ) - try FileManager.default.moveItem(atPath: path, toPath: targetPath) - } - - /// All of the main files have been moved across, we now need to replace the current database key with - /// the one included in the backup - try dependencies[singleton: .storage].replaceDatabaseKey(path: encKeyPath, password: password) - - /// The import process has completed so we need to restart the app - DispatchQueue.main.async { - self?.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Import Complete", - body: .text("The import completed successfully, Session must be reopened in order to complete the process."), - cancelTitle: "Exit", - cancelStyle: .alert_text, - onCancel: { _ in exit(0) } - ) + let deleteEnumerator: FileManager.DirectoryEnumerator? = FileManager.default.enumerator( + at: URL( + fileURLWithPath: dependencies[singleton: .fileManager].appSharedDataDirectoryPath ), - transitionType: .present + includingPropertiesForKeys: [.isRegularFileKey, .isHiddenKey] ) + let fileUrls: [URL] = (deleteEnumerator?.allObjects + .compactMap { $0 as? URL } + .filter { url -> Bool in + guard let resourceValues = try? url.resourceValues(forKeys: [.isHiddenKey]) else { + return true + } + + return (resourceValues.isHidden != true) + }) + .defaulting(to: []) + try fileUrls.forEach { url in + /// The database `wal` and `shm` files might not exist anymore at this point + /// so we should only remove files which exist to prevent errors + guard FileManager.default.fileExists(atPath: url.path) else { return } + + try FileManager.default.removeItem(atPath: url.path) + } + + /// Current account data has been removed, we now need to copy over the + /// newly imported data + DispatchQueue.main.async { + modalActivityIndicator.setMessage( + "Moving imported data..." + ) + } + + try paths.forEach { path in + /// Need to ensure the destination directry + let targetPath: String = [ + dependencies[singleton: .fileManager].appSharedDataDirectoryPath, + path.replacingOccurrences(of: tmpUnencryptPath, with: "") + ].joined() // Already has '/' after 'appSharedDataDirectoryPath' + + try FileManager.default.createDirectory( + atPath: URL(fileURLWithPath: targetPath) + .deletingLastPathComponent() + .path, + withIntermediateDirectories: true + ) + try FileManager.default.moveItem(atPath: path, toPath: targetPath) + } + + /// All of the main files have been moved across, we now need to replace the current database key with + /// the one included in the backup + try dependencies[singleton: .storage].replaceDatabaseKey(path: encKeyPath, password: password) + + /// The import process has completed so we need to restart the app + DispatchQueue.main.async { + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Import Complete", + body: .text("The import completed successfully, Session must be reopened in order to complete the process."), + cancelTitle: "Exit", + cancelStyle: .alert_text, + onCancel: { _ in exit(0) } + ) + ), + transitionType: .present + ) + } } - } - catch { - modalActivityIndicator.dismiss { - showError(error) + catch { + modalActivityIndicator.dismiss { + showError(error) + } } } } From 04ba13cd3985100f6a8d3a5c0863302d8e736eaa Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 10:25:56 +1000 Subject: [PATCH 21/59] Rework AppSetup process to be async/await --- Session.xcodeproj/project.pbxproj | 30 ++++++++----- SessionMessagingKit/Utilities/AppSetup.swift | 44 ++++++++++---------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 3a44ca63e2..4c587354e2 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -519,7 +519,6 @@ FD2272B12C33E337004D8A6C /* ProxiedContentDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22729D2C33E336004D8A6C /* ProxiedContentDownloader.swift */; }; FD2272B22C33E337004D8A6C /* PreparedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22729E2C33E336004D8A6C /* PreparedRequest.swift */; }; FD2272B32C33E337004D8A6C /* BatchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22729F2C33E336004D8A6C /* BatchRequest.swift */; }; - FD2272B42C33E337004D8A6C /* SwarmDrainBehaviour.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272A02C33E336004D8A6C /* SwarmDrainBehaviour.swift */; }; FD2272B52C33E337004D8A6C /* BatchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272A12C33E336004D8A6C /* BatchResponse.swift */; }; FD2272B72C33E337004D8A6C /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272A32C33E337004D8A6C /* Request.swift */; }; FD2272B92C33E337004D8A6C /* ResponseInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272A52C33E337004D8A6C /* ResponseInfo.swift */; }; @@ -535,7 +534,6 @@ FD2272D42C34ECE1004D8A6C /* BencodeEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272D32C34ECE1004D8A6C /* BencodeEncoder.swift */; }; FD2272D62C34ED6A004D8A6C /* RetryWithDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */; }; FD2272D82C34EDE7004D8A6C /* SnodeAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272D72C34EDE6004D8A6C /* SnodeAPIEndpoint.swift */; }; - FD2272DD2C34EFFA004D8A6C /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */; }; FD2272E02C3502BE004D8A6C /* Setting+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DF2C3502BE004D8A6C /* Setting+Theme.swift */; }; FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E52C351378004D8A6C /* SUIKImageFormat.swift */; }; FD2272EA2C351CA7004D8A6C /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E92C351CA7004D8A6C /* Threading.swift */; }; @@ -646,8 +644,8 @@ FD37EA1728AC5605003AE748 /* NotificationContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1628AC5605003AE748 /* NotificationContentViewModel.swift */; }; FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */; }; FD39352C28F382920084DADA /* VersionFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39352B28F382920084DADA /* VersionFooterView.swift */; }; - FD39353628F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */; }; FD3937082E4AD3FE00571F17 /* NoopDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3937072E4AD3F800571F17 /* NoopDependency.swift */; }; + FD39370A2E4B04E000571F17 /* SwarmDrainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3937092E4B04DB00571F17 /* SwarmDrainer.swift */; }; FD39370C2E4D7BCA00571F17 /* DocumentPickerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39370B2E4D7BBE00571F17 /* DocumentPickerHandler.swift */; }; FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; }; FD3C906727E416AF00CD579F /* BlindedIdLookupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */; }; @@ -985,6 +983,10 @@ FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CC27BC641200C60D73 /* Set+Utilities.swift */; }; FDC498B92AC15FE300EDD897 /* AppNotificationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC498B82AC15FE300EDD897 /* AppNotificationAction.swift */; }; FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC6D75F2862B3F600B04575 /* Dependencies.swift */; }; + FDCC22D02E52E46000C77B1A /* GroupAuthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCC22CF2E52E45A00C77B1A /* GroupAuthData.swift */; }; + FDCC22D22E56E0BC00C77B1A /* StreamLifecycleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCC22D12E56E0B600C77B1A /* StreamLifecycleManager.swift */; }; + FDCC22D42E5C0D9900C77B1A /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */; }; + FDCC22D62E5D2ADC00C77B1A /* CancellationAwareAsyncStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCC22D52E5D2AD700C77B1A /* CancellationAwareAsyncStream.swift */; }; FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */; }; FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */; }; FDD20C162A09E64A003898FB /* GetExpiriesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C152A09E64A003898FB /* GetExpiriesRequest.swift */; }; @@ -1894,7 +1896,6 @@ FD22729D2C33E336004D8A6C /* ProxiedContentDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxiedContentDownloader.swift; sourceTree = ""; }; FD22729E2C33E336004D8A6C /* PreparedRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreparedRequest.swift; sourceTree = ""; }; FD22729F2C33E336004D8A6C /* BatchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchRequest.swift; sourceTree = ""; }; - FD2272A02C33E336004D8A6C /* SwarmDrainBehaviour.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwarmDrainBehaviour.swift; sourceTree = ""; }; FD2272A12C33E336004D8A6C /* BatchResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchResponse.swift; sourceTree = ""; }; FD2272A32C33E337004D8A6C /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; FD2272A52C33E337004D8A6C /* ResponseInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseInfo.swift; sourceTree = ""; }; @@ -1978,10 +1979,10 @@ FD37EA1628AC5605003AE748 /* NotificationContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentViewModel.swift; sourceTree = ""; }; FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundViewModel.swift; sourceTree = ""; }; FD39352B28F382920084DADA /* VersionFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionFooterView.swift; sourceTree = ""; }; - FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = ""; }; + FD39353528F7C3390084DADA /* _010_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _010_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = ""; }; FD3937072E4AD3F800571F17 /* NoopDependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopDependency.swift; sourceTree = ""; }; + FD3937092E4B04DB00571F17 /* SwarmDrainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwarmDrainer.swift; sourceTree = ""; }; FD39370B2E4D7BBE00571F17 /* DocumentPickerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPickerHandler.swift; sourceTree = ""; }; - FD39353528F7C3390084DADA /* _010_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _010_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = ""; }; FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = ""; }; FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookupSpec.swift; sourceTree = ""; }; FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModel.swift; sourceTree = ""; }; @@ -2263,6 +2264,9 @@ FDC438CC27BC641200C60D73 /* Set+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+Utilities.swift"; sourceTree = ""; }; FDC498B82AC15FE300EDD897 /* AppNotificationAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNotificationAction.swift; sourceTree = ""; }; FDC6D75F2862B3F600B04575 /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = ""; }; + FDCC22CF2E52E45A00C77B1A /* GroupAuthData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupAuthData.swift; sourceTree = ""; }; + FDCC22D12E56E0B600C77B1A /* StreamLifecycleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamLifecycleManager.swift; sourceTree = ""; }; + FDCC22D52E5D2AD700C77B1A /* CancellationAwareAsyncStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellationAwareAsyncStream.swift; sourceTree = ""; }; FDCCC6E82ABA7402002BBEF5 /* EmojiGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiGenerator.swift; sourceTree = ""; }; FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGroupOnlyRequest.swift; sourceTree = ""; }; FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; @@ -3630,6 +3634,7 @@ C3BBE0B32554F0D30050F1E3 /* Utilities */ = { isa = PBXGroup; children = ( + FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */, 94B6BAF92E38454F00E718BB /* SessionProState.swift */, FD428B1E2B4B758B006D0888 /* AppReadiness.swift */, FDE5218D2E03A06700061B8E /* AttachmentManager.swift */, @@ -3794,7 +3799,6 @@ C3CA3B11255CF17200F4C6D4 /* Utilities */ = { isa = PBXGroup; children = ( - FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */, C38EF3DC255B6DF1007E1867 /* DirectionalPanGestureRecognizer.swift */, C38EF240255B6D67007E1867 /* UIView+OWS.swift */, FD71161D28D9772700B47552 /* UIViewController+OWS.swift */, @@ -4157,10 +4161,12 @@ FDE755042C9BB4ED002A2623 /* Bencode.swift */, FDE755032C9BB4ED002A2623 /* BencodeDecoder.swift */, FD2272D32C34ECE1004D8A6C /* BencodeEncoder.swift */, + FDCC22D52E5D2AD700C77B1A /* CancellationAwareAsyncStream.swift */, FDB11A5A2DD1900B00BEF49F /* CurrentValueAsyncStream.swift */, FD3FAB682AF1ADCA00DC5421 /* FileManager.swift */, FD6A38F02C2A66B100762359 /* KeychainStorage.swift */, FD78EA032DDEC3C000D55B50 /* MultiTaskManager.swift */, + FDCC22D12E56E0B600C77B1A /* StreamLifecycleManager.swift */, FD2272E92C351CA7004D8A6C /* Threading.swift */, FDAA16752AC28A3B00DDBF77 /* UserDefaultsType.swift */, ); @@ -4748,6 +4754,7 @@ FDC1BD642CFD6C44002CDC71 /* Types */ = { isa = PBXGroup; children = ( + FDCC22CF2E52E45A00C77B1A /* GroupAuthData.swift */, FDC1BD652CFD6C4E002CDC71 /* Config.swift */, FD78E9F52DDD43AB00D55B50 /* Mutation.swift */, FDB11A512DCC6AFF00BEF49F /* OpenGroupUrlInfo.swift */, @@ -5022,7 +5029,7 @@ FD2272A32C33E337004D8A6C /* Request.swift */, FDD23ADD2E44501B0057E853 /* RequestCategory.swift */, FD2272A52C33E337004D8A6C /* ResponseInfo.swift */, - FD2272A02C33E336004D8A6C /* SwarmDrainBehaviour.swift */, + FD3937092E4B04DB00571F17 /* SwarmDrainer.swift */, FD2272962C33E335004D8A6C /* UpdatableTimestamp.swift */, FD2272972C33E335004D8A6C /* ValidatableResponse.swift */, ); @@ -6136,7 +6143,6 @@ C38EF32E255B6DBF007E1867 /* ImageCache.swift in Sources */, C38EF3BA255B6DE7007E1867 /* ImageEditorItem.swift in Sources */, C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */, - FD2272DD2C34EFFA004D8A6C /* AppSetup.swift in Sources */, C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */, FDE754E72C9BB051002A2623 /* OWSViewController.swift in Sources */, C38EF38B255B6DD2007E1867 /* AttachmentPrepViewController.swift in Sources */, @@ -6184,6 +6190,7 @@ FDF848C029405C5A007DCAE5 /* ONSResolveResponse.swift in Sources */, FD2272AC2C33E337004D8A6C /* IPv4.swift in Sources */, FD2272D82C34EDE7004D8A6C /* SnodeAPIEndpoint.swift in Sources */, + FD39370A2E4B04E000571F17 /* SwarmDrainer.swift in Sources */, FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */, FDB5DAF32A96DD4F002C8721 /* PreparedRequest+Sending.swift in Sources */, FDF848C629405C5B007DCAE5 /* DeleteAllMessagesRequest.swift in Sources */, @@ -6237,7 +6244,6 @@ FD2272AA2C33E337004D8A6C /* UpdatableTimestamp.swift in Sources */, FD3765E92ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift in Sources */, FD5E93D22C12B0580038C25A /* AppVersionResponse.swift in Sources */, - FD2272B42C33E337004D8A6C /* SwarmDrainBehaviour.swift in Sources */, 941375BB2D5184C20058F244 /* HTTPHeader+SessionNetwork.swift in Sources */, FD2272AE2C33E337004D8A6C /* HTTPQueryParam.swift in Sources */, FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */, @@ -6253,6 +6259,7 @@ FD2272C72C34EAF5004D8A6C /* ColumnExpressible.swift in Sources */, FDB3486E2BE8457F00B716C2 /* BackgroundTaskManager.swift in Sources */, 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */, + FDCC22D62E5D2ADC00C77B1A /* CancellationAwareAsyncStream.swift in Sources */, FD428B192B4B576F006D0888 /* AppContext.swift in Sources */, FD2272D42C34ECE1004D8A6C /* BencodeEncoder.swift in Sources */, FDE755052C9BB4EE002A2623 /* BencodeDecoder.swift in Sources */, @@ -6327,6 +6334,7 @@ FDB3DA862E1E1F0E00148F8D /* TaskCancellation.swift in Sources */, FDE754CC2C9BAF37002A2623 /* MediaUtils.swift in Sources */, FDE754DE2C9BAF8A002A2623 /* Crypto+SessionUtilitiesKit.swift in Sources */, + FDCC22D22E56E0BC00C77B1A /* StreamLifecycleManager.swift in Sources */, FDF2220F281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift in Sources */, FD848B9A28442CE6000E298B /* StorageError.swift in Sources */, FDE755222C9BC1BA002A2623 /* LibSessionError.swift in Sources */, @@ -6503,6 +6511,7 @@ FDB5DADE2A95D847002C8721 /* GroupUpdatePromoteMessage.swift in Sources */, FD245C662850665900B966DD /* OpenGroupAPI.swift in Sources */, FD245C5B2850660500B966DD /* ReadReceipt.swift in Sources */, + FDCC22D42E5C0D9900C77B1A /* AppSetup.swift in Sources */, FD428B1F2B4B758B006D0888 /* AppReadiness.swift in Sources */, FD22726B2C32911C004D8A6C /* SendReadReceiptsJob.swift in Sources */, B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */, @@ -6566,6 +6575,7 @@ FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */, FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */, C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */, + FDCC22D02E52E46000C77B1A /* GroupAuthData.swift in Sources */, FD981BC42DC304E600564172 /* MessageDeduplication.swift in Sources */, FDF0B74B28061F7A004C14C5 /* InteractionAttachment.swift in Sources */, FD09796E27FA6D0000936362 /* Contact.swift in Sources */, diff --git a/SessionMessagingKit/Utilities/AppSetup.swift b/SessionMessagingKit/Utilities/AppSetup.swift index 4535c28cd4..3a97f2fd02 100644 --- a/SessionMessagingKit/Utilities/AppSetup.swift +++ b/SessionMessagingKit/Utilities/AppSetup.swift @@ -1,18 +1,14 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import AVFoundation import GRDB import SessionUIKit -import SessionMessagingKit +import SessionNetworkingKit import SessionUtilitiesKit public enum AppSetup { - public static func performSetup( - migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, - using dependencies: Dependencies - ) async { - var backgroundTask: SessionBackgroundTask? = SessionBackgroundTask(label: #function, using: dependencies) - + public static func performSetup(using dependencies: Dependencies) async throws { /// Order matters here. /// /// All of these "singletons" should have any dependencies used in their @@ -33,23 +29,27 @@ public enum AppSetup { windowManager: OWSWindowManager(default: ()) ) - try await dependencies[singleton: .storage].perform( - migrations: SNMessagingKit.migrations, - onProgressUpdate: { [weak self] progress, minEstimatedTotalTime in - self?.loadingViewController?.updateProgress( - progress: progress, - minEstimatedTotalTime: minEstimatedTotalTime - ) - } + dependencies.warm(cache: .appVersion) + dependencies.warm(singleton: .network) + + /// Configure the different targets + SNUtilitiesKit.configure( + networkMaxFileSize: Network.maxFileSize, + maxValidImageDimention: ImageDataManager.DataSource.maxValidDimension, + using: dependencies ) + SNMessagingKit.configure(using: dependencies) } - public static func setup( - backgroundTask: SessionBackgroundTask? = nil, - migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, - using dependencies: Dependencies - ) async { - + public static func performDatabaseMigrations( + using dependencies: Dependencies, + migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil + ) async throws { + /// Run the migrations + try await dependencies[singleton: .storage].perform( + migrations: SNMessagingKit.migrations, + onProgressUpdate: migrationProgressChanged + ) } public static func postMigrationSetup(using dependencies: Dependencies) async throws { @@ -60,7 +60,7 @@ public enum AppSetup { dumpSessionIds: Set, unreadCount: Int? ) - let userInfo: UserInfo? = try await dependencies[singleton: .storage].readAsync { db -> UserInfo? in + let userInfo: UserInfo? = try? await dependencies[singleton: .storage].readAsync { db -> UserInfo? in guard let ed25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else { return nil } From d44704bed9fa06054ba5e0a2d3e70a542c6717d7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 10:34:31 +1000 Subject: [PATCH 22/59] Refactored the CheckForAppUpdatesJob to use async/await --- .../Jobs/CheckForAppUpdatesJob.swift | 47 +++++++++---------- SessionNetworkingKit/Types/Network.swift | 6 +++ 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift b/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift index 45022e0421..15472207ea 100644 --- a/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift +++ b/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift @@ -47,32 +47,29 @@ public enum CheckForAppUpdatesJob: JobExecutor { return deferred(updatedJob) } - dependencies[singleton: .network] - .checkClientVersion(ed25519SecretKey: dependencies[cache: .general].ed25519SecretKey) - .subscribe(on: scheduler, using: dependencies) - .receive(on: scheduler, using: dependencies) - .sinkUntilComplete( - receiveCompletion: { _ in - var updatedJob: Job = job.with( - failureCount: 0, - nextRunTimestamp: (dependencies.dateNow.timeIntervalSince1970 + updateCheckFrequency) - ) - - dependencies[singleton: .storage].write { db in - try updatedJob.save(db) - } + Task { [dependencies] in + let versionInfo: AppVersionResponse? = try? await dependencies[singleton: .network] + .checkClientVersion(ed25519SecretKey: dependencies[cache: .general].ed25519SecretKey) + + switch (versionInfo, versionInfo?.prerelease) { + case (.none, _): break + case (.some(let info), .none): + Log.info(.cat, "Latest version: \(info.version) (Current: \(dependencies[cache: .appVersion].versionInfo))") - success(updatedJob, false) - }, - receiveValue: { _, versionInfo in - switch versionInfo.prerelease { - case .none: - Log.info(.cat, "Latest version: \(versionInfo.version) (Current: \(dependencies[cache: .appVersion].versionInfo))") - - case .some(let prerelease): - Log.info(.cat, "Latest version: \(versionInfo.version), pre-release version: \(prerelease.version) (Current: \(dependencies[cache: .appVersion].versionInfo))") - } - } + case (.some(let info), .some(let prerelease)): + Log.info(.cat, "Latest version: \(info.version), pre-release version: \(prerelease.version) (Current: \(dependencies[cache: .appVersion].versionInfo))") + } + + var updatedJob: Job = job.with( + failureCount: 0, + nextRunTimestamp: (dependencies.dateNow.timeIntervalSince1970 + updateCheckFrequency) ) + + try? await dependencies[singleton: .storage].writeAsync { db in + try updatedJob.save(db) + } + + success(updatedJob, false) + } } } diff --git a/SessionNetworkingKit/Types/Network.swift b/SessionNetworkingKit/Types/Network.swift index 8124825278..92e1b44cc6 100644 --- a/SessionNetworkingKit/Types/Network.swift +++ b/SessionNetworkingKit/Types/Network.swift @@ -54,6 +54,12 @@ public protocol NetworkType { func clearCache() async } +public extension NetworkType { + func checkClientVersion(ed25519SecretKey: [UInt8]) async throws -> AppVersionResponse { + return try await checkClientVersion(ed25519SecretKey: ed25519SecretKey).value + } +} + /// We manually handle thread-safety using the `NSLock` so can ensure this is `Sendable` public final class NetworkSyncState: @unchecked Sendable { private let lock = NSLock() From da6c4d44fe86b392acbe18b4200979857fcf1ecf Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 10:34:54 +1000 Subject: [PATCH 23/59] Updated usage of the getSessionId function --- .../Closed Groups/EditGroupViewModel.swift | 45 ++++++++------- .../New Conversation/NewMessageScreen.swift | 57 ++++++++++--------- 2 files changed, 52 insertions(+), 50 deletions(-) diff --git a/Session/Closed Groups/EditGroupViewModel.swift b/Session/Closed Groups/EditGroupViewModel.swift index 8309d70282..1ef23ab5ad 100644 --- a/Session/Closed Groups/EditGroupViewModel.swift +++ b/Session/Closed Groups/EditGroupViewModel.swift @@ -533,27 +533,15 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl case (.some(let inviteByIdValue), _): // This could be an ONS name - let viewController = ModalActivityIndicatorViewController() { modalActivityIndicator in - SnodeAPI - .getSessionID(for: inviteByIdValue, using: dependencies) - .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) - .receive(on: DispatchQueue.main, using: dependencies) - .sinkUntilComplete( - receiveCompletion: { result in - switch result { - case .finished: break - case .failure(let error): - modalActivityIndicator.dismiss { - switch error { - case SnodeAPIError.onsNotFound: - return showError("onsErrorNotRecognized".localized()) - default: - return showError("onsErrorUnableToSearch".localized()) - } - } - } - }, - receiveValue: { sessionIdHexString in + let viewController = ModalActivityIndicatorViewController() { [weak self, dependencies] modalActivityIndicator in + Task { [weak self, modalActivityIndicator, dependencies] in + do { + let sessionIdHexString: String = try await SnodeAPI.getSessionID( + for: inviteByIdValue, + using: dependencies + ) + + await MainActor.run { guard !currentMemberIds.contains(sessionIdHexString) else { // FIXME: Localise this return showError("This Account ID or ONS belongs to an existing member") @@ -568,7 +556,20 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl } } } - ) + } + catch { + await MainActor.run { + modalActivityIndicator.dismiss { + switch error { + case SnodeAPIError.onsNotFound: + return showError("onsErrorNotRecognized".localized()) + default: + return showError("onsErrorUnableToSearch".localized()) + } + } + } + } + } } self?.transitionToScreen(viewController, transitionType: .present) } diff --git a/Session/Home/New Conversation/NewMessageScreen.swift b/Session/Home/New Conversation/NewMessageScreen.swift index 725fa06ed8..901d0d9010 100644 --- a/Session/Home/New Conversation/NewMessageScreen.swift +++ b/Session/Home/New Conversation/NewMessageScreen.swift @@ -86,40 +86,41 @@ struct NewMessageScreen: View { // This could be an ONS name ModalActivityIndicatorViewController - .present(fromViewController: self.host.controller?.navigationController!, canCancel: false) { modalActivityIndicator in - SnodeAPI - .getSessionID(for: accountIdOrONS, using: dependencies) - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .receive(on: DispatchQueue.main) - .sinkUntilComplete( - receiveCompletion: { result in - switch result { - case .finished: break - case .failure(let error): - modalActivityIndicator.dismiss { - let message: String = { - switch error { - case SnodeAPIError.onsNotFound: - return "onsErrorNotRecognized".localized() - default: - return "onsErrorUnableToSearch".localized() - } - }() - - errorString = message - } + .present(fromViewController: self.host.controller?.navigationController!, canCancel: false) { [dependencies] modalActivityIndicator in + Task { [accountIdOrONS, dependencies] in + do { + let sessionIdHexString: String = try await SnodeAPI.getSessionID( + for: accountIdOrONS, + using: dependencies + ) + + await MainActor.run { + modalActivityIndicator.dismiss { + self.startNewDM(with: sessionIdHexString) + } } - }, - receiveValue: { sessionId in - modalActivityIndicator.dismiss { - self.startNewDM(with: sessionId) + } + catch { + await MainActor.run { + modalActivityIndicator.dismiss { + let message: String = { + switch error { + case SnodeAPIError.onsNotFound: + return "onsErrorNotRecognized".localized() + default: + return "onsErrorUnableToSearch".localized() + } + }() + + self.errorString = message + } } } - ) + } } } - private func startNewDM(with sessionId: String) { + @MainActor private func startNewDM(with sessionId: String) { dependencies[singleton: .app].presentConversationCreatingIfNeeded( for: sessionId, variant: .contact, From 922218f7151b1c2c7c389b6c79ac521583d97afe Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 10:35:10 +1000 Subject: [PATCH 24/59] Odds and ends --- .../LibSession+SessionMessagingKit.swift | 14 +++++--------- .../LibSession/Types/Mutation.swift | 1 + 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 315d7dca76..af8cf93315 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -206,11 +206,11 @@ public extension LibSession { // MARK: - State Management - public func loadState(_ db: ObservingDatabase, requestId: String?) { + public func loadState(_ db: ObservingDatabase) { // Ensure we have the ed25519 key and that we haven't already loaded the state before // we continue guard configStore.isEmpty else { - return Log.warn(.libSession, "Ignoring loadState\(requestId.map { " for \($0)" } ?? "") due to existing state") + return Log.warn(.libSession, "Ignoring loadState due to existing state") } /// Retrieve the existing dumps from the database @@ -282,7 +282,7 @@ public extension LibSession { ) } - Log.info(.libSession, "Completed loadState\(requestId.map { " for \($0)" } ?? "")") + Log.info(.libSession, "Completed loadState") } public func loadDefaultStateFor( @@ -950,7 +950,7 @@ public protocol LibSessionCacheType: LibSessionImmutableCacheType, MutableCacheT // MARK: - State Management - func loadState(_ db: ObservingDatabase, requestId: String?) + func loadState(_ db: ObservingDatabase) func loadDefaultStateFor( variant: ConfigDump.Variant, sessionId: SessionId, @@ -1169,10 +1169,6 @@ public extension LibSessionCacheType { return try perform(for: variant, sessionId: userSessionId, change: { _ in try change() }) } - func loadState(_ db: ObservingDatabase) { - loadState(db, requestId: nil) - } - func addEvent(key: ObservableKey, value: AnyHashable?) { addEvent(ObservedEvent(key: key, value: value)) } @@ -1212,7 +1208,7 @@ private final class NoopLibSessionCache: LibSessionCacheType, NoopDependency { // MARK: - State Management - func loadState(_ db: ObservingDatabase, requestId: String?) {} + func loadState(_ db: ObservingDatabase) {} func loadDefaultStateFor( variant: ConfigDump.Variant, sessionId: SessionId, diff --git a/SessionMessagingKit/LibSession/Types/Mutation.swift b/SessionMessagingKit/LibSession/Types/Mutation.swift index 3929d526d1..b63ea0034d 100644 --- a/SessionMessagingKit/LibSession/Types/Mutation.swift +++ b/SessionMessagingKit/LibSession/Types/Mutation.swift @@ -2,6 +2,7 @@ import Foundation import GRDB +import SessionNetworkingKit import SessionUtilitiesKit public extension LibSession { From ecf3e82e55c8bef9a740ce06a7261024f9223aff Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 26 Aug 2025 13:15:05 +1000 Subject: [PATCH 25/59] Refactored Onboarding to be an actor and use async/await --- Session.xcodeproj/project.pbxproj | 4 + Session/Home/HomeVC.swift | 4 +- Session/Home/HomeViewModel.swift | 2 +- Session/Meta/AppDelegate.swift | 143 +++-- Session/Meta/SessionApp.swift | 4 +- Session/Notifications/SyncPushTokensJob.swift | 2 +- Session/Onboarding/DisplayNameScreen.swift | 68 +- Session/Onboarding/LandingScreen.swift | 67 +- Session/Onboarding/LoadAccountScreen.swift | 44 +- Session/Onboarding/LoadingScreen.swift | 81 ++- Session/Onboarding/Onboarding.swift | 602 +++++++++--------- Session/Onboarding/PNModeScreen.swift | 76 +-- .../Settings/DeveloperSettingsViewModel.swift | 34 +- Session/Settings/NukeDataModal.swift | 6 +- .../Database/Models/SessionThread.swift | 6 +- .../Jobs/ConfigurationSyncJob.swift | 4 +- .../Config Handling/LibSession+Shared.swift | 4 +- .../Pollers/CommunityPoller.swift | 1 + .../Pollers/GroupPoller.swift | 2 +- .../Pollers/PollerType.swift | 2 +- SessionNetworkingKit/Types/Network.swift | 1 + .../Utilities/AsyncSequence+Utilities.swift | 20 + .../Utilities/AsyncStream+Utilities.swift | 13 + 23 files changed, 621 insertions(+), 569 deletions(-) create mode 100644 SessionUtilitiesKit/Utilities/AsyncStream+Utilities.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 4c587354e2..c5467a3cef 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -987,6 +987,7 @@ FDCC22D22E56E0BC00C77B1A /* StreamLifecycleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCC22D12E56E0B600C77B1A /* StreamLifecycleManager.swift */; }; FDCC22D42E5C0D9900C77B1A /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */; }; FDCC22D62E5D2ADC00C77B1A /* CancellationAwareAsyncStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCC22D52E5D2AD700C77B1A /* CancellationAwareAsyncStream.swift */; }; + FDCC22D82E5D3C1400C77B1A /* AsyncStream+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCC22D72E5D3C1000C77B1A /* AsyncStream+Utilities.swift */; }; FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */; }; FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */; }; FDD20C162A09E64A003898FB /* GetExpiriesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C152A09E64A003898FB /* GetExpiriesRequest.swift */; }; @@ -2267,6 +2268,7 @@ FDCC22CF2E52E45A00C77B1A /* GroupAuthData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupAuthData.swift; sourceTree = ""; }; FDCC22D12E56E0B600C77B1A /* StreamLifecycleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamLifecycleManager.swift; sourceTree = ""; }; FDCC22D52E5D2AD700C77B1A /* CancellationAwareAsyncStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellationAwareAsyncStream.swift; sourceTree = ""; }; + FDCC22D72E5D3C1000C77B1A /* AsyncStream+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncStream+Utilities.swift"; sourceTree = ""; }; FDCCC6E82ABA7402002BBEF5 /* EmojiGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiGenerator.swift; sourceTree = ""; }; FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGroupOnlyRequest.swift; sourceTree = ""; }; FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; @@ -3955,6 +3957,7 @@ 94C58AC82D2E036E00609195 /* Permissions.swift */, FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */, FD78EA052DDEC8F100D55B50 /* AsyncSequence+Utilities.swift */, + FDCC22D72E5D3C1000C77B1A /* AsyncStream+Utilities.swift */, FD7443452D07CA9F00862443 /* CGFloat+Utilities.swift */, FD7443462D07CA9F00862443 /* CGPoint+Utilities.swift */, FD7443472D07CA9F00862443 /* CGRect+Utilities.swift */, @@ -6325,6 +6328,7 @@ FDE754DD2C9BAF8A002A2623 /* Mnemonic.swift in Sources */, FD52CB652E13B6E900A4DA70 /* ObservationBuilder.swift in Sources */, FDBEE52E2B6A18B900C143A0 /* UserDefaultsConfig.swift in Sources */, + FDCC22D82E5D3C1400C77B1A /* AsyncStream+Utilities.swift in Sources */, FD78EA042DDEC3C500D55B50 /* MultiTaskManager.swift in Sources */, FD78EA062DDEC8F600D55B50 /* AsyncSequence+Utilities.swift in Sources */, FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */, diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 87ceafc7e6..afcc1eabd0 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -340,7 +340,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate, viewModel.dependencies[singleton: .appContext].isMainAppAndActive { - appDelegate.startPollersIfNeeded() + Task { await appDelegate.startPollersIfNeeded() } } // Onion request path countries cache @@ -769,7 +769,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi self.navigationController?.setViewControllers([ self, searchController ], animated: true) } - @objc private func createNewConversation() { + @MainActor @objc private func createNewConversation() { viewModel.dependencies[singleton: .app].createNewConversation() } diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 71f81abccc..af188fc9b4 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -159,7 +159,7 @@ public class HomeViewModel: NavigatableStateHolder { isInitialQuery: Bool, using dependencies: Dependencies ) async -> State { - let startedAsNewUser: Bool = (dependencies[cache: .onboarding].initialFlow == .register) + let startedAsNewUser: Bool = (await dependencies[singleton: .onboarding].initialFlow == .register) var userProfile: Profile = previousState.userProfile var serviceNetwork: ServiceNetwork = previousState.serviceNetwork var forceOffline: Bool = previousState.forceOffline diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index bb5c0a52b2..b5ecabe772 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -133,7 +133,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } try await AppSetup.postMigrationSetup(using: dependencies) - self?.completePostMigrationSetup( + await self?.completePostMigrationSetup( calledFrom: .enterForeground(initialLaunchFailed: initialLaunchFailed) ) } @@ -156,7 +156,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // NOTE: Fix an edge case where user taps on the callkit notification // but answers the call on another device - stopPollers(shouldStopUserPoller: !self.hasCallOngoing()) + Task(priority: .userInitiated) { [weak self] in + await self?.stopPollers(shouldStopUserPoller: self?.hasCallOngoing() != true) + } // Stop all jobs except for message sending and when completed suspend the database dependencies[singleton: .jobRunner].stopAndClearPendingJobs(exceptForVariant: .messageSend) { [dependencies] neededBackgroundProcessing in @@ -179,7 +181,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD Log.info(.cat, "applicationWillTerminate.") Log.flush() - stopPollers() + Task(priority: .userInitiated) { [weak self] in + await self?.stopPollers() + } } func applicationDidBecomeActive(_ application: UIApplication) { @@ -193,9 +197,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD Task { dependencies[singleton: .storage].resumeDatabaseAccess() await dependencies[singleton: .network].resumeNetworkAccess() + await ensureRootViewController(calledFrom: .didBecomeActive) } - - ensureRootViewController(calledFrom: .didBecomeActive) dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [weak self] in self?.handleActivation() @@ -433,7 +436,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } /// Now that the theme settings have been applied we can complete the migrations - self.completePostMigrationSetup(calledFrom: .finishLaunching) + await self.completePostMigrationSetup(calledFrom: .finishLaunching) } catch { await MainActor.run { [weak self] in @@ -449,7 +452,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD if backgroundTask != nil { backgroundTask = nil } } - private func completePostMigrationSetup(calledFrom lifecycleMethod: LifecycleMethod) { + private func completePostMigrationSetup(calledFrom lifecycleMethod: LifecycleMethod) async { Log.info(.cat, "Migrations completed, performing setup and ensuring rootViewController") dependencies[singleton: .jobRunner].setExecutor(SyncPushTokensJob.self, for: .syncPushTokens) @@ -471,12 +474,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } // Setup the UI if needed, then trigger any post-UI setup actions - self.ensureRootViewController(calledFrom: lifecycleMethod) { [weak self, dependencies] success in + await self.ensureRootViewController(calledFrom: lifecycleMethod) { [weak self, dependencies] success in // If we didn't successfully ensure the rootViewController then don't continue as // the user is in an invalid state (and should have already been shown a modal) guard success else { return } - Log.info(.cat, "RootViewController ready for state: \(dependencies[cache: .onboarding].state), readying remaining processes") + let onboardingState: Onboarding.State = await dependencies[singleton: .onboarding].state + .first(defaultValue: .unknown) + Log.info(.cat, "RootViewController ready for state: \(onboardingState), readying remaining processes") self?.initialLaunchFailed = false /// Trigger any launch-specific jobs and start the JobRunner with `jobRunner.appDidFinishLaunching(using:)` some @@ -505,7 +510,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD dependencies.mutate(cache: .appVersion) { $0.mainAppLaunchDidComplete() } /// App won't be ready for extensions and no need to enqueue a config sync unless we successfully completed startup - dependencies[singleton: .storage].writeAsync { db in + try? await dependencies[singleton: .storage].writeAsync { db in /// Increment the launch count (guaranteed to change which results in the write actually doing something and /// outputting and error if the DB is suspended) db[.activeCounter] = ((db[.activeCounter] ?? 0) + 1) @@ -514,7 +519,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// ensure that any pending local state gets pushed and any jobs waiting for a successful config sync are run /// /// **Note:** We only want to do this if the app is active, and the user has completed the Onboarding process - if dependencies[singleton: .appContext].isAppForegroundAndActive && dependencies[cache: .onboarding].state == .completed { + if dependencies[singleton: .appContext].isAppForegroundAndActive && onboardingState == .completed { dependencies.mutate(cache: .libSession) { $0.syncAllPendingPushes(db) } } } @@ -608,7 +613,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } try await AppSetup.postMigrationSetup(using: dependencies) - self?.completePostMigrationSetup(calledFrom: lifecycleMethod) + await self?.completePostMigrationSetup(calledFrom: lifecycleMethod) } catch { await MainActor.run { @@ -689,18 +694,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// There is a warning which can happen on launch because the Database read can be blocked by another database operation /// which could result in this blocking the main thread, as a result we want to check the identity exists on a background thread /// and then return to the main thread only when required - DispatchQueue.global(qos: .default).async { [weak self, dependencies] in - guard dependencies[cache: .onboarding].state == .completed else { return } + Task(priority: .medium) { [weak self, dependencies] in + guard await dependencies[singleton: .onboarding].state.first() == .completed else { return } self?.enableBackgroundRefreshIfNecessary() dependencies[singleton: .jobRunner].appDidBecomeActive() - self?.startPollersIfNeeded() + await self?.startPollersIfNeeded() + /// Fetch the Session Network info in the background Task { await dependencies[singleton: .sessionNetworkApiClient].fetchInfoInBackground() } if dependencies[singleton: .appContext].isMainApp { - DispatchQueue.main.async { + await MainActor.run { self?.handleAppActivatedWithOngoingCallIfNeeded() } } @@ -709,8 +715,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private func ensureRootViewController( calledFrom lifecycleMethod: LifecycleMethod, - onComplete: @escaping ((Bool) -> Void) = { _ in } - ) { + onComplete: @escaping ((Bool) async -> Void) = { _ in } + ) async { let hasInitialRootViewController: Bool = self.hasInitialRootViewController // Always call the completion block and indicate whether we successfully created the UI @@ -722,18 +728,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD lifecycleMethod == .enterForeground(initialLaunchFailed: true) ) && !hasInitialRootViewController - else { return DispatchQueue.main.async { onComplete(hasInitialRootViewController) } } + else { return await onComplete(hasInitialRootViewController) } /// Start a timeout for the creation of the rootViewController setup process (if it takes too long then we want to give the user /// the option to export their logs) - let longRunningStartupTimoutCancellable: AnyCancellable = Just(()) - .delay(for: .seconds(AppDelegate.maxRootViewControllerInitialQueryDuration), scheduler: DispatchQueue.main) - .sink( - receiveCompletion: { _ in }, - receiveValue: { [weak self] _ in - self?.showFailedStartupAlert(calledFrom: lifecycleMethod, error: .startupTimeout) - } - ) + let startupTimeoutTask: Task = Task { [weak self] in + try? await Task.sleep(for: .seconds(Int(AppDelegate.maxRootViewControllerInitialQueryDuration))) + guard !Task.isCancelled else { return } + + await MainActor.run { [weak self] in + self?.showFailedStartupAlert(calledFrom: lifecycleMethod, error: .startupTimeout) + } + } // All logic which needs to run after the 'rootViewController' is created let rootViewControllerSetupComplete: (UIViewController) -> Void = { [weak self, dependencies] rootViewController in @@ -786,52 +792,57 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } // Setup is completed so run any post-setup tasks - onComplete(true) + Task(priority: .high) { await onComplete(true) } } // Navigate to the approriate screen depending on the onboarding state - dependencies.warm(cache: .onboarding) + try? await dependencies[singleton: .onboarding].loadInitialState() - switch dependencies[cache: .onboarding].state { - case .noUser, .noUserInvalidKeyPair, .noUserInvalidSeedGeneration: - if dependencies[cache: .onboarding].state == .noUserInvalidKeyPair { + switch await dependencies[singleton: .onboarding].state.first() { + case .none, .unknown, .noUser, .noUserInvalidKeyPair, .noUserInvalidSeedGeneration: + if await dependencies[singleton: .onboarding].state.first() == .noUserInvalidKeyPair { Log.critical(.cat, "Failed to load credentials for existing user, generated a new identity.") } - else if dependencies[cache: .onboarding].state == .noUserInvalidSeedGeneration { + else if await dependencies[singleton: .onboarding].state.first() == .noUserInvalidSeedGeneration { Log.critical(.cat, "Failed to create an initial identity for a potentially new user.") } - DispatchQueue.main.async { [dependencies] in + await MainActor.run { [dependencies] in /// Once the onboarding process is complete we need to call `handleActivation` let viewController = SessionHostingViewController(rootView: LandingScreen(using: dependencies) { [weak self] in self?.handleActivation() }) viewController.setUpNavBarSessionIcon() - longRunningStartupTimoutCancellable.cancel() + startupTimeoutTask.cancel() rootViewControllerSetupComplete(viewController) } case .missingName: - DispatchQueue.main.async { [dependencies] in - let viewController = SessionHostingViewController(rootView: DisplayNameScreen(using: dependencies)) + let initialFlow: Onboarding.Flow = await dependencies[singleton: .onboarding].initialFlow + + await MainActor.run { [dependencies] in + let viewController = SessionHostingViewController( + rootView: DisplayNameScreen(flow: initialFlow, using: dependencies) + ) viewController.setUpNavBarSessionIcon() - longRunningStartupTimoutCancellable.cancel() + startupTimeoutTask.cancel() rootViewControllerSetupComplete(viewController) /// Once the onboarding process is complete we need to call `handleActivation` - dependencies[cache: .onboarding].onboardingCompletePublisher - .subscribe(on: DispatchQueue.main, using: dependencies) - .receive(on: DispatchQueue.main, using: dependencies) - .sinkUntilComplete(receiveCompletion: { [weak self] _ in self?.handleActivation() }) + Task(priority: .userInitiated) { [weak self] in + if let _ = await dependencies[singleton: .onboarding].state.first(where: { $0 == .completed }) { + self?.handleActivation() + } + } } case .completed: - DispatchQueue.main.async { [dependencies] in + await MainActor.run { [dependencies] in /// We want to start observing the changes for the 'HomeVC' and want to wait until we actually get data back before we /// continue as we don't want to show a blank home screen let viewController: HomeVC = HomeVC(using: dependencies) viewController.afterInitialConversationsLoaded { - longRunningStartupTimoutCancellable.cancel() + startupTimeoutTask.cancel() rootViewControllerSetupComplete(viewController) } } @@ -887,10 +898,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [dependencies] in - guard dependencies[cache: .onboarding].state == .completed else { return } - - dependencies[singleton: .app].createNewConversation() - completionHandler(true) + Task(priority: .userInitiated) { + guard await dependencies[singleton: .onboarding].state.first() == .completed else { return } + + await MainActor.run { [dependencies] in + dependencies[singleton: .app].createNewConversation() + } + completionHandler(true) + } } } @@ -972,29 +987,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Polling - public func startPollersIfNeeded() { - guard dependencies[cache: .onboarding].state == .completed else { return } - - /// Start the pollers on a background thread so that any database queries they need to run don't - /// block the main thread - Task(priority: .userInitiated) { [dependencies] in - await dependencies[singleton: .currentUserPoller].startIfNeeded() - await dependencies[singleton: .groupPollerManager].startAllPollers() - await dependencies[singleton: .communityPollerManager].startAllPollers() - } + public func startPollersIfNeeded() async { + guard await dependencies[singleton: .onboarding].state.first() == .completed else { return } + + await dependencies[singleton: .currentUserPoller].startIfNeeded() + await dependencies[singleton: .groupPollerManager].startAllPollers() + await dependencies[singleton: .communityPollerManager].startAllPollers() } - public func stopPollers(shouldStopUserPoller: Bool = true) { - guard dependencies[cache: .onboarding].state == .completed else { return } + public func stopPollers(shouldStopUserPoller: Bool = true) async { + guard await dependencies[singleton: .onboarding].state.first() == .completed else { return } - Task(priority: .userInitiated) { [dependencies] in - if shouldStopUserPoller { - await dependencies[singleton: .currentUserPoller].stop() - } - - await dependencies[singleton: .groupPollerManager].stopAndRemoveAllPollers() - await dependencies[singleton: .communityPollerManager].stopAndRemoveAllPollers() + if shouldStopUserPoller { + await dependencies[singleton: .currentUserPoller].stop() } + + await dependencies[singleton: .groupPollerManager].stopAndRemoveAllPollers() + await dependencies[singleton: .communityPollerManager].stopAndRemoveAllPollers() } // MARK: - App Link diff --git a/Session/Meta/SessionApp.swift b/Session/Meta/SessionApp.swift index 50d31a602f..451eab04ce 100644 --- a/Session/Meta/SessionApp.swift +++ b/Session/Meta/SessionApp.swift @@ -108,7 +108,7 @@ public class SessionApp: SessionAppType { ) } - public func createNewConversation() { + @MainActor public func createNewConversation() { guard let homeViewController: HomeVC = self.homeViewController else { return } let viewController = SessionHostingViewController( @@ -249,7 +249,7 @@ public protocol SessionAppType { dismissing presentingViewController: UIViewController?, animated: Bool ) - func createNewConversation() + @MainActor func createNewConversation() func resetData(onReset: (() async -> ())) async func showPromotedScreen() } diff --git a/Session/Notifications/SyncPushTokensJob.swift b/Session/Notifications/SyncPushTokensJob.swift index 684d27f463..8d045749ed 100644 --- a/Session/Notifications/SyncPushTokensJob.swift +++ b/Session/Notifications/SyncPushTokensJob.swift @@ -34,7 +34,7 @@ public enum SyncPushTokensJob: JobExecutor { guard dependencies[defaults: .appGroup, key: .isMainAppActive] else { return deferred(job) // Don't need to do anything if it's not the main app } - guard dependencies[cache: .onboarding].state == .completed else { + guard dependencies[singleton: .onboarding].syncState.state == .completed else { Log.info(.syncPushTokensJob, "Deferred due to incomplete registration") return deferred(job) } diff --git a/Session/Onboarding/DisplayNameScreen.swift b/Session/Onboarding/DisplayNameScreen.swift index 78e98ae7c7..9af617eefc 100644 --- a/Session/Onboarding/DisplayNameScreen.swift +++ b/Session/Onboarding/DisplayNameScreen.swift @@ -13,9 +13,11 @@ struct DisplayNameScreen: View { @State private var error: String? = nil private let dependencies: Dependencies + private let initialFlow: Onboarding.Flow - public init(using dependencies: Dependencies) { + public init(flow: Onboarding.Flow, using dependencies: Dependencies) { self.dependencies = dependencies + self.initialFlow = flow } var body: some View { @@ -28,7 +30,7 @@ struct DisplayNameScreen: View { ) { Spacer(minLength: 0) - let title: String = (dependencies[cache: .onboarding].initialFlow == .register ? + let title: String = (initialFlow == .register ? "displayNamePick".localized() : "displayNameNew".localized() ) @@ -40,7 +42,7 @@ struct DisplayNameScreen: View { Spacer(minLength: 0) .frame(maxHeight: 2 * Values.mediumSpacing) - let explanation: String = (dependencies[cache: .onboarding].initialFlow == .register ? + let explanation: String = (initialFlow == .register ? "displayNameDescription".localized() : "displayNameErrorNew".localized() ) @@ -127,46 +129,54 @@ struct DisplayNameScreen: View { return } - // Store the new name in the onboarding cache - dependencies.mutate(cache: .onboarding) { $0.setDisplayName(displayName) } - - // If we are not in the registration flow then we are finished and should go straight - // to the home screen - guard dependencies[cache: .onboarding].initialFlow == .register else { - return dependencies.mutate(cache: .onboarding) { [dependencies] onboarding in + Task(priority: .userInitiated) { + // Store the new name in the onboarding cache + await dependencies[singleton: .onboarding].setDisplayName(displayName) + + // If we are not in the registration flow then we are finished and should go straight + // to the home screen + guard initialFlow == .register else { // If the `initialFlow` is `none` then it means the user is just providing a missing displayName // and so shouldn't change the APNS setting, otherwise we should base it on the users selection // during the onboarding process - let shouldSyncPushTokens: Bool = (onboarding.initialFlow != .none && onboarding.useAPNS) - - onboarding.completeRegistration { - // Trigger the 'SyncPushTokensJob' directly as we don't want to wait for paths to build - // before requesting the permission from the user - if shouldSyncPushTokens { - SyncPushTokensJob - .run(uploadOnlyIfStale: false, using: dependencies) - .sinkUntilComplete() - } + let shouldSyncPushTokens: Bool = await { + guard initialFlow != .none else { return false } - // Go to the home screen + return await dependencies[singleton: .onboarding].useAPNS + }() + + await dependencies[singleton: .onboarding].completeRegistration() + + // Trigger the 'SyncPushTokensJob' directly as we don't want to wait for paths to build + // before requesting the permission from the user + if shouldSyncPushTokens { + SyncPushTokensJob + .run(uploadOnlyIfStale: false, using: dependencies) + .sinkUntilComplete() + } + + // Go to the home screen + return await MainActor.run { let homeVC: HomeVC = HomeVC(using: dependencies) dependencies[singleton: .app].setHomeViewController(homeVC) self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) } } + + // Need to get the PN mode if registering + await MainActor.run { + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: PNModeScreen(using: dependencies) + ) + viewController.setUpNavBarSessionIcon() + self.host.controller?.navigationController?.pushViewController(viewController, animated: true) + } } - - // Need to get the PN mode if registering - let viewController: SessionHostingViewController = SessionHostingViewController( - rootView: PNModeScreen(using: dependencies) - ) - viewController.setUpNavBarSessionIcon() - self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } } struct DisplayNameView_Previews: PreviewProvider { static var previews: some View { - DisplayNameScreen(using: Dependencies.createEmpty()) + DisplayNameScreen(flow: .register, using: Dependencies.createEmpty()) } } diff --git a/Session/Onboarding/LandingScreen.swift b/Session/Onboarding/LandingScreen.swift index c94d6624e9..bd1a2ca95c 100644 --- a/Session/Onboarding/LandingScreen.swift +++ b/Session/Onboarding/LandingScreen.swift @@ -10,39 +10,26 @@ struct LandingScreen: View { public class ViewModel { fileprivate let dependencies: Dependencies private let onOnboardingComplete: () -> () - private var disposables: Set = Set() + private var onboardingStateObservationTask: Task? init(onOnboardingComplete: @escaping () -> Void, using dependencies: Dependencies) { self.dependencies = dependencies self.onOnboardingComplete = onOnboardingComplete } - fileprivate func register(setupComplete: () -> ()) { - // Reset the Onboarding cache to create a new user (in case the user previously went back) - dependencies.set(cache: .onboarding, to: Onboarding.Cache(flow: .register, using: dependencies)) + fileprivate func setupFor(flow: Onboarding.Flow) async { + /// Reset the Onboarding cache to create a new user (in case the user previously went back) + let onboarding: Onboarding.Manager = Onboarding.Manager(flow: flow, using: dependencies) + try? await onboarding.loadInitialState() + dependencies.set(singleton: .onboarding, to: onboarding) /// Once the onboarding process is complete we need to call `onOnboardingComplete` - dependencies[cache: .onboarding].onboardingCompletePublisher - .subscribe(on: DispatchQueue.main, using: dependencies) - .receive(on: DispatchQueue.main, using: dependencies) - .sink(receiveValue: { [weak self] _ in self?.onOnboardingComplete() }) - .store(in: &disposables) - - setupComplete() - } - - fileprivate func restore(setupComplete: () -> ()) { - // Reset the Onboarding cache to create a new user (in case the user previously went back) - dependencies.set(cache: .onboarding, to: Onboarding.Cache(flow: .restore, using: dependencies)) - - /// Once the onboarding process is complete we need to call `onOnboardingComplete` - dependencies[cache: .onboarding].onboardingCompletePublisher - .subscribe(on: DispatchQueue.main, using: dependencies) - .receive(on: DispatchQueue.main, using: dependencies) - .sink(receiveValue: { [weak self] _ in self?.onOnboardingComplete() }) - .store(in: &disposables) - - setupComplete() + onboardingStateObservationTask?.cancel() + onboardingStateObservationTask = Task { [weak self, dependencies] in + if let _ = await dependencies[singleton: .onboarding].state.first(where: { $0 == .completed }) { + self?.onOnboardingComplete() + } + } } } @@ -158,22 +145,28 @@ struct LandingScreen: View { } private func register() { - viewModel.register { - let viewController: SessionHostingViewController = SessionHostingViewController( - rootView: DisplayNameScreen(using: viewModel.dependencies) - ) - viewController.setUpNavBarSessionIcon() - self.host.controller?.navigationController?.pushViewController(viewController, animated: true) + Task(priority: .userInitiated) { + await viewModel.setupFor(flow: .register) + await MainActor.run { + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: DisplayNameScreen(flow: .register, using: viewModel.dependencies) + ) + viewController.setUpNavBarSessionIcon() + self.host.controller?.navigationController?.pushViewController(viewController, animated: true) + } } } private func restore() { - viewModel.restore { - let viewController: SessionHostingViewController = SessionHostingViewController( - rootView: LoadAccountScreen(using: viewModel.dependencies) - ) - viewController.setNavBarTitle("loadAccount".localized()) - self.host.controller?.navigationController?.pushViewController(viewController, animated: true) + Task(priority: .userInitiated) { + await viewModel.setupFor(flow: .restore) + await MainActor.run { + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: LoadAccountScreen(using: viewModel.dependencies) + ) + viewController.setNavBarTitle("loadAccount".localized()) + self.host.controller?.navigationController?.pushViewController(viewController, animated: true) + } } } diff --git a/Session/Onboarding/LoadAccountScreen.swift b/Session/Onboarding/LoadAccountScreen.swift index 7bd02faebe..0039ae9bdc 100644 --- a/Session/Onboarding/LoadAccountScreen.swift +++ b/Session/Onboarding/LoadAccountScreen.swift @@ -54,29 +54,33 @@ struct LoadAccountScreen: View { } private func continueWithSeed(seed: Data, from source: Onboarding.SeedSource, onSuccess: (() -> ())?, onError: (() -> ())?) { - do { - guard seed.count == 16 else { throw Mnemonic.DecodingError.generic } + Task(priority: .userInitiated) { + do { + guard seed.count == 16 else { throw Mnemonic.DecodingError.generic } + + try await dependencies[singleton: .onboarding].setSeedData(seed) + } + catch { + errorString = source.genericErrorMessage + try await Task.sleep(for: .seconds(1)) + await MainActor.run { onError?() } + return + } - try dependencies.mutate(cache: .onboarding) { try $0.setSeedData(seed) } - } - catch { - errorString = source.genericErrorMessage - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - onError?() + Task { + try await Task.sleep(for: .seconds(1)) + await MainActor.run { onSuccess?() } + } + + await MainActor.run { + // Otherwise continue on to request push notifications permissions + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: PNModeScreen(using: dependencies) + ) + viewController.setUpNavBarSessionIcon() + self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } - return - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - onSuccess?() } - - // Otherwise continue on to request push notifications permissions - let viewController: SessionHostingViewController = SessionHostingViewController( - rootView: PNModeScreen(using: dependencies) - ) - viewController.setUpNavBarSessionIcon() - self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } func continueWithHexEncodedSeed(onSuccess: (() -> ())?, onError: (() -> ())?) { diff --git a/Session/Onboarding/LoadingScreen.swift b/Session/Onboarding/LoadingScreen.swift index b731acafbd..d6d3d3838f 100644 --- a/Session/Onboarding/LoadingScreen.swift +++ b/Session/Onboarding/LoadingScreen.swift @@ -12,45 +12,51 @@ struct LoadingScreen: View { public class ViewModel { fileprivate let dependencies: Dependencies fileprivate let preview: Bool - fileprivate var profileRetrievalCancellable: AnyCancellable? + fileprivate let initialFlow: Onboarding.Flow + fileprivate var profileRetrievalTask: Task? - init(preview: Bool, using dependencies: Dependencies) { + init(preview: Bool, initialFlow: Onboarding.Flow, using dependencies: Dependencies) { self.preview = preview + self.initialFlow = initialFlow self.dependencies = dependencies } deinit { - profileRetrievalCancellable?.cancel() + profileRetrievalTask?.cancel() } fileprivate func observeProfileRetrieving(onComplete: @escaping (Bool) -> ()) { - profileRetrievalCancellable = dependencies[cache: .onboarding].displayNamePublisher - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .timeout(.seconds(15), scheduler: DispatchQueue.main, customError: { NetworkError.timeout(error: "", rawData: nil) }) - .receive(on: DispatchQueue.main) - .sink( - receiveCompletion: { _ in }, - receiveValue: { displayName in onComplete(displayName?.isEmpty == false) } - ) - } - - fileprivate func completeRegistration(onComplete: @escaping () -> ()) { - dependencies.mutate(cache: .onboarding) { [dependencies] onboarding in - let shouldSyncPushTokens: Bool = onboarding.useAPNS - - onboarding.completeRegistration { - // Trigger the 'SyncPushTokensJob' directly as we don't want to wait for paths to build - // before requesting the permission from the user - if shouldSyncPushTokens { - SyncPushTokensJob - .run(uploadOnlyIfStale: false, using: dependencies) - .sinkUntilComplete() + profileRetrievalTask = Task(priority: .userInitiated) { [dependencies] in + await withTaskGroup { [dependencies] group in + group.addTask { + return (await dependencies[singleton: .onboarding].displayName + .compactMap { $0 } + .first(where: { _ in true }) ?? "") + } + group.addTask { + try? await Task.sleep(for: .seconds(15)) + return "" } - onComplete() + let displayName: String? = await group.next() + group.cancelAll() + onComplete((displayName ?? "").isEmpty == false) } } } + + fileprivate func completeRegistration() async { + let shouldSyncPushTokens: Bool = await dependencies[singleton: .onboarding].useAPNS + await dependencies[singleton: .onboarding].completeRegistration() + + // Trigger the 'SyncPushTokensJob' directly as we don't want to wait for paths to build + // before requesting the permission from the user + if shouldSyncPushTokens { + SyncPushTokensJob + .run(uploadOnlyIfStale: false, using: dependencies) + .sinkUntilComplete() + } + } } @EnvironmentObject var host: HostWrapper @@ -61,8 +67,16 @@ struct LoadingScreen: View { // MARK: - Initialization - public init(preview: Bool = false, using dependencies: Dependencies) { - self.viewModel = ViewModel(preview: preview, using: dependencies) + public init( + preview: Bool = false, + initialFlow: Onboarding.Flow, + using dependencies: Dependencies + ) { + self.viewModel = ViewModel( + preview: preview, + initialFlow: initialFlow, + using: dependencies + ) } // MARK: - UI @@ -123,13 +137,13 @@ struct LoadingScreen: View { } private func finishLoading(success: Bool) { - viewModel.profileRetrievalCancellable?.cancel() + viewModel.profileRetrievalTask?.cancel() animationTimer?.invalidate() animationTimer = nil guard success else { let viewController: SessionHostingViewController = SessionHostingViewController( - rootView: DisplayNameScreen(using: viewModel.dependencies) + rootView: DisplayNameScreen(flow: viewModel.initialFlow, using: viewModel.dependencies) ) viewController.setUpNavBarSessionIcon() if let navigationController = self.host.controller?.navigationController { @@ -145,8 +159,11 @@ struct LoadingScreen: View { withAnimation(.linear(duration: 0.3)) { self.percentage = 1 } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - viewModel.completeRegistration { + + Task(priority: .userInitiated) { + try? await Task.sleep(for: .milliseconds(500)) + await viewModel.completeRegistration() + await MainActor.run { // Go to the home screen let homeVC: HomeVC = HomeVC(using: viewModel.dependencies) viewModel.dependencies[singleton: .app].setHomeViewController(homeVC) @@ -217,6 +234,6 @@ struct CircularProgressView: View { struct LoadingView_Previews: PreviewProvider { static var previews: some View { - LoadingScreen(preview: true, using: Dependencies.createEmpty()) + LoadingScreen(preview: true, initialFlow: .register, using: Dependencies.createEmpty()) } } diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 4c2419ff15..59a617a699 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -9,12 +9,10 @@ import SessionNetworkingKit // MARK: - Cache -public extension Cache { - static let onboarding: CacheConfig = Dependencies.create( +public extension Singleton { + static let onboarding: SingletonConfig = Dependencies.create( identifier: "onboarding", - createInstance: { dependencies in Onboarding.Cache(flow: .none, using: dependencies) }, - mutableInstance: { $0 }, - immutableInstance: { $0 } + createInstance: { dependencies in Onboarding.Manager(flow: .none, using: dependencies) } ) } @@ -28,6 +26,7 @@ public extension Log.Category { public enum Onboarding { public enum State: CustomStringConvertible { + case unknown case noUser case noUserInvalidKeyPair case noUserInvalidSeedGeneration @@ -37,6 +36,7 @@ public enum Onboarding { // stringlint:ignore_contents public var description: String { switch self { + case .unknown: return "Unknown" case .noUser: return "No User" case .noUserInvalidKeyPair: return "No User Invalid Key Pair" case .noUserInvalidSeedGeneration: return "No User Invalid Seed Generation" @@ -66,38 +66,28 @@ public enum Onboarding { } } -// MARK: - Onboarding.Cache +// MARK: - Onboarding.Manager extension Onboarding { - class Cache: OnboardingCacheType { + actor Manager: OnboardingManagerType { private let dependencies: Dependencies + nonisolated public let syncState: OnboardingManagerSyncState = OnboardingManagerSyncState() public let id: UUID public let initialFlow: Onboarding.Flow - public var state: State - private let completionSubject: CurrentValueSubject = CurrentValueSubject(false) + public var state: AsyncStream { stateStream.stream } + public var displayName: AsyncStream { displayNameStream.stream } - public var seed: Data - public var ed25519KeyPair: KeyPair - public var x25519KeyPair: KeyPair - public var userSessionId: SessionId - public var useAPNS: Bool + public var seed: Data = Data() + public var ed25519KeyPair: KeyPair = .empty + public var x25519KeyPair: KeyPair = .empty + public var userSessionId: SessionId = .invalid + public var useAPNS: Bool = false - public var displayName: String - private var _displayNamePublisher: AnyPublisher? - private var hasInitialDisplayName: Bool + private var hasInitialDisplayName: Bool = false private var userProfileConfigMessage: ProcessedMessage? - private var disposables: Set = Set() - - public var displayNamePublisher: AnyPublisher { - _displayNamePublisher ?? Fail(error: NetworkError.notFound).eraseToAnyPublisher() - } - - public var onboardingCompletePublisher: AnyPublisher { - completionSubject - .filter { $0 } - .map { _ in () } - .eraseToAnyPublisher() - } + private var retrieveDisplayNameTask: Task? + private let stateStream: CurrentValueAsyncStream = CurrentValueAsyncStream(.unknown) + private let displayNameStream: CurrentValueAsyncStream = CurrentValueAsyncStream(nil) // MARK: - Initialization @@ -105,19 +95,47 @@ extension Onboarding { self.dependencies = dependencies self.id = dependencies.randomUUID() self.initialFlow = flow + } + + /// This initializer should only be used in the `DeveloperSettingsViewModel` when swapping between network service layers + init( + ed25519KeyPair: KeyPair, + x25519KeyPair: KeyPair, + displayName: String, + using dependencies: Dependencies + ) { + self.dependencies = dependencies + self.id = dependencies.randomUUID() + self.initialFlow = .devSettings + self.seed = Data() + self.ed25519KeyPair = ed25519KeyPair + self.x25519KeyPair = x25519KeyPair + self.userSessionId = SessionId(.standard, publicKey: x25519KeyPair.publicKey) + self.useAPNS = dependencies[defaults: .standard, key: .isUsingFullAPNs] + self.hasInitialDisplayName = !displayName.isEmpty + Task { [self] in + syncState.update(state: .completed) + await stateStream.send(.completed) + await displayNameStream.send(displayName) + } + } + + deinit { + Task { [stateStream, displayNameStream] in + await stateStream.finishCurrentStreams() + await displayNameStream.finishCurrentStreams() + } + } + + // MARK: - Functions + + public func loadInitialState() async throws { /// Try to load the users `ed25519KeyPair` from the database and generate the `x25519KeyPair` from it - var ed25519KeyPair: KeyPair = .empty - let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) - dependencies[singleton: .storage].readAsync( - retrieve: { db in Identity.fetchUserEd25519KeyPair(db) }, - completion: { result in - ed25519KeyPair = ((try? result.get()) ?? .empty) - semaphore.signal() - } - ) - semaphore.wait() - let x25519KeyPair: KeyPair = { + ed25519KeyPair = try await dependencies[singleton: .storage].readAsync { db in + Identity.fetchUserEd25519KeyPair(db) ?? .empty + } + x25519KeyPair = { guard ed25519KeyPair != .empty, let x25519PublicKey: [UInt8] = dependencies[singleton: .crypto].generate( @@ -132,30 +150,24 @@ extension Onboarding { }() /// Retrieve the users `displayName` from `libSession` (the source of truth) - let displayName: String = dependencies.mutate(cache: .libSession) { $0.profile }.name - let hasInitialDisplayName: Bool = !displayName.isEmpty - - self.ed25519KeyPair = ed25519KeyPair - self.displayName = displayName - self.hasInitialDisplayName = hasInitialDisplayName - self.x25519KeyPair = x25519KeyPair - self.userSessionId = (x25519KeyPair != .empty ? + await displayNameStream.send(dependencies.mutate(cache: .libSession) { $0.profile }.name) + hasInitialDisplayName = await !(displayNameStream.currentValue ?? "").isEmpty + userSessionId = (x25519KeyPair != .empty ? SessionId(.standard, publicKey: x25519KeyPair.publicKey) : .invalid ) - self.state = { + + let expectedState: Onboarding.State = { guard ed25519KeyPair != .empty else { return .noUser } guard x25519KeyPair != .empty else { return .noUserInvalidKeyPair } guard hasInitialDisplayName else { return .missingName } return .completed }() - self.seed = Data() /// Overwritten below - self.useAPNS = false /// Overwritten below /// Update the cached values depending on the `initialState` - switch state { - case .noUser, .noUserInvalidKeyPair, .noUserInvalidSeedGeneration: + switch expectedState { + case .unknown, .noUser, .noUserInvalidKeyPair, .noUserInvalidSeedGeneration: /// Remove the `LibSession.Cache` just in case (to ensure no previous state remains) dependencies.remove(cache: .libSession) @@ -169,91 +181,70 @@ extension Onboarding { else { /// Seed or identity generation failed so leave the `Onboarding.Cache` in an invalid state for the UI to /// recover somehow - self.state = .noUserInvalidSeedGeneration + syncState.update(state: .noUserInvalidSeedGeneration) + await stateStream.send(.noUserInvalidSeedGeneration) return } /// The identity data was successfully generated so store it for the onboarding process - self.state = .noUserInvalidKeyPair - self.seed = finalSeedData - self.ed25519KeyPair = identity.ed25519KeyPair - self.x25519KeyPair = identity.x25519KeyPair - self.userSessionId = SessionId(.standard, publicKey: identity.x25519KeyPair.publicKey) - self.displayName = "" + seed = finalSeedData + ed25519KeyPair = identity.ed25519KeyPair + x25519KeyPair = identity.x25519KeyPair + userSessionId = SessionId(.standard, publicKey: identity.x25519KeyPair.publicKey) + await displayNameStream.send(nil) + syncState.update(state: .noUserInvalidKeyPair) + await stateStream.send(.noUserInvalidKeyPair) case .missingName, .completed: - self.useAPNS = dependencies[defaults: .standard, key: .isUsingFullAPNs] - - /// If we are already in a completed state then updated the completion subject accordingly - if self.state == .completed { - self.completionSubject.send(true) - } + useAPNS = dependencies[defaults: .standard, key: .isUsingFullAPNs] + syncState.update(state: expectedState) + await stateStream.send(expectedState) } } - /// This initializer should only be used in the `DeveloperSettingsViewModel` when swapping between network service layers - init( - ed25519KeyPair: KeyPair, - x25519KeyPair: KeyPair, - displayName: String, - using dependencies: Dependencies - ) { - self.dependencies = dependencies - self.id = dependencies.randomUUID() - self.state = .completed - self.initialFlow = .devSettings - self.seed = Data() - self.ed25519KeyPair = ed25519KeyPair - self.x25519KeyPair = x25519KeyPair - self.userSessionId = SessionId(.standard, publicKey: x25519KeyPair.publicKey) - self.useAPNS = dependencies[defaults: .standard, key: .isUsingFullAPNs] - self.displayName = displayName - self.hasInitialDisplayName = !displayName.isEmpty - self._displayNamePublisher = nil - } - - // MARK: - Functions - public func setSeedData(_ seedData: Data) throws { - /// Reset the disposables in case this was called with different data/ - disposables = Set() - /// Generate the keys and store them let identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair) = try Identity.generate( from: seedData, using: dependencies ) - self.seed = seedData - self.ed25519KeyPair = identity.ed25519KeyPair - self.x25519KeyPair = identity.x25519KeyPair - self.userSessionId = SessionId(.standard, publicKey: identity.x25519KeyPair.publicKey) + seed = seedData + ed25519KeyPair = identity.ed25519KeyPair + x25519KeyPair = identity.x25519KeyPair + userSessionId = SessionId(.standard, publicKey: identity.x25519KeyPair.publicKey) - /// **Note:** We trigger this as a "background poll" as doing so means the received messages will be - /// processed immediately rather than async as part of a Job - let poller: CurrentUserPoller = CurrentUserPoller( - pollerName: "Onboarding Poller", // stringlint:ignore - pollerQueue: Threading.pollerQueue, - pollerDestination: .swarm(self.userSessionId.hexString), - pollerDrainBehaviour: .alwaysRandom, - namespaces: [.configUserProfile], - shouldStoreMessages: false, - logStartAndStopCalls: false, - customAuthMethod: Authentication.standard( - sessionId: userSessionId, - ed25519PublicKey: identity.ed25519KeyPair.publicKey, - ed25519SecretKey: identity.ed25519KeyPair.secretKey - ), - using: dependencies - ) - - typealias PollResult = (configMessage: ProcessedMessage, displayName: String?) - let publisher: AnyPublisher = poller - .poll(forceSynchronousProcessing: true) - .tryMap { [userSessionId, dependencies] messages, _, _, _ -> PollResult? in + /// Kick off the request to get the display name + retrieveDisplayNameTask?.cancel() + retrieveDisplayNameTask = Task(priority: .userInitiated) { [weak self, userSessionId, dependencies] in + /// **Note:** We trigger this as a "background poll" as doing so means the received messages will be + /// processed immediately rather than async as part of a Job + let poller: CurrentUserPoller = CurrentUserPoller( + pollerName: "Onboarding Poller", // stringlint:ignore + destination: .swarm(userSessionId.hexString), + swarmDrainStrategy: .alwaysRandom, + namespaces: [.configUserProfile], + failureCount: 0, + shouldStoreMessages: false, + logStartAndStopCalls: false, + customAuthMethod: Authentication.standard( + sessionId: userSessionId, + ed25519PublicKey: identity.ed25519KeyPair.publicKey, + ed25519SecretKey: identity.ed25519KeyPair.secretKey + ), + using: dependencies + ) + guard !Task.isCancelled else { return } + + do { + let messages: [ProcessedMessage] = try await poller + .poll(forceSynchronousProcessing: true) + .response + guard + !Task.isCancelled, let targetMessage: ProcessedMessage = messages.last, /// Just in case there are multiple case let .config(_, _, serverHash, serverTimestampMs, data, _) = targetMessage - else { return nil } + else { return } /// In order to process the config message we need to create and load a `libSession` cache, but we don't want to load this into /// memory at this stage in case the user cancels the onboarding process part way through @@ -279,225 +270,211 @@ extension Onboarding { ] ) - return (targetMessage, cache.displayName) - } - .handleEvents( - receiveOutput: { [weak self] result in - guard let result: PollResult = result else { return } - - /// Only store the `displayName` returned from the swarm if the user hasn't provided one in the display - /// name step (otherwise the user could enter a display name and have it immediately overwritten due to the - /// config request running slow) - if - self?.hasInitialDisplayName != true, - let displayName: String = result.displayName, - !displayName.isEmpty - { - self?.displayName = displayName - } - - self?.userProfileConfigMessage = result.configMessage + guard !Task.isCancelled else { return } + + /// Only store the `displayName` returned from the swarm if the user hasn't provided one in the display + /// name step (otherwise the user could enter a display name and have it immediately overwritten due to the + /// config request running slow) + if + await self?.hasInitialDisplayName != true, + let displayName: String = cache.displayName, + !displayName.isEmpty + { + await self?.displayNameStream.send(displayName) } - ) - .map { result -> String? in result?.displayName } - .catch { error -> AnyPublisher in + + await self?.setUserProfileConfigMessage(targetMessage) + } + catch { Log.warn(.onboarding, "Failed to retrieve existing profile information due to error: \(error).") - return Just(nil) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() } - .shareReplay(1) - .eraseToAnyPublisher() - - /// Store the publisher and cancelable so we only make one request during onboarding - _displayNamePublisher = publisher - publisher - .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) - .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) - .store(in: &disposables) + + guard let self = self else { return } + + /// Always emit a value if we got a response (doesn't matter it it was a successful response or not, we just want to + /// finish loading) + await displayNameStream.send(displayNameStream.currentValue ?? "") + } } - func setUseAPNS(_ useAPNS: Bool) { + func setUseAPNS(_ useAPNS: Bool) async { self.useAPNS = useAPNS } - func setDisplayName(_ displayName: String) { - self.displayName = displayName + func setDisplayName(_ displayName: String) async { + await displayNameStream.send(displayName) } - func completeRegistration(onComplete: @escaping (() -> Void)) { - DispatchQueue.global(qos: .userInitiated).async(using: dependencies) { [weak self, initialFlow, originalState = state, userSessionId, ed25519KeyPair, x25519KeyPair, useAPNS, displayName, userProfileConfigMessage, dependencies] in - /// Cache the users session id (so we don't need to fetch it from the database every time) - dependencies.mutate(cache: .general) { - $0.setSecretKey(ed25519SecretKey: ed25519KeyPair.secretKey) - } - - /// If we had a proper `initialFlow` then create a new `libSession` cache for the user + private func setUserProfileConfigMessage(_ userProfileConfigMessage: ProcessedMessage) { + self.userProfileConfigMessage = userProfileConfigMessage + } + + func completeRegistration() async { + /// Cache the users session id (so we don't need to fetch it from the database every time) + dependencies.mutate(cache: .general) { + $0.setSecretKey(ed25519SecretKey: ed25519KeyPair.secretKey) + } + + /// If we had a proper `initialFlow` then create a new `libSession` cache for the user + if initialFlow != .none { + dependencies.set( + cache: .libSession, + to: LibSession.Cache( + userSessionId: userSessionId, + using: dependencies + ) + ) + } + + let originalState: State = await stateStream.currentValue + let displayName: String = (await displayNameStream.currentValue ?? "") + try? await dependencies[singleton: .storage].writeAsync { [self] db in + /// Only update the identity/contact/Note to Self state if we have a proper `initialFlow` if initialFlow != .none { - dependencies.set( - cache: .libSession, - to: LibSession.Cache( - userSessionId: userSessionId, - using: dependencies + /// Store the user identity information + try Identity.store(db, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair) + + /// Create a contact for the current user and set their approval/trusted statuses so they don't get weird behaviours + try Contact + .fetchOrCreate(db, id: userSessionId.hexString, using: dependencies) + .upsert(db) + try Contact + .filter(id: userSessionId.hexString) + .updateAll( /// Current user `Contact` record not synced so no need to use `updateAllAndConfig` + db, + Contact.Columns.isTrusted.set(to: true), /// Always trust the current user + Contact.Columns.isApproved.set(to: true), + Contact.Columns.didApproveMe.set(to: true) ) + db.addContactEvent(id: userSessionId.hexString, change: .isTrusted(true)) + db.addContactEvent(id: userSessionId.hexString, change: .isApproved(true)) + db.addContactEvent(id: userSessionId.hexString, change: .didApproveMe(true)) + + /// Create the 'Note to Self' thread (not visible by default) + try SessionThread.upsert( + db, + id: userSessionId.hexString, + variant: .contact, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(false)), + using: dependencies ) - } - - dependencies[singleton: .storage].writeAsync( - updates: { db in - /// Only update the identity/contact/Note to Self state if we have a proper `initialFlow` - if initialFlow != .none { - /// Store the user identity information - try Identity.store(db, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair) - - /// Create a contact for the current user and set their approval/trusted statuses so they don't get weird behaviours - try Contact - .fetchOrCreate(db, id: userSessionId.hexString, using: dependencies) - .upsert(db) - try Contact - .filter(id: userSessionId.hexString) - .updateAll( /// Current user `Contact` record not synced so no need to use `updateAllAndConfig` - db, - Contact.Columns.isTrusted.set(to: true), /// Always trust the current user - Contact.Columns.isApproved.set(to: true), - Contact.Columns.didApproveMe.set(to: true) - ) - db.addContactEvent(id: userSessionId.hexString, change: .isTrusted(true)) - db.addContactEvent(id: userSessionId.hexString, change: .isApproved(true)) - db.addContactEvent(id: userSessionId.hexString, change: .didApproveMe(true)) - - /// Create the 'Note to Self' thread (not visible by default) - try SessionThread.upsert( - db, - id: userSessionId.hexString, - variant: .contact, - values: SessionThread.TargetValues(shouldBeVisible: .setTo(false)), - using: dependencies - ) - - /// Load the initial `libSession` state (won't have been created on launch due to lack of ed25519 key) - dependencies.mutate(cache: .libSession) { cache in - cache.loadState(db) - - /// If we have a `userProfileConfigMessage` then we should try to handle it here as if we don't then - /// we won't even process it (because the hash may be deduped via another process) - if let userProfileConfigMessage: ProcessedMessage = userProfileConfigMessage { - try? cache.handleConfigMessages( - db, - swarmPublicKey: userSessionId.hexString, - messages: ConfigMessageReceiveJob - .Details(messages: [userProfileConfigMessage]) - .messages - ) - } - - /// Update the `displayName` and trigger a dump/push of the config - try? cache.performAndPushChange(db, for: .userProfile) { - try? cache.updateProfile(displayName: displayName) - } - } - - /// Clear the `lastNameUpdate` timestamp and forcibly set the `displayName` provided - /// during the onboarding step (we do this after handling the config message because we want - /// the value provided during onboarding to superseed any retrieved from the config) - try Profile - .fetchOrCreate(db, id: userSessionId.hexString) - .upsert(db) - try Profile - .filter(id: userSessionId.hexString) - .updateAll(db, Profile.Columns.lastNameUpdate.set(to: nil)) - try Profile.updateIfNeeded( - db, - publicKey: userSessionId.hexString, - displayNameUpdate: .currentUserUpdate(displayName), - displayPictureUpdate: .none, - sentTimestamp: dependencies.dateNow.timeIntervalSince1970, - using: dependencies - ) - - /// Emit observation events (_shouldn't_ be needed since this is happening during onboarding but - /// doesn't hurt just to be safe) - db.addEvent(useAPNS, forKey: .isUsingFullAPNs) - } - /// Now that everything is saved we should update the `Onboarding.Cache` `state` to be `completed` (we do - /// this within the db write query because then `updateAllAndConfig` below will trigger a config sync which is - /// dependant on this `state` being updated) - self?.state = .completed + /// Load the initial `libSession` state (won't have been created on launch due to lack of ed25519 key) + dependencies.mutate(cache: .libSession) { cache in + cache.loadState(db) - /// We need to explicitly `updateAllAndConfig` the `shouldBeVisible` value to `false` for new accounts otherwise it - /// won't actually get synced correctly and could result in linking a second device and having the 'Note to Self' conversation incorrectly - /// being visible - if initialFlow == .register { - try SessionThread.updateVisibility( + /// If we have a `userProfileConfigMessage` then we should try to handle it here as if we don't then + /// we won't even process it (because the hash may be deduped via another process) + if let userProfileConfigMessage: ProcessedMessage = userProfileConfigMessage { + try? cache.handleConfigMessages( db, - threadId: userSessionId.hexString, - isVisible: false, - using: dependencies + swarmPublicKey: userSessionId.hexString, + messages: ConfigMessageReceiveJob + .Details(messages: [userProfileConfigMessage]) + .messages ) } - }, - completion: { _ in - /// No need to show the seed again if the user is restoring - dependencies.setAsync(.hasViewedSeed, (initialFlow == .restore)) - - /// Now that the onboarding process is completed we can store the `UserMetadata` for the Share and Notification - /// extensions (prior to this point the account is in an invalid state so they can't be used) - do { - try dependencies[singleton: .extensionHelper].saveUserMetadata( - sessionId: userSessionId, - ed25519SecretKey: ed25519KeyPair.secretKey, - unreadCount: 0 + else { + /// We need to explicitly set the `priority` value to `hiddenPriority` for new accounts + /// otherwise it won't actually get synced correctly and could result in linking a second device and + /// having the 'Note to Self' conversation incorrectly being visible + try? LibSession.updateNoteToSelf( + db, + priority: LibSession.hiddenPriority, + using: dependencies ) - } catch { Log.error(.onboarding, "Falied to save user metadata: \(error)") } - - /// Store whether the user wants to use APNS - dependencies[defaults: .standard, key: .isUsingFullAPNs] = useAPNS - - /// Log the resolution - switch (initialFlow, originalState) { - case (.none, _), (.devSettings, _): break - case (.register, _): Log.info(.onboarding, "Registration completed") - case (.restore, .missingName): Log.info(.onboarding, "Missing name replaced") - case (.restore, _): Log.info(.onboarding, "Restore account completed") } - /// Send an event indicating that registration is complete - self?.completionSubject.send(true) - - DispatchQueue.main.async(using: dependencies) { - onComplete() + /// Update the `displayName` and trigger a dump/push of the config + try? cache.performAndPushChange(db, for: .userProfile) { + try? cache.updateProfile(displayName: displayName) } } + + /// Clear the `lastNameUpdate` timestamp and forcibly set the `displayName` provided + /// during the onboarding step (we do this after handling the config message because we want + /// the value provided during onboarding to superseed any retrieved from the config) + try Profile + .fetchOrCreate(db, id: userSessionId.hexString) + .upsert(db) + try Profile + .filter(id: userSessionId.hexString) + .updateAll(db, Profile.Columns.lastNameUpdate.set(to: nil)) + try Profile.updateIfNeeded( + db, + publicKey: userSessionId.hexString, + displayNameUpdate: .currentUserUpdate(displayName), + displayPictureUpdate: .none, + sentTimestamp: dependencies.dateNow.timeIntervalSince1970, + using: dependencies + ) + + /// Emit observation events (_shouldn't_ be needed since this is happening during onboarding but + /// doesn't hurt just to be safe) + db.addEvent(useAPNS, forKey: .isUsingFullAPNs) + } + } + + /// No need to show the seed again if the user is restoring + await dependencies.set(.hasViewedSeed, (initialFlow == .restore)) + + /// Now that the onboarding process is completed we can store the `UserMetadata` for the Share and Notification + /// extensions (prior to this point the account is in an invalid state so they can't be used) + do { + try dependencies[singleton: .extensionHelper].saveUserMetadata( + sessionId: userSessionId, + ed25519SecretKey: ed25519KeyPair.secretKey, + unreadCount: 0 ) + } catch { Log.error(.onboarding, "Falied to save user metadata: \(error)") } + + /// Store whether the user wants to use APNS + dependencies[defaults: .standard, key: .isUsingFullAPNs] = useAPNS + + /// Log the resolution + switch (initialFlow, originalState) { + case (.none, _), (.devSettings, _): break + case (.register, _): Log.info(.onboarding, "Registration completed") + case (.restore, .missingName): Log.info(.onboarding, "Missing name replaced") + case (.restore, _): Log.info(.onboarding, "Restore account completed") + } + + /// Now flag the state as completed + syncState.update(state: .completed) + await stateStream.send(.completed) + + /// Perform a config sync if needed (this needs to be done for new accounts to ensure the initial state is synced correctly) + try? await dependencies[singleton: .storage].writeAsync { [self] db in + ConfigurationSyncJob.enqueue(db, swarmPublicKey: userSessionId.hexString, using: dependencies) } } } } -// MARK: - OnboardingCacheType +// MARK: - OnboardingManagerType -/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way -public protocol OnboardingImmutableCacheType: ImmutableCacheType { - var id: UUID { get } - var state: Onboarding.State { get } - var initialFlow: Onboarding.Flow { get } +/// We manually handle thread-safety using the `NSLock` so can ensure this is `Sendable` +public final class OnboardingManagerSyncState: @unchecked Sendable { + private let lock = NSLock() + private var _state: Onboarding.State = .unknown - var seed: Data { get } - var ed25519KeyPair: KeyPair { get } - var x25519KeyPair: KeyPair { get } - var userSessionId: SessionId { get } - var useAPNS: Bool { get } - - var displayName: String { get } - var displayNamePublisher: AnyPublisher { get } - var onboardingCompletePublisher: AnyPublisher { get } + public var state: Onboarding.State { lock.withLock { _state } } + + func update(state: Onboarding.State) { + lock.withLock { self._state = state } + } } -public protocol OnboardingCacheType: OnboardingImmutableCacheType, MutableCacheType { +// MARK: - OnboardingManagerType + +public protocol OnboardingManagerType: Actor { + @available(*, deprecated, message: "Should try to refactor the code to use proper async/await") + nonisolated var syncState: OnboardingManagerSyncState { get } + var id: UUID { get } - var state: Onboarding.State { get } var initialFlow: Onboarding.Flow { get } + var state: AsyncStream { get } + var displayName: AsyncStream { get } var seed: Data { get } var ed25519KeyPair: KeyPair { get } @@ -505,21 +482,12 @@ public protocol OnboardingCacheType: OnboardingImmutableCacheType, MutableCacheT var userSessionId: SessionId { get } var useAPNS: Bool { get } - var displayName: String { get } - var displayNamePublisher: AnyPublisher { get } - var onboardingCompletePublisher: AnyPublisher { get } - - func setSeedData(_ seedData: Data) throws - func setUseAPNS(_ useAPNS: Bool) - func setDisplayName(_ displayName: String) + func loadInitialState() async throws + func setSeedData(_ seedData: Data) async throws + func setUseAPNS(_ useAPNS: Bool) async + func setDisplayName(_ displayName: String) async /// Complete the registration process storing the created/updated user state in the database and creating /// the `libSession` state if needed - /// - /// **Note:** The `onComplete` callback will be run on the main thread - func completeRegistration(onComplete: @escaping (() -> Void)) -} - -public extension OnboardingCacheType { - func completeRegistration() { completeRegistration(onComplete: {}) } + func completeRegistration() async } diff --git a/Session/Onboarding/PNModeScreen.swift b/Session/Onboarding/PNModeScreen.swift index 40100bf54e..69e64a8c59 100644 --- a/Session/Onboarding/PNModeScreen.swift +++ b/Session/Onboarding/PNModeScreen.swift @@ -126,46 +126,50 @@ struct PNModeScreen: View { } private func register() { - // Store whether we want to use APNS - dependencies.mutate(cache: .onboarding) { $0.setUseAPNS(currentSelection == .fast) } - - // If we are registering then we can just continue on - guard dependencies[cache: .onboarding].initialFlow != .register else { - return completeRegistration() + Task(priority: .userInitiated) { + // Store whether we want to use APNS + await dependencies[singleton: .onboarding].setUseAPNS(currentSelection == .fast) + + // If we are registering then we can just continue on + let initialFlow: Onboarding.Flow = await dependencies[singleton: .onboarding].initialFlow + guard initialFlow != .register else { + return await completeRegistration() + } + + // Check if we already have a profile name (ie. profile retrieval completed while waiting on + // this screen) + guard await dependencies[singleton: .onboarding].displayName.first() != nil else { + // If we have one then we can go straight to the home screen + return await self.completeRegistration() + } + + // If we don't have one then show a loading indicator and try to retrieve the existing name + await MainActor.run { + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: LoadingScreen(initialFlow: initialFlow, using: dependencies) + ) + viewController.setUpNavBarSessionIcon() + self.host.controller?.navigationController?.pushViewController(viewController, animated: true) + } } + } + + private func completeRegistration() async { + let shouldSyncPushTokens: Bool = await dependencies[singleton: .onboarding].useAPNS + await dependencies[singleton: .onboarding].completeRegistration() - // Check if we already have a profile name (ie. profile retrieval completed while waiting on - // this screen) - guard dependencies[cache: .onboarding].displayName.isEmpty else { - // If we have one then we can go straight to the home screen - return self.completeRegistration() + // Trigger the 'SyncPushTokensJob' directly as we don't want to wait for paths to build + // before requesting the permission from the user + if shouldSyncPushTokens { + SyncPushTokensJob + .run(uploadOnlyIfStale: false, using: dependencies) + .sinkUntilComplete() } - // If we don't have one then show a loading indicator and try to retrieve the existing name - let viewController: SessionHostingViewController = SessionHostingViewController( - rootView: LoadingScreen(using: dependencies) - ) - viewController.setUpNavBarSessionIcon() - self.host.controller?.navigationController?.pushViewController(viewController, animated: true) - } - - private func completeRegistration() { - dependencies.mutate(cache: .onboarding) { [dependencies] onboarding in - let shouldSyncPushTokens: Bool = onboarding.useAPNS - - onboarding.completeRegistration { - // Trigger the 'SyncPushTokensJob' directly as we don't want to wait for paths to build - // before requesting the permission from the user - if shouldSyncPushTokens { - SyncPushTokensJob - .run(uploadOnlyIfStale: false, using: dependencies) - .sinkUntilComplete() - } - - let homeVC: HomeVC = HomeVC(using: dependencies) - dependencies[singleton: .app].setHomeViewController(homeVC) - self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) - } + await MainActor.run { + let homeVC: HomeVC = HomeVC(using: dependencies) + dependencies[singleton: .app].setHomeViewController(homeVC) + self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) } } } diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index 47f15b8d6a..ffebfd56db 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -1229,27 +1229,29 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, dependencies.warm(singleton: .network) /// Run the onboarding process as if we are recovering an account (will setup the device in it's proper state) - Onboarding.Cache( + let updatedOnboarding: Onboarding.Manager = Onboarding.Manager( ed25519KeyPair: identityData.ed25519KeyPair, x25519KeyPair: identityData.x25519KeyPair, displayName: existingProfile.name .nullIfEmpty .defaulting(to: "Anonymous"), using: dependencies - ).completeRegistration { [dependencies] in - /// Re-enable developer mode - dependencies.setAsync(.developerModeEnabled, true) - - /// Restart the current user poller (there won't be any other pollers though) - Task { @MainActor [poller = dependencies[singleton: .currentUserPoller]] in - await poller.startIfNeeded() - } - - /// Re-sync the push tokens (if there are any) - SyncPushTokensJob.run(uploadOnlyIfStale: false, using: dependencies).sinkUntilComplete() - - Log.info("[DevSettings] Completed swap to \(String(describing: updatedNetwork))") + ) + dependencies.set(singleton: .onboarding, to: updatedOnboarding) + await updatedOnboarding.completeRegistration() + + /// Re-enable developer mode + dependencies.setAsync(.developerModeEnabled, true) + + /// Restart the current user poller (there won't be any other pollers though) + Task { @MainActor [poller = dependencies[singleton: .currentUserPoller]] in + await poller.startIfNeeded() } + + /// Re-sync the push tokens (if there are any) + SyncPushTokensJob.run(uploadOnlyIfStale: false, using: dependencies).sinkUntilComplete() + + Log.info("[DevSettings] Completed swap to \(String(describing: updatedNetwork))") } private func updateFlag(for feature: FeatureConfig, to updatedFlag: Bool?) { @@ -1781,7 +1783,9 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, "Clearing current account data..." ) - (UIApplication.shared.delegate as? AppDelegate)?.stopPollers() + Task(priority: .userInitiated) { + await (UIApplication.shared.delegate as? AppDelegate)?.stopPollers() + } } /// Need to shut everything down before the swap out the data to prevent crashes diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index 0a8e1f2e8b..19c3e8df64 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -332,15 +332,15 @@ final class NukeDataModal: Modal { dependencies[singleton: .notificationsManager].clearAllNotifications() UIApplication.shared.applicationIconBadgeNumber = 0 - // Stop any pollers - (UIApplication.shared.delegate as? AppDelegate)?.stopPollers() - // Call through to the SessionApp's "resetAppData" which will wipe out logs, database and // profile storage let wasUnlinked: Bool = dependencies[defaults: .standard, key: .wasUnlinked] let serviceNetwork: ServiceNetwork = dependencies[feature: .serviceNetwork] Task { + // Stop any pollers + await (UIApplication.shared.delegate as? AppDelegate)?.stopPollers() + await dependencies[singleton: .app].resetData { [dependencies] in // Resetting the data clears the old user defaults. We need to restore the unlink default. dependencies[defaults: .standard, key: .wasUnlinked] = wasUnlinked diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 1684d94c27..38b31c0c78 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -149,7 +149,11 @@ public struct SessionThread: Codable, Identifiable, Equatable, Hashable, Fetchab switch ObservationContext.observingDb { case .none: Log.error("[SessionThread] Could not process 'aroundInsert' due to missing observingDb.") case .some(let observingDb): - observingDb.dependencies.setAsync(.hasSavedThread, true) + /// Only set the `hasSavedThread` value if it's not the 'Note to Self' thread + if id != observingDb.dependencies[cache: .general].sessionId.hexString { + observingDb.dependencies.setAsync(.hasSavedThread, true) + } + observingDb.addConversationEvent(id: id, type: .created) } } diff --git a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift index e642bb2ff6..7c9e26451b 100644 --- a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift +++ b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift @@ -196,9 +196,9 @@ public enum ConfigurationSyncJob: JobExecutor { // If the failure is due to being offline then we should automatically // retry if the connection is re-established Task { [dependencies] in - let currentStatus: NetworkStatus = (await dependencies[singleton: .network] + let currentStatus: NetworkStatus = await dependencies[singleton: .network] .networkStatus - .first(where: { _ in true }) ?? .unknown) + .first(defaultValue: .unknown) // If we are currently connected then use the standard retry behaviour guard currentStatus != .connected else { diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift index ce4d1986dd..24bcd3c4e1 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift @@ -924,7 +924,7 @@ public extension Dependencies { } } - private func set(_ key: Setting.BoolKey, _ value: Bool?) async { + func set(_ key: Setting.BoolKey, _ value: Bool?) async { let targetVariant: ConfigDump.Variant switch key { @@ -943,7 +943,7 @@ public extension Dependencies { } } - private func set(_ key: Setting.EnumKey, _ value: T?) async { + func set(_ key: Setting.EnumKey, _ value: T?) async { let mutation: LibSession.Mutation? = try? await self.mutateAsyncAware(cache: .libSession) { cache in try cache.perform(for: .local) { cache.set(key, value) diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift index 52858b3c2b..a09bb0c4dc 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift @@ -706,6 +706,7 @@ public final class CommunityPollerManagerSyncState: @unchecked Sendable { // MARK: - CommunityPollerManagerType public protocol CommunityPollerManagerType { + @available(*, deprecated, message: "Should try to refactor the code to use proper async/await") nonisolated var syncState: CommunityPollerManagerSyncState { get } var serversBeingPolled: Set { get async } var allPollers: [any PollerType] { get async } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift index 25b1951728..8c9006ee28 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift @@ -120,7 +120,7 @@ public actor GroupPoller: SwarmPollerType { /// `GroupKeys` config message guard isExpired != true, - let response: PollResponse = await receivedPollResponse.first(where: { _ in true }), + let response: PollResponse = await receivedPollResponse.first(), !response.contains(where: { $0.namespace == .configGroupKeys }) else { return } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift index 9a50228715..4fbaab59e1 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift @@ -134,7 +134,7 @@ public extension PollerType { pollTask = Task { /// Don't bother trying to poll if we don't have a network connection, just wait for one to be established let networkStatus: NetworkStatus? = await dependencies[singleton: .network].networkStatus - .first(where: { _ in true }) + .first() if networkStatus != .connected { Log.info(.poller, "\(pollerName) waiting for network to connect before starting to poll.") diff --git a/SessionNetworkingKit/Types/Network.swift b/SessionNetworkingKit/Types/Network.swift index 92e1b44cc6..1e622c342b 100644 --- a/SessionNetworkingKit/Types/Network.swift +++ b/SessionNetworkingKit/Types/Network.swift @@ -20,6 +20,7 @@ public extension Singleton { public protocol NetworkType { var isSuspended: Bool { get async } nonisolated var networkStatus: AsyncStream { get } + @available(*, deprecated, message: "Should try to refactor the code to use proper async/await") nonisolated var syncState: NetworkSyncState { get } func getActivePaths() async throws -> [LibSession.Path] diff --git a/SessionUtilitiesKit/Utilities/AsyncSequence+Utilities.swift b/SessionUtilitiesKit/Utilities/AsyncSequence+Utilities.swift index 3992824965..d445280a1f 100644 --- a/SessionUtilitiesKit/Utilities/AsyncSequence+Utilities.swift +++ b/SessionUtilitiesKit/Utilities/AsyncSequence+Utilities.swift @@ -1,5 +1,25 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +import Foundation + +public extension AsyncSequence { + func asAsyncStream() -> AsyncStream { + AsyncStream { continuation in + let task: Task = Task { + for try await element in self { + continuation.yield(element) + } + + continuation.finish() + } + + continuation.onTermination = { _ in + task.cancel() + } + } + } +} + public extension AsyncSequence where Element: Equatable { func removeDuplicates() -> AsyncThrowingStream { return AsyncThrowingStream { continuation in diff --git a/SessionUtilitiesKit/Utilities/AsyncStream+Utilities.swift b/SessionUtilitiesKit/Utilities/AsyncStream+Utilities.swift new file mode 100644 index 0000000000..812463b7a5 --- /dev/null +++ b/SessionUtilitiesKit/Utilities/AsyncStream+Utilities.swift @@ -0,0 +1,13 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension AsyncStream { + func first() async -> Element? { + return await first(where: { _ in true }) + } + + func first(defaultValue: Element) async -> Element { + return (await first(where: { _ in true }) ?? defaultValue) + } +} From da39851b192439d978734a4fa1fe7c49fddb7ef4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 27 Aug 2025 15:56:29 +1000 Subject: [PATCH 26/59] Moved network settings to it's own screen, added devnet support --- Session.xcodeproj/project.pbxproj | 16 +- Session/Home/HomeVC.swift | 7 +- Session/Meta/AppDelegate.swift | 11 +- Session/Meta/Session+SNUIKit.swift | 2 +- .../DeveloperNetworkSettingsViewModel.swift | 1011 +++++++++++++++++ .../DeveloperSettingsViewModel+Testing.swift | 227 ++++ .../DeveloperSettingsViewModel.swift | 281 +---- .../DeveloperSettingsViewModel+Testing.swift | 114 -- Session/Settings/SettingsViewModel.swift | 7 +- Session/Shared/Views/SessionCell.swift | 4 +- .../Models/UnsubscribeRequest.swift | 8 +- .../LibSession/LibSession+Networking.swift | 41 +- .../ShareNavController.swift | 2 +- .../Modals & Toast/ConfirmationModal.swift | 2 +- .../Dependency Injection/Dependencies.swift | 7 + .../General/Feature+ServiceNetwork.swift | 93 ++ 16 files changed, 1421 insertions(+), 412 deletions(-) create mode 100644 Session/Settings/DeveloperSettings/DeveloperNetworkSettingsViewModel.swift create mode 100644 Session/Settings/DeveloperSettings/DeveloperSettingsViewModel+Testing.swift rename Session/Settings/{ => DeveloperSettings}/DeveloperSettingsViewModel.swift (86%) delete mode 100644 Session/Settings/DeveloperSettingsViewModel+Testing.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index c5467a3cef..4864f879dd 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -988,6 +988,7 @@ FDCC22D42E5C0D9900C77B1A /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */; }; FDCC22D62E5D2ADC00C77B1A /* CancellationAwareAsyncStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCC22D52E5D2AD700C77B1A /* CancellationAwareAsyncStream.swift */; }; FDCC22D82E5D3C1400C77B1A /* AsyncStream+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCC22D72E5D3C1000C77B1A /* AsyncStream+Utilities.swift */; }; + FDCC22DB2E5E897800C77B1A /* DeveloperNetworkSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCC22DA2E5E897200C77B1A /* DeveloperNetworkSettingsViewModel.swift */; }; FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */; }; FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */; }; FDD20C162A09E64A003898FB /* GetExpiriesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C152A09E64A003898FB /* GetExpiriesRequest.swift */; }; @@ -2269,6 +2270,7 @@ FDCC22D12E56E0B600C77B1A /* StreamLifecycleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamLifecycleManager.swift; sourceTree = ""; }; FDCC22D52E5D2AD700C77B1A /* CancellationAwareAsyncStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellationAwareAsyncStream.swift; sourceTree = ""; }; FDCC22D72E5D3C1000C77B1A /* AsyncStream+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncStream+Utilities.swift"; sourceTree = ""; }; + FDCC22DA2E5E897200C77B1A /* DeveloperNetworkSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperNetworkSettingsViewModel.swift; sourceTree = ""; }; FDCCC6E82ABA7402002BBEF5 /* EmojiGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiGenerator.swift; sourceTree = ""; }; FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGroupOnlyRequest.swift; sourceTree = ""; }; FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; @@ -3443,6 +3445,7 @@ C360969125AD1765008B62B2 /* Settings */ = { isa = PBXGroup; children = ( + FDCC22D92E5E896200C77B1A /* DeveloperSettings */, FD8A5B002DBEFBF9004C689B /* SessionNetworkScreen */, FD37E9CD28A1E682003AE748 /* Views */, 9422569A2C23F8F000C0FDBF /* QRCodeScreen.swift */, @@ -3458,8 +3461,6 @@ FD860CB72D66BC9500BBE29C /* AppIconViewModel.swift */, FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */, B86BD08523399CEF000F5AE3 /* SeedModal.swift */, - FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */, - FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */, B894D0742339EDCF00B4D94D /* NukeDataModal.swift */, FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */, ); @@ -4897,6 +4898,16 @@ path = Types; sourceTree = ""; }; + FDCC22D92E5E896200C77B1A /* DeveloperSettings */ = { + isa = PBXGroup; + children = ( + FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */, + FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */, + FDCC22DA2E5E897200C77B1A /* DeveloperNetworkSettingsViewModel.swift */, + ); + path = DeveloperSettings; + sourceTree = ""; + }; FDDC08F029A300D500BF9681 /* Utilities */ = { isa = PBXGroup; children = ( @@ -6810,6 +6821,7 @@ 7B3A3934298882D6002FE4AC /* SessionCarouselViewDelegate.swift in Sources */, 45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */, 942256802C23F8BB00C0FDBF /* StartConversationScreen.swift in Sources */, + FDCC22DB2E5E897800C77B1A /* DeveloperNetworkSettingsViewModel.swift in Sources */, 7B9F71C928470667006DFE7B /* ReactionListSheet.swift in Sources */, FD12A8412AD63BEA00EEBA0D /* NavigatableState.swift in Sources */, 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */, diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index afcc1eabd0..1675a8b032 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -471,10 +471,9 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi displayPictureUrl: nil, profile: userProfile, profileIcon: { - switch (serviceNetwork, forceOffline) { - case (.testnet, false): return .letter("T", false) // stringlint:ignore - case (.testnet, true): return .letter("T", true) // stringlint:ignore - default: return .none + switch (serviceNetwork, serviceNetwork.title.first) { + case (.mainnet, _), (_, .none): return .none + case (_, .some(let letter)): return .letter(letter, forceOffline) } }(), additionalProfile: nil, diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index b5ecabe772..1bff0f5a74 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -196,11 +196,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // FIXME: Seems like there are some discrepancies between the expectations of how the iOS lifecycle methods work, we should look into them and ensure the code behaves as expected (in this case there were situations where these two wouldn't get called when returning from the background) Task { dependencies[singleton: .storage].resumeDatabaseAccess() - await dependencies[singleton: .network].resumeNetworkAccess() await ensureRootViewController(calledFrom: .didBecomeActive) } - dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [weak self] in + dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [weak self, dependencies] in + /// Wait for the app to be ready before resuming network access (this is mostly for initial launch which also calls + /// `applicationDidBecomeActive` (and if we don't want then the network can be started before any env variables + /// have been processed which could be inefficient if we immediately tear down and change the network) + Task { await dependencies[singleton: .network].resumeNetworkAccess() } + self?.handleActivation() /// Clear all notifications whenever we become active once the app is ready @@ -363,8 +367,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD var backgroundTask: SessionBackgroundTask? = SessionBackgroundTask(label: #function, using: dependencies) Log.setup(with: Logger(primaryPrefix: "Session", using: dependencies)) - LibSession.setupLogger(using: dependencies) Log.info(.cat, "Setting up environment.") + LibSession.setupLogger(using: dependencies) /// If we are running automated tests we should process environment variables before we do anything else await DeveloperSettingsViewModel.processUnitTestEnvVariablesIfNeeded(using: dependencies) @@ -436,6 +440,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } /// Now that the theme settings have been applied we can complete the migrations + Log.info(.cat, "Environment setup complete.") await self.completePostMigrationSetup(calledFrom: .finishLaunching) } catch { diff --git a/Session/Meta/Session+SNUIKit.swift b/Session/Meta/Session+SNUIKit.swift index 242f7009e3..7b2ad94bfd 100644 --- a/Session/Meta/Session+SNUIKit.swift +++ b/Session/Meta/Session+SNUIKit.swift @@ -40,7 +40,7 @@ internal struct SessionSNUIKitConfig: SNUIKit.ConfigType { func navBarSessionIcon() -> NavBarSessionIcon { switch (dependencies[feature: .serviceNetwork], dependencies[feature: .forceOffline]) { case (.mainnet, false): return NavBarSessionIcon() - case (.testnet, _), (.mainnet, true): + case (.testnet, _), (.devnet, _), (.mainnet, true): return NavBarSessionIcon( showDebugUI: true, serviceNetworkTitle: dependencies[feature: .serviceNetwork].title, diff --git a/Session/Settings/DeveloperSettings/DeveloperNetworkSettingsViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperNetworkSettingsViewModel.swift new file mode 100644 index 0000000000..116d699173 --- /dev/null +++ b/Session/Settings/DeveloperSettings/DeveloperNetworkSettingsViewModel.swift @@ -0,0 +1,1011 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import GRDB +import DifferenceKit +import SessionUIKit +import SessionNetworkingKit +import SessionMessagingKit +import SessionUtilitiesKit + +class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { + public let dependencies: Dependencies + public let navigatableState: NavigatableState = NavigatableState() + public let state: TableDataState = TableDataState() + public let observableState: ObservableTableSourceState = ObservableTableSourceState() + + private var updatedDevnetPubkey: String? + private var updatedDevnetIp: String? + private var updatedDevnetHttpPort: String? + private var updatedDevnetOmqPort: String? + + /// This value is the current state of the view + @MainActor @Published private(set) var internalState: State + private var observationTask: Task? + + // MARK: - Initialization + + @MainActor init(using dependencies: Dependencies) { + self.dependencies = dependencies + self.internalState = State.initialState(using: dependencies) + + /// Bind the state + self.observationTask = ObservationBuilder + .initialValue(self.internalState) + .debounce(for: .never) + .using(dependencies: dependencies) + .query(DeveloperNetworkSettingsViewModel.queryState) + .assign { [weak self] updatedState in + guard let self = self else { return } + + // FIXME: To slightly reduce the size of the changes this new observation mechanism is currently wired into the old SessionTableViewController observation mechanism, we should refactor it so everything uses the new mechanism + let oldState: State = self.internalState + self.internalState = updatedState + self.pendingTableDataSubject.send(updatedState.sections(viewModel: self, previousState: oldState)) + } + } + + // MARK: - Config + + public enum Section: SessionTableSection { + case general + case devnetConfig + + var title: String? { + switch self { + case .general: return nil + case .devnetConfig: return "Devnet Configuration" + } + } + + var style: SessionTableSectionStyle { + switch self { + case .general: return .padding + default: return .titleRoundedContent + } + } + } + + public enum TableItem: Hashable, Differentiable, CaseIterable { + case environment + case pushNotificationService + case forceOffline + + case devnetPubkey + case devnetIp + case devnetHttpPort + case devnetOmqPort + + // MARK: - Conformance + + public typealias DifferenceIdentifier = String + + public var differenceIdentifier: String { + switch self { + case .environment: return "environment" + case .pushNotificationService: return "pushNotificationService" + case .forceOffline: return "forceOffline" + + case .devnetPubkey: return "devnetPubkey" + case .devnetIp: return "devnetIp" + case .devnetHttpPort: return "devnetHttpPort" + case .devnetOmqPort: return "devnetOmqPort" + } + } + + public func isContentEqual(to source: TableItem) -> Bool { + self.differenceIdentifier == source.differenceIdentifier + } + + public static var allCases: [TableItem] { + var result: [TableItem] = [] + switch TableItem.environment { + case .environment: result.append(.environment); fallthrough + case .pushNotificationService: result.append(.pushNotificationService); fallthrough + case .forceOffline: result.append(.forceOffline); fallthrough + + case .devnetPubkey: result.append(.devnetPubkey); fallthrough + case .devnetIp: result.append(.devnetIp); fallthrough + case .devnetHttpPort: result.append(.devnetHttpPort); fallthrough + case .devnetOmqPort: result.append(.devnetOmqPort) + } + + return result + } + } + + // MARK: - Content + + public struct State: Equatable, ObservableKeyProvider { + struct NetworkState: Equatable, Hashable { + let environment: ServiceNetwork + let pushNotificationService: PushNotificationAPI.Service + let forceOffline: Bool + + let devnetConfig: ServiceNetwork.DevnetConfiguration + + public func with( + environment: ServiceNetwork? = nil, + pushNotificationService: PushNotificationAPI.Service? = nil, + forceOffline: Bool? = nil, + devnetConfig: ServiceNetwork.DevnetConfiguration? = nil + ) -> NetworkState { + return NetworkState( + environment: (environment ?? self.environment), + pushNotificationService: (pushNotificationService ?? self.pushNotificationService), + forceOffline: (forceOffline ?? self.forceOffline), + devnetConfig: (devnetConfig ?? self.devnetConfig) + ) + } + } + + let initialState: NetworkState + let pendingState: NetworkState + + @MainActor public func sections(viewModel: DeveloperNetworkSettingsViewModel, previousState: State) -> [SectionModel] { + DeveloperNetworkSettingsViewModel.sections( + state: self, + previousState: previousState, + viewModel: viewModel + ) + } + + public let observedKeys: Set = [ + .updateScreen(DeveloperNetworkSettingsViewModel.self) + ] + + static func initialState(using dependencies: Dependencies) -> State { + let initialState: NetworkState = NetworkState( + environment: dependencies[feature: .serviceNetwork], + pushNotificationService: dependencies[feature: .pushNotificationService], + forceOffline: dependencies[feature: .forceOffline], + devnetConfig: dependencies[feature: .devnetConfig] + ) + + return State( + initialState: initialState, + pendingState: initialState + ) + } + } + + let title: String = "Developer Network Settings" + + lazy var footerButtonInfo: AnyPublisher = $internalState + .map { [weak self] state -> SessionButton.Info? in + return SessionButton.Info( + style: .bordered, + title: "set".localized(), + isEnabled: { + guard state.initialState != state.pendingState else { return false } + + return ( + state.pendingState.environment != .devnet || + state.pendingState.devnetConfig.isValid + ) + }(), + accessibility: Accessibility( + identifier: "Set button", + label: "Set button" + ), + minWidth: 110, + onTap: { [weak self] in + Task { [weak self] in + await self?.saveChanges() + } + } + ) + } + .eraseToAnyPublisher() + + @Sendable private static func queryState( + previousState: State, + events: [ObservedEvent], + isInitialQuery: Bool, + using dependencies: Dependencies + ) async -> State { + return State( + initialState: previousState.initialState, + pendingState: (events.first?.value as? State.NetworkState ?? previousState.pendingState) + ) + } + + private static func sections( + state: State, + previousState: State, + viewModel: DeveloperNetworkSettingsViewModel + ) -> [SectionModel] { + let general: SectionModel = SectionModel( + model: .general, + elements: [ + SessionCell.Info( + id: .environment, + title: "Environment", + subtitle: """ + The environment used for sending requests and storing messages. + + Current: \(state.pendingState.environment.title) + """, + trailingAccessory: .icon(.squarePen), + onTap: { [weak viewModel] in + viewModel?.showEnvironmentModal(pendingState: state.pendingState) + } + ), + SessionCell.Info( + id: .pushNotificationService, + title: "Push Notification Service", + subtitle: """ + The service used for subscribing for push notifications. + + The production service only works for production builds and neither option works in the Simulator. + + Current: \(state.pendingState.pushNotificationService.title) + """, + trailingAccessory: .icon(.squarePen), + onTap: { [weak viewModel] in + viewModel?.showPushServiceModal(pendingState: state.pendingState) + } + ), + SessionCell.Info( + id: .forceOffline, + title: "Force Offline", + subtitle: """ + Shut down the current network and cause all future network requests to fail after a 1 second delay with a 'serviceUnavailable' error. + """, + trailingAccessory: .toggle( + state.pendingState.forceOffline, + oldValue: previousState.pendingState.forceOffline + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.notifyAsync( + priority: .immediate, + key: .updateScreen(DeveloperNetworkSettingsViewModel.self), + value: state.pendingState.with(forceOffline: !state.pendingState.forceOffline) + ) + } + ) + ] + ) + + /// Only show the `devnetConfig` section if the environment is set to `devnet` + guard state.pendingState.environment == .devnet else { + return [general] + } + + let devnetConfig: SectionModel = SectionModel( + model: .devnetConfig, + elements: [ + SessionCell.Info( + id: .devnetPubkey, + title: "Public Key", + subtitle: """ + The public key for the devnet seed node. + + Current Value: \(state.pendingState.devnetConfig.pubkey.isEmpty ? "None" : state.pendingState.devnetConfig.pubkey) + """, + trailingAccessory: .icon(.squarePen), + onTap: { [weak viewModel] in + viewModel?.showDevnetPubkeyModal(pendingState: state.pendingState) + } + ), + SessionCell.Info( + id: .devnetIp, + title: "IP Address", + subtitle: """ + The IP address for the devnet seed node. + + Current Value: \(state.pendingState.devnetConfig.ip.isEmpty ? "None" : state.pendingState.devnetConfig.ip) + """, + trailingAccessory: .icon(.squarePen), + onTap: { [weak viewModel] in + viewModel?.showDevnetIpModal(pendingState: state.pendingState) + } + ), + SessionCell.Info( + id: .devnetIp, + title: "HTTP Port", + subtitle: """ + The HTTP port for the devnet seed node. + + Current Value: \(state.pendingState.devnetConfig.httpPort) + """, + trailingAccessory: .icon(.squarePen), + onTap: { [weak viewModel] in + viewModel?.showDevnetHttpPortModal(pendingState: state.pendingState) + } + ), + SessionCell.Info( + id: .devnetIp, + title: "QUIC Port", + subtitle: """ + The QUIC port for the devnet seed node. + + Current Value: \(state.pendingState.devnetConfig.omqPort) + """, + trailingAccessory: .icon(.squarePen), + onTap: { [weak viewModel] in + viewModel?.showDevnetOmqPortModal(pendingState: state.pendingState) + } + ) + ] + ) + + return [general, devnetConfig] + } + + // MARK: - Functions + + private func showEnvironmentModal(pendingState: State.NetworkState) { + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Environment", + body: .radio( + explanation: ThemedAttributedString( + string: "The environment used for sending requests and storing messages." + ), + warning: nil, + options: ServiceNetwork.allCases.map { network in + ConfirmationModal.Info.Body.RadioOptionInfo( + title: network.title, + enabled: true, + selected: pendingState.environment == network + ) + } + ), + confirmTitle: "select".localized(), + cancelStyle: .alert_text, + onConfirm: { [dependencies] modal in + let selected: ServiceNetwork = { + switch modal.info.body { + case .radio(_, _, let options): + return options + .enumerated() + .first(where: { _, value in value.selected }) + .map { index, _ in + guard index < ServiceNetwork.allCases.count else { + return nil + } + + return ServiceNetwork.allCases[index] + } + .defaulting(to: .mainnet) + + default: return .mainnet + } + }() + + dependencies.notifyAsync( + priority: .immediate, + key: .updateScreen(DeveloperNetworkSettingsViewModel.self), + value: pendingState.with(environment: selected) + ) + } + ) + ), + transitionType: .present + ) + } + + private func showPushServiceModal(pendingState: State.NetworkState) { + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Push Notification Service", + body: .radio( + explanation: ThemedAttributedString( + string: "The service used for subscribing for push notifications." + ), + warning: ThemedAttributedString( + string: "The production service only works for production builds and neither option works in the Simulator." + ), + options: PushNotificationAPI.Service.allCases.map { network in + ConfirmationModal.Info.Body.RadioOptionInfo( + title: network.title, + enabled: true, + selected: pendingState.pushNotificationService == network + ) + } + ), + confirmTitle: "select".localized(), + cancelStyle: .alert_text, + onConfirm: { [dependencies] modal in + let selected: PushNotificationAPI.Service = { + switch modal.info.body { + case .radio(_, _, let options): + return options + .enumerated() + .first(where: { _, value in value.selected }) + .map { index, _ in + guard index < PushNotificationAPI.Service.allCases.count else { + return nil + } + + return PushNotificationAPI.Service.allCases[index] + } + .defaulting(to: .apns) + + default: return .apns + } + }() + + dependencies.notifyAsync( + priority: .immediate, + key: .updateScreen(DeveloperNetworkSettingsViewModel.self), + value: pendingState.with(pushNotificationService: selected) + ) + } + ) + ), + transitionType: .present + ) + } + + private func showDevnetPubkeyModal(pendingState: State.NetworkState) { + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Devnet Pubkey", + body: .input( + explanation: ThemedAttributedString( + string: """ + The public key for the devnet seed node. + + This is 64 character hexadecimal value. + """ + ), + info: ConfirmationModal.Info.Body.InputInfo( + placeholder: "Enter Pubkey", + initialValue: pendingState.devnetConfig.pubkey, + inputChecker: { text in + guard text.count <= 64 else { + return "Value must be a 64 character hexadecimal string." + } + + return nil + } + ), + onChange: { [weak self] value in + self?.updatedDevnetPubkey = value + } + ), + confirmTitle: "save".localized(), + confirmEnabled: .afterChange { [weak self] _ in + guard let value: String = self?.updatedDevnetPubkey else { + return false + } + + return ( + Hex.isValid(value) && + value.trimmingCharacters(in: .whitespacesAndNewlines).count == 64 + ) + }, + cancelStyle: .alert_text, + dismissOnConfirm: false, + onConfirm: { [weak self, dependencies] modal in + guard + let value: String = self?.updatedDevnetPubkey, + Hex.isValid(value), + value.trimmingCharacters(in: .whitespacesAndNewlines).count == 64 + else { + modal.updateContent( + withError: "Value must be a 64 character hexadecimal string." + ) + return + } + + modal.dismiss(animated: true) + dependencies.notifyAsync( + priority: .immediate, + key: .updateScreen(DeveloperNetworkSettingsViewModel.self), + value: pendingState.with( + devnetConfig: pendingState.devnetConfig.with( + pubkey: value + ) + ) + ) + } + ) + ), + transitionType: .present + ) + } + + private func showDevnetIpModal(pendingState: State.NetworkState) { + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Devnet IP", + body: .input( + explanation: ThemedAttributedString( + string: """ + The IP address for the devnet seed node. + + This must be in the format: '255.255.255.255' + """ + ), + info: ConfirmationModal.Info.Body.InputInfo( + placeholder: "Enter IP", + initialValue: pendingState.devnetConfig.ip + ), + onChange: { [weak self] value in self?.updatedDevnetIp = value } + ), + confirmTitle: "save".localized(), + confirmEnabled: .afterChange { [weak self] _ in + guard let value: String = self?.updatedDevnetIp else { + return false + } + + return ( + value.split(separator: ".").count == 4 && + value.split(separator: ".").allSatisfy({ part in + UInt8(part, radix: 10) != nil + }) + ) + }, + cancelStyle: .alert_text, + dismissOnConfirm: false, + onConfirm: { [weak self, dependencies] modal in + guard + let value: String = self?.updatedDevnetIp, + value.split(separator: ".").count == 4, + value.split(separator: ".").allSatisfy({ part in + UInt8(part, radix: 10) != nil + }) + else { + modal.updateContent( + withError: "Value must be a valid IP address in the format: '255.255.255.255'." + ) + return + } + + modal.dismiss(animated: true) + dependencies.notifyAsync( + priority: .immediate, + key: .updateScreen(DeveloperNetworkSettingsViewModel.self), + value: pendingState.with( + devnetConfig: pendingState.devnetConfig.with( + ip: value + ) + ) + ) + } + ) + ), + transitionType: .present + ) + } + + private func showDevnetHttpPortModal(pendingState: State.NetworkState) { + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Devnet HTTP Port", + body: .input( + explanation: ThemedAttributedString( + string: """ + The HTTP port for the devnet seed node. + + Value must be a number between 0 and 65,535. + """ + ), + info: ConfirmationModal.Info.Body.InputInfo( + placeholder: "Enter HTTP port", + initialValue: "\(pendingState.devnetConfig.httpPort)" + ), + onChange: { [weak self] value in self?.updatedDevnetHttpPort = value } + ), + confirmTitle: "save".localized(), + confirmEnabled: .afterChange { [weak self] _ in + guard let value: String = self?.updatedDevnetHttpPort else { + return false + } + + return (UInt16(value, radix: 10) != nil) + }, + cancelStyle: .alert_text, + dismissOnConfirm: false, + onConfirm: { [weak self, dependencies] modal in + guard + let value: String = self?.updatedDevnetHttpPort, + let httpPort: UInt16 = UInt16(value, radix: 10) + else { + modal.updateContent( + withError: "Value must be a number between 0 and 65,535." + ) + return + } + + modal.dismiss(animated: true) + dependencies.notifyAsync( + priority: .immediate, + key: .updateScreen(DeveloperNetworkSettingsViewModel.self), + value: pendingState.with( + devnetConfig: pendingState.devnetConfig.with( + httpPort: httpPort + ) + ) + ) + } + ) + ), + transitionType: .present + ) + } + + private func showDevnetOmqPortModal(pendingState: State.NetworkState) { + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Devnet QUIC Port", + body: .input( + explanation: ThemedAttributedString( + string: """ + The QUIC port for the devnet seed node. + + Value must be a number between 0 and 65,535. + """ + ), + info: ConfirmationModal.Info.Body.InputInfo( + placeholder: "Enter QUIC port", + initialValue: "\(pendingState.devnetConfig.omqPort)" + ), + onChange: { [weak self] value in self?.updatedDevnetOmqPort = value } + ), + confirmTitle: "save".localized(), + confirmEnabled: .afterChange { [weak self] _ in + guard let value: String = self?.updatedDevnetOmqPort else { + return false + } + + return (UInt16(value, radix: 10) != nil) + }, + cancelStyle: .alert_text, + dismissOnConfirm: false, + onConfirm: { [weak self, dependencies] modal in + guard + let value: String = self?.updatedDevnetOmqPort, + let omqPort: UInt16 = UInt16(value, radix: 10) + else { + modal.updateContent( + withError: "Value must be a number between 0 and 65,535." + ) + return + } + + modal.dismiss(animated: true) + dependencies.notifyAsync( + priority: .immediate, + key: .updateScreen(DeveloperNetworkSettingsViewModel.self), + value: pendingState.with( + devnetConfig: pendingState.devnetConfig.with( + omqPort: omqPort + ) + ) + ) + } + ) + ), + transitionType: .present + ) + } + + // MARK: - Saving + + @MainActor private func saveChanges(hasConfirmed: Bool = false) async { + guard internalState.initialState != internalState.pendingState else { return } + + let networkEnvironmentChanged: Bool = ( + internalState.initialState.environment != internalState.pendingState.environment || ( + internalState.initialState.environment == .devnet && + internalState.initialState.devnetConfig.isValid && + internalState.initialState.devnetConfig != internalState.pendingState.devnetConfig + ) + ) + let pushServiceChanged: Bool = ( + internalState.initialState.pushNotificationService != internalState.pendingState.pushNotificationService + ) + + /// Changing the network settings can result in data being cleared from the database so we should confirm that is desired before + /// we make the changes + guard hasConfirmed && (networkEnvironmentChanged || pushServiceChanged) else { + switch (networkEnvironmentChanged, pushServiceChanged) { + case (false, false): break /// Most likely just the `forceOffline` (or some new) change + case (false, true): + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Change Push Notification Service", + body: .attributedText( + ThemedAttributedString( + stringWithHTMLTags: """ + Are you sure you want to update the Push Notification Service to \(internalState.pendingState.pushNotificationService.title)? + + Warning: + This will unsubscribe from the current service and subscribe to the new service which may take a few minutes. + """, + font: ConfirmationModal.explanationFont + ), + scrollMode: .never + ), + confirmTitle: "confirm".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text, + onConfirm: { [weak self] _ in + Task { [weak self] in + await self?.saveChanges(hasConfirmed: true) + } + } + ) + ), + transitionType: .present + ) + + case (true, false): + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Change Environment", + body: .attributedText( + ThemedAttributedString( + stringWithHTMLTags: """ + Are you sure you want to change the environment to \(internalState.pendingState.environment.title)? + + Warning: + This will result in all conversation and snode data being cleared and any pending network requests being cancelled. + """, + font: ConfirmationModal.explanationFont + ), + scrollMode: .never + ), + confirmTitle: "confirm".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text, + onConfirm: { [weak self] _ in + Task { [weak self] in + await self?.saveChanges(hasConfirmed: true) + } + } + ) + ), + transitionType: .present + ) + + case (true, true): + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Change Environment", + body: .attributedText( + ThemedAttributedString( + stringWithHTMLTags: """ + Are you sure you want to change the environment to \(internalState.pendingState.environment.title) and the Push Notification Service to \(internalState.pendingState.pushNotificationService.title)? + + Warning: + This will result in all conversation and snode data being cleared and any pending network requests being cancelled. The device will unsubscribe from the current PN service and subscribe to the new service which may take a few minutes. + """, + font: ConfirmationModal.explanationFont + ), + scrollMode: .never + ), + confirmTitle: "confirm".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text, + onConfirm: { [weak self] _ in + Task { [weak self] in + await self?.saveChanges(hasConfirmed: true) + } + } + ) + ), + transitionType: .present + ) + } + return + } + + /// If the `forceOffline` value changed then apply the change + if internalState.initialState.forceOffline != internalState.pendingState.forceOffline { + dependencies.set(feature: .forceOffline, to: internalState.pendingState.forceOffline) + await dependencies[singleton: .network].setNetworkStatus( + status: internalState.pendingState.forceOffline ? .unknown : .disconnected + ) + } + + /// If the network environment changed then we should make those changes first (since they result in the database being cleared) + if networkEnvironmentChanged { + let state: State.NetworkState = internalState.pendingState + + await DeveloperNetworkSettingsViewModel.updateEnvironment( + serviceNetwork: state.environment, + devnetConfig: (state.environment == .devnet && state.devnetConfig.isValid ? + state.devnetConfig : + nil + ), + using: dependencies + ) + } + + /// Now that any environment changes have been made (which may result in rebuilding the network state, and likely clearing the + /// database) we can trigger the push service change + if pushServiceChanged && dependencies[defaults: .standard, key: .isUsingFullAPNs] { + /// Disable push notifications to trigger the unsubscribe, then re-enable them after updating the feature setting + dependencies[defaults: .standard, key: .isUsingFullAPNs] = false + + SyncPushTokensJob + .run(uploadOnlyIfStale: false, using: dependencies) + .handleEvents( + receiveOutput: { [state = internalState.pendingState, dependencies] _ in + dependencies.set( + feature: .pushNotificationService, + to: state.pushNotificationService + ) + dependencies[defaults: .standard, key: .isUsingFullAPNs] = true + } + ) + .flatMap { [dependencies] _ in + SyncPushTokensJob.run(uploadOnlyIfStale: false, using: dependencies) + } + .sinkUntilComplete() + } + + /// Changes have been saved so we can dismiss the screen + self.dismissScreen() + } + + // MARK: - Environment Changing + + internal static func updateEnvironment( + serviceNetwork: ServiceNetwork, + devnetConfig: ServiceNetwork.DevnetConfiguration?, + using dependencies: Dependencies + ) async { + struct IdentityData { + let ed25519KeyPair: KeyPair + let x25519KeyPair: KeyPair + } + + /// Make sure we are actually changing the network before clearing all of the data + guard + serviceNetwork != dependencies[feature: .serviceNetwork] || ( + serviceNetwork == .devnet && + devnetConfig?.isValid == true && + devnetConfig != dependencies[feature: .devnetConfig] + ) + else { return } + + /// Need to ensure we can retrieve the identity data before resetting everything (otherwise it'll wipe everything which we don't want) + let identityData: IdentityData + + do { + identityData = try await dependencies[singleton: .storage].readAsync(value: { db in + IdentityData( + ed25519KeyPair: KeyPair( + publicKey: Array(try Identity + .filter(Identity.Columns.variant == Identity.Variant.ed25519PublicKey) + .fetchOne(db, orThrow: StorageError.objectNotFound) + .data), + secretKey: Array(try Identity + .filter(Identity.Columns.variant == Identity.Variant.ed25519SecretKey) + .fetchOne(db, orThrow: StorageError.objectNotFound) + .data) + ), + x25519KeyPair: KeyPair( + publicKey: Array(try Identity + .filter(Identity.Columns.variant == Identity.Variant.x25519PublicKey) + .fetchOne(db, orThrow: StorageError.objectNotFound) + .data), + secretKey: Array(try Identity + .filter(Identity.Columns.variant == Identity.Variant.x25519PrivateKey) + .fetchOne(db, orThrow: StorageError.objectNotFound) + .data) + ) + ) + }) + } + catch { return Log.warn("[DevSettings] Environment change ignored due to error fetching identity data: \(error)") } + + Log.info("[DevSettings] Swapping to \(String(describing: serviceNetwork)), clearing data") + + /// Stop all pollers + dependencies.remove(singleton: .currentUserPoller) + dependencies.remove(singleton: .groupPollerManager) + dependencies.remove(singleton: .communityPollerManager) + + /// Reset the network (only if it's already been created - don't want to initialise the network if it hasn't already been started) + /// + /// **Note:** We need to set this to a `NoopNetwork` because a number of objects observe the `networkStatus` which + /// would result in automatic re-creation of the network with it's current config (since the `serviceNetwork` hasn't been updated + /// yet) + if dependencies.has(singleton: .network) { + await dependencies[singleton: .network].suspendNetworkAccess() + await dependencies[singleton: .network].finishCurrentObservations() + await dependencies[singleton: .network].clearCache() + } + + dependencies.set(singleton: .network, to: LibSession.NoopNetwork()) + + /// Unsubscribe from push notifications (do this after resetting the network as they are server requests so aren't dependant on a service + /// layer and we don't want these to be cancelled) + if let existingToken: String = try? await dependencies[singleton: .storage].readAsync(value: { db in db[.lastRecordedPushToken] }) { + Task.detached(priority: .userInitiated) { + try? await PushNotificationAPI.unsubscribeAll( + token: Data(hex: existingToken), + using: dependencies + ) + } + } + + /// Clear the snodeAPI caches + dependencies.remove(cache: .snodeAPI) + + /// Remove the libSession state (store the profile locally to maintain the name between environments) + let existingProfile: Profile = dependencies.mutate(cache: .libSession) { $0.profile } + dependencies.remove(cache: .libSession) + + /// Remove any network-specific data + try? await dependencies[singleton: .storage].writeAsync { [dependencies] db in + let userSessionId: SessionId = dependencies[cache: .general].sessionId + + _ = try SnodeReceivedMessageInfo.deleteAll(db) + _ = try SessionThread.deleteAll(db) + _ = try MessageDeduplication.deleteAll(db) + _ = try ClosedGroup.deleteAll(db) + _ = try OpenGroup.deleteAll(db) + _ = try Capability.deleteAll(db) + _ = try GroupMember.deleteAll(db) + _ = try Contact + .filter(Contact.Columns.id != userSessionId.hexString) + .deleteAll(db) + _ = try Profile + .filter(Profile.Columns.id != userSessionId.hexString) + .deleteAll(db) + _ = try BlindedIdLookup.deleteAll(db) + _ = try ConfigDump.deleteAll(db) + } + + /// Remove the `ExtensionHelper` cache + dependencies[singleton: .extensionHelper].deleteCache() + + Log.info("[DevSettings] Reloading state for \(String(describing: serviceNetwork))") + + /// Update to the new `ServiceNetwork` + dependencies.set(feature: .serviceNetwork, to: serviceNetwork) + + if let devnetConfig: ServiceNetwork.DevnetConfiguration = devnetConfig { + dependencies.set(feature: .devnetConfig, to: devnetConfig) + } + + /// Remove the temporary NoopNetwork and warm a new instance now that the `serviceNetwork` has been updated + dependencies.remove(singleton: .network) + dependencies.warm(singleton: .network) + + /// Run the onboarding process as if we are recovering an account (will setup the device in it's proper state) + let updatedOnboarding: Onboarding.Manager = Onboarding.Manager( + ed25519KeyPair: identityData.ed25519KeyPair, + x25519KeyPair: identityData.x25519KeyPair, + displayName: existingProfile.name + .nullIfEmpty + .defaulting(to: "Anonymous"), + using: dependencies + ) + dependencies.set(singleton: .onboarding, to: updatedOnboarding) + await updatedOnboarding.completeRegistration() + + /// Re-enable developer mode + dependencies.setAsync(.developerModeEnabled, true) + + /// Restart the current user poller (there won't be any other pollers though) + Task { @MainActor [poller = dependencies[singleton: .currentUserPoller]] in + await poller.startIfNeeded() + } + + /// Re-sync the push tokens (if there are any) + SyncPushTokensJob.run(uploadOnlyIfStale: false, using: dependencies).sinkUntilComplete() + + Log.info("[DevSettings] Completed swap to \(String(describing: serviceNetwork))") + } +} diff --git a/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel+Testing.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel+Testing.swift new file mode 100644 index 0000000000..e0c3b409c3 --- /dev/null +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel+Testing.swift @@ -0,0 +1,227 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import UIKit +import SessionUtilitiesKit + +// MARK: - Automated Test Convenience + +extension DeveloperSettingsViewModel { + /// Processes and sets feature flags based on environment variables when running in the iOS simulator to allow extenrally + /// triggered automated tests to start in a specific state or with specific features enabled + /// + /// In order to use these with Appium (a UI testing framework used internally) these settings can be added to the device + /// configuration as below, where the name of the value should match exactly to the `EnvironmentVariable` value + /// below and the value should match one of the options documented below + /// ``` + /// const iOSCapabilities: AppiumXCUITestCapabilities = { + /// 'appium:processArguments': { + /// env: { + /// 'serviceNetwork': 'testnet', + /// 'animationsEnabled': 'false', + /// 'debugDisappearingMessageDurations': 'true' + /// } + /// } + /// } + /// ``` + /// + /// **Note:** All values need to be provided as strings (eg. booleans) + static func processUnitTestEnvVariablesIfNeeded(using dependencies: Dependencies) async { +#if targetEnvironment(simulator) + enum EnvironmentVariable: String { + /// Disables animations for the app (where possible) + /// + /// **Value:** `true`/`false` (default: `true`) + case animationsEnabled + + /// Controls whether the "keys" for strings should be displayed instead of their localized values + /// + /// **Value:** `true`/`false` (default: `false`) + case showStringKeys + + /// Controls whether pubkeys included in the logs should be truncated or not + /// + /// **Value:** `true`/`false` (default: `true` in debug builds, `false` otherwise) + case truncatePubkeysInLogs + + /// Controls whether the app communicates with mainnet or testnet by default + /// + /// **Value:** `"mainnet"`/`"testnet"`/`"devnet"` (default: `"mainnet"`) + /// + /// **Note:** When set to `devnet` the `devnetPubkey`, `devnetIp`, `devnetHttpPort` and + /// `devnetOmqPort` values all must be provided, if any are missing then `testnet` will be used instead + case serviceNetwork + + /// Controls the pubkey which is used for the seed node when `devnet` is used + /// + /// **Value:** 64 character hex encoded public key + /// + /// **Note:** This will be ignored if `serviceNetwork` is not `devnet` + case devnetPubkey + + /// Controls the ip address which is used for the seed node when `devnet` is used + /// + /// **Value:** IP address in the form of `"255.255.255.255"` + /// + /// **Note:** This will be ignored if `serviceNetwork` is not `devnet` + case devnetIp + + /// Controls the port which is used for HTTP connections to the seed node when `devnet` is used + /// + /// **Value:** `0-65,535` + /// + /// **Note:** This will be ignored if `serviceNetwork` is not `devnet` + case devnetHttpPort + + /// Controls the port which is used for QUIC connections to the seed node when `devnet` is used + /// + /// **Value:** `0-65,535` + /// + /// **Note:** This will be ignored if `serviceNetwork` is not `devnet` + case devnetOmqPort + + /// Controls whether the app should trigger it's "Force Offline" behaviour (the network doesn't connect and all requests + /// fail after a 1 second delay with a serviceUnavailable error) + /// + /// **Value:** `true`/`false` (default: `false`) + case forceOffline + + /// Controls whether the app should offer the debug durations for disappearing messages (eg. `10s`, `30s`, etc.) + /// + /// **Value:** `true`/`false` (default: `false`) + case debugDisappearingMessageDurations + + /// Controls the number of messages that the CommunityPoller should try to retrieve every time it polls + /// + /// **Value:** `1-256` (default: `100`, a value of `0` will use the default) + case communityPollLimit + } + + let envVars: [EnvironmentVariable: String] = ProcessInfo.processInfo.environment + .reduce(into: [:]) { result, next in + guard let variable: EnvironmentVariable = EnvironmentVariable(rawValue: next.key) else { + return + } + + result[variable] = next.value + } + let allKeys: Set = Set(envVars.keys) + + for (key, value) in envVars { + switch key { + case .animationsEnabled: + dependencies.set(feature: .animationsEnabled, to: (value == "true")) + + guard value == "false" else { return } + + await UIView.setAnimationsEnabled(false) + + case .showStringKeys: + dependencies.set(feature: .showStringKeys, to: (value == "true")) + + case .truncatePubkeysInLogs: + dependencies.set(feature: .truncatePubkeysInLogs, to: (value == "true")) + + case .serviceNetwork: + let (network, devnetConfig): (ServiceNetwork, ServiceNetwork.DevnetConfiguration?) = { + switch value { + case "testnet": return (.testnet, nil) + case "devnet": + /// Ensure values were provided first + guard + let pubkey: String = envVars[.devnetPubkey], + let ip: String = envVars[.devnetIp], + let httpPort: String = envVars[.devnetHttpPort], + let omqPort: String = envVars[.devnetOmqPort] + else { + let requiredKeys: Set = [ + .devnetPubkey, + .devnetIp, + .devnetHttpPort, + .devnetOmqPort + ] + let missingKeys: Set = requiredKeys.subtracting(allKeys) + Log.warn("Using testnet as required devnet environment variables are missing: \(missingKeys.map { "'\($0.rawValue)'" }.joined(separator: ", "))") + return (.testnet, nil) + } + + /// Validate each value + var errors: [String] = [] + var finalHttpPort: UInt16 = 0 + var finalOmqPort: UInt16 = 0 + + if !Hex.isValid(pubkey) || pubkey.count != 64 { + errors.append("'devnetPubkey' must be a 64 character hex string") + } + + if + ip.split(separator: ".").count != 4 || + !ip.split(separator: ".").allSatisfy({ part in + UInt8(part, radix: 10) != nil + }) + { + errors.append("'devnetIp' must be in the format: '255.255.255.255'") + } + + if let parsedHttpPort: UInt16 = UInt16(httpPort, radix: 10) { + finalHttpPort = parsedHttpPort + } + else { + errors.append("'devnetHttpPort' must be a number between 0 and 65,535") + } + + if let parsedOmqPort: UInt16 = UInt16(omqPort, radix: 10) { + finalOmqPort = parsedOmqPort + } + else { + errors.append("'devnetOmqPort' must be a number between 0 and 65,535") + } + + guard errors.isEmpty else { + Log.warn("Using testnet environment as devnet environment variables are invalid: \(errors.map { "\($0)" }.joined(separator: ", "))") + return (.testnet, nil) + } + + /// We have a valid devnet config so use it + return ( + .devnet, + ServiceNetwork.DevnetConfiguration( + pubkey: pubkey, + ip: ip, + httpPort: finalHttpPort, + omqPort: finalOmqPort + ) + ) + + default: return (.mainnet, nil) + } + }() + + await DeveloperNetworkSettingsViewModel.updateEnvironment( + serviceNetwork: network, + devnetConfig: devnetConfig, + using: dependencies + ) + + /// These are handled in the `serviceNetwork` case + case .devnetPubkey, .devnetIp, .devnetHttpPort, .devnetOmqPort: break + + case .forceOffline: + dependencies.set(feature: .forceOffline, to: (value == "true")) + + case .debugDisappearingMessageDurations: + dependencies.set(feature: .debugDisappearingMessageDurations, to: (value == "true")) + + case .communityPollLimit: + guard + let intValue: Int = Int(value), + intValue >= 1 && intValue < 256 + else { return } + + dependencies.set(feature: .communityPollLimit, to: intValue) + } + } +#endif + } +} diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift similarity index 86% rename from Session/Settings/DeveloperSettingsViewModel.swift rename to Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift index d12e8ea2fe..e73a098497 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift @@ -88,10 +88,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case advancedLogging case loggingCategory(String) - case serviceNetwork - case forceOffline + case networkConfig case resetSnodeCache - case pushNotificationService case debugDisappearingMessageDurations @@ -129,10 +127,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .advancedLogging: return "advancedLogging" case .loggingCategory(let categoryIdentifier): return "loggingCategory-\(categoryIdentifier)" - case .serviceNetwork: return "serviceNetwork" - case .forceOffline: return "forceOffline" + case .networkConfig: return "networkConfig" case .resetSnodeCache: return "resetSnodeCache" - case .pushNotificationService: return "pushNotificationService" case .debugDisappearingMessageDurations: return "debugDisappearingMessageDurations" @@ -180,10 +176,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .advancedLogging: result.append(.advancedLogging); fallthrough case .loggingCategory: result.append(.loggingCategory("")); fallthrough - case .serviceNetwork: result.append(.serviceNetwork); fallthrough - case .forceOffline: result.append(.forceOffline); fallthrough + case .networkConfig: result.append(.networkConfig); fallthrough case .resetSnodeCache: result.append(.resetSnodeCache); fallthrough - case .pushNotificationService: result.append(.pushNotificationService); fallthrough case .debugDisappearingMessageDurations: result.append(.debugDisappearingMessageDurations); fallthrough @@ -231,10 +225,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, let advancedLogging: Bool let loggingCategories: [Log.Category: Log.Level] - let serviceNetwork: ServiceNetwork - let forceOffline: Bool - let pushNotificationService: PushNotificationAPI.Service - let debugDisappearingMessageDurations: Bool let communityPollLimit: Int @@ -286,10 +276,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, advancedLogging: (self?.showAdvancedLogging == true), loggingCategories: dependencies[feature: .allLogLevels].currentValues(using: dependencies), - serviceNetwork: dependencies[feature: .serviceNetwork], - forceOffline: dependencies[feature: .forceOffline], - pushNotificationService: dependencies[feature: .pushNotificationService], - debugDisappearingMessageDurations: dependencies[feature: .debugDisappearingMessageDurations], communityPollLimit: dependencies[feature: .communityPollLimit], @@ -498,43 +484,23 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, model: .network, elements: [ SessionCell.Info( - id: .serviceNetwork, - title: "Environment", + id: .networkConfig, + title: "Network Configuration", subtitle: """ - The environment used for sending requests and storing messages. + Configure settings related to how and where network requests are sent. - Warning: - Changing between some of these options can result in all conversation and snode data being cleared and any pending network requests being cancelled. + Service Network: \(dependencies[feature: .serviceNetwork].title) + PN Service: \(dependencies[feature: .pushNotificationService].title) """, - trailingAccessory: .dropDown { current.serviceNetwork.title }, + trailingAccessory: .icon(.chevronRight), onTap: { [weak self, dependencies] in self?.transitionToScreen( SessionTableViewController( - viewModel: SessionListViewModel( - title: "Environment", - options: ServiceNetwork.allCases, - behaviour: .autoDismiss( - initialSelection: current.serviceNetwork, - onOptionSelected: self?.updateServiceNetwork - ), - using: dependencies - ) + viewModel: DeveloperNetworkSettingsViewModel(using: dependencies) ) ) } ), - SessionCell.Info( - id: .forceOffline, - title: "Force Offline", - subtitle: """ - Shut down the current network and cause all future network requests to fail after a 1 second delay with a 'serviceUnavailable' error. - """, - trailingAccessory: .toggle( - current.forceOffline, - oldValue: previous?.forceOffline - ), - onTap: { [weak self] in self?.updateForceOffline(current: current.forceOffline) } - ), SessionCell.Info( id: .resetSnodeCache, title: "Reset Service Node Cache", @@ -543,32 +509,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, """, trailingAccessory: .highlightingBackgroundLabel(title: "Reset Cache"), onTap: { [weak self] in self?.resetServiceNodeCache() } - ), - SessionCell.Info( - id: .pushNotificationService, - title: "Push Notification Service", - subtitle: """ - The service used for subscribing for push notifications. The production service only works for production builds and neither service work in the Simulator. - - Warning: - Changing this option will result in unsubscribing from the current service and subscribing to the new service which may take a few minutes. - """, - trailingAccessory: .dropDown { current.pushNotificationService.title }, - onTap: { [weak self, dependencies] in - self?.transitionToScreen( - SessionTableViewController( - viewModel: SessionListViewModel( - title: "Push Notification Service", - options: PushNotificationAPI.Service.allCases, - behaviour: .autoDismiss( - initialSelection: current.pushNotificationService, - onOptionSelected: self?.updatePushNotificationService - ), - using: dependencies - ) - ) - ) - } ) ] ) @@ -994,32 +934,18 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, updateFlag(for: .truncatePubkeysInLogs, to: nil) - case .copyDocumentsPath: break // Not a feature + case .copyDocumentsPath: break // Not a feature case .copyAppGroupPath: break // Not a feature - case .resetSnodeCache: break // Not a feature case .createMockContacts: break // Not a feature case .exportDatabase: break // Not a feature case .importDatabase: break // Not a feature case .advancedLogging: break // Not a feature + case .networkConfig: break // Not a feature + case .resetSnodeCache: break // Not a feature case .defaultLogLevel: updateDefaulLogLevel(to: nil) // Always reset case .loggingCategory: resetLoggingCategories() // Always reset - case .serviceNetwork: - guard dependencies.hasSet(feature: .serviceNetwork) else { return } - - updateServiceNetwork(to: nil) - - case .forceOffline: - guard dependencies.hasSet(feature: .forceOffline) else { return } - - updateFlag(for: .forceOffline, to: nil) - - case .pushNotificationService: - guard dependencies.hasSet(feature: .pushNotificationService) else { return } - - updatePushNotificationService(to: nil) - case .debugDisappearingMessageDurations: guard dependencies.hasSet(feature: .debugDisappearingMessageDurations) else { return } @@ -1134,171 +1060,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, forceRefresh(type: .databaseQuery) } - private func updateServiceNetwork(to updatedNetwork: ServiceNetwork?) { - Task { - await DeveloperSettingsViewModel.updateServiceNetwork(to: updatedNetwork, using: dependencies) - await MainActor.run { forceRefresh(type: .databaseQuery) } - } - } - - private func updatePushNotificationService(to updatedService: PushNotificationAPI.Service?) { - guard - dependencies[defaults: .standard, key: .isUsingFullAPNs], - updatedService != dependencies[feature: .pushNotificationService] - else { - forceRefresh(type: .databaseQuery) - return - } - - /// Disable push notifications to trigger the unsubscribe, then re-enable them after updating the feature setting - dependencies[defaults: .standard, key: .isUsingFullAPNs] = false - - SyncPushTokensJob - .run(uploadOnlyIfStale: false, using: dependencies) - .handleEvents( - receiveOutput: { [weak self, dependencies] _ in - dependencies.set(feature: .pushNotificationService, to: updatedService) - dependencies[defaults: .standard, key: .isUsingFullAPNs] = true - - self?.forceRefresh(type: .databaseQuery) - } - ) - .flatMap { [dependencies] _ in SyncPushTokensJob.run(uploadOnlyIfStale: false, using: dependencies) } - .sinkUntilComplete() - } - - internal static func updateServiceNetwork( - to updatedNetwork: ServiceNetwork?, - using dependencies: Dependencies - ) async { - struct IdentityData { - let ed25519KeyPair: KeyPair - let x25519KeyPair: KeyPair - } - - /// Make sure we are actually changing the network before clearing all of the data - guard - updatedNetwork != dependencies[feature: .serviceNetwork], - let identityData: IdentityData = try? await dependencies[singleton: .storage].readAsync(value: { db in - IdentityData( - ed25519KeyPair: KeyPair( - publicKey: Array(try Identity - .filter(Identity.Columns.variant == Identity.Variant.ed25519PublicKey) - .fetchOne(db, orThrow: StorageError.objectNotFound) - .data), - secretKey: Array(try Identity - .filter(Identity.Columns.variant == Identity.Variant.ed25519SecretKey) - .fetchOne(db, orThrow: StorageError.objectNotFound) - .data) - ), - x25519KeyPair: KeyPair( - publicKey: Array(try Identity - .filter(Identity.Columns.variant == Identity.Variant.x25519PublicKey) - .fetchOne(db, orThrow: StorageError.objectNotFound) - .data), - secretKey: Array(try Identity - .filter(Identity.Columns.variant == Identity.Variant.x25519PrivateKey) - .fetchOne(db, orThrow: StorageError.objectNotFound) - .data) - ) - ) - }) - else { return } - - Log.info("[DevSettings] Swapping to \(String(describing: updatedNetwork)), clearing data") - - /// Stop all pollers - dependencies.remove(singleton: .currentUserPoller) - dependencies.remove(singleton: .groupPollerManager) - dependencies.remove(singleton: .communityPollerManager) - - /// Reset the network - /// - /// **Note:** We need to set this to a `NoopNetwork` because a number of objects observe the `networkStatus` which - /// would result in automatic re-creation of the network with it's current config (since the `serviceNetwork` hasn't been updated - /// yet) - await dependencies[singleton: .network].suspendNetworkAccess() - await dependencies[singleton: .network].finishCurrentObservations() - await dependencies[singleton: .network].clearCache() - dependencies.set(singleton: .network, to: LibSession.NoopNetwork()) - - /// Unsubscribe from push notifications (do this after resetting the network as they are server requests so aren't dependant on a service - /// layer and we don't want these to be cancelled) - if let existingToken: String = try? await dependencies[singleton: .storage].readAsync(value: { db in db[.lastRecordedPushToken] }) { - Task.detached(priority: .userInitiated) { - try? await PushNotificationAPI.unsubscribeAll( - token: Data(hex: existingToken), - using: dependencies - ) - } - } - - /// Clear the snodeAPI caches - dependencies.remove(cache: .snodeAPI) - - /// Remove the libSession state (store the profile locally to maintain the name between environments) - let existingProfile: Profile = dependencies.mutate(cache: .libSession) { $0.profile } - dependencies.remove(cache: .libSession) - - /// Remove any network-specific data - try? await dependencies[singleton: .storage].writeAsync { [dependencies] db in - let userSessionId: SessionId = dependencies[cache: .general].sessionId - - _ = try SnodeReceivedMessageInfo.deleteAll(db) - _ = try SessionThread.deleteAll(db) - _ = try MessageDeduplication.deleteAll(db) - _ = try ClosedGroup.deleteAll(db) - _ = try OpenGroup.deleteAll(db) - _ = try Capability.deleteAll(db) - _ = try GroupMember.deleteAll(db) - _ = try Contact - .filter(Contact.Columns.id != userSessionId.hexString) - .deleteAll(db) - _ = try Profile - .filter(Profile.Columns.id != userSessionId.hexString) - .deleteAll(db) - _ = try BlindedIdLookup.deleteAll(db) - _ = try ConfigDump.deleteAll(db) - } - - /// Remove the `ExtensionHelper` cache - dependencies[singleton: .extensionHelper].deleteCache() - - Log.info("[DevSettings] Reloading state for \(String(describing: updatedNetwork))") - - /// Update to the new `ServiceNetwork` - dependencies.set(feature: .serviceNetwork, to: updatedNetwork) - - /// Remove the temporary NoopNetwork and warm a new instance now that the `serviceNetwork` has been updated - dependencies.remove(singleton: .network) - dependencies.warm(singleton: .network) - - /// Run the onboarding process as if we are recovering an account (will setup the device in it's proper state) - let updatedOnboarding: Onboarding.Manager = Onboarding.Manager( - ed25519KeyPair: identityData.ed25519KeyPair, - x25519KeyPair: identityData.x25519KeyPair, - displayName: existingProfile.name - .nullIfEmpty - .defaulting(to: "Anonymous"), - using: dependencies - ) - dependencies.set(singleton: .onboarding, to: updatedOnboarding) - await updatedOnboarding.completeRegistration() - - /// Re-enable developer mode - dependencies.setAsync(.developerModeEnabled, true) - - /// Restart the current user poller (there won't be any other pollers though) - Task { @MainActor [poller = dependencies[singleton: .currentUserPoller]] in - await poller.startIfNeeded() - } - - /// Re-sync the push tokens (if there are any) - SyncPushTokensJob.run(uploadOnlyIfStale: false, using: dependencies).sinkUntilComplete() - - Log.info("[DevSettings] Completed swap to \(String(describing: updatedNetwork))") - } - private func updateFlag(for feature: FeatureConfig, to updatedFlag: Bool?) { /// Update to the new flag dependencies.set(feature: feature, to: updatedFlag) @@ -1315,16 +1076,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, } } - private func updateForceOffline(current: Bool) { - updateFlag(for: .forceOffline, to: !current) - - // Reset the network cache - Task { - await dependencies[singleton: .network].setNetworkStatus(status: current ? .unknown : .disconnected) - dependencies.remove(singleton: .network) - } - } - private func resetServiceNodeCache() { self.transitionToScreen( ConfirmationModal( @@ -2048,12 +1799,6 @@ final class PollLimitInputView: UIView, UITextFieldDelegate, SessionCell.Accesso // MARK: - Listable Conformance -extension ServiceNetwork: @retroactive ContentIdentifiable {} -extension ServiceNetwork: @retroactive ContentEquatable {} -extension ServiceNetwork: Listable {} -extension PushNotificationAPI.Service: @retroactive ContentIdentifiable {} -extension PushNotificationAPI.Service: @retroactive ContentEquatable {} -extension PushNotificationAPI.Service: Listable {} extension Log.Level: @retroactive ContentIdentifiable {} extension Log.Level: @retroactive ContentEquatable {} extension Log.Level: Listable {} diff --git a/Session/Settings/DeveloperSettingsViewModel+Testing.swift b/Session/Settings/DeveloperSettingsViewModel+Testing.swift deleted file mode 100644 index 8b5137801b..0000000000 --- a/Session/Settings/DeveloperSettingsViewModel+Testing.swift +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import UIKit -import SessionUtilitiesKit - -// MARK: - Automated Test Convenience - -extension DeveloperSettingsViewModel { - /// Processes and sets feature flags based on environment variables when running in the iOS simulator to allow extenrally - /// triggered automated tests to start in a specific state or with specific features enabled - /// - /// In order to use these with Appium (a UI testing framework used internally) these settings can be added to the device - /// configuration as below, where the name of the value should match exactly to the `EnvironmentVariable` value - /// below and the value should match one of the options documented below - /// ``` - /// const iOSCapabilities: AppiumXCUITestCapabilities = { - /// 'appium:processArguments': { - /// env: { - /// 'serviceNetwork': 'testnet', - /// 'animationsEnabled': 'false', - /// 'debugDisappearingMessageDurations': 'true' - /// } - /// } - /// } - /// ``` - /// - /// **Note:** All values need to be provided as strings (eg. booleans) - static func processUnitTestEnvVariablesIfNeeded(using dependencies: Dependencies) async { -#if targetEnvironment(simulator) - enum EnvironmentVariable: String { - /// Disables animations for the app (where possible) - /// - /// **Value:** `true`/`false` (default: `true`) - case animationsEnabled - - /// Controls whether the "keys" for strings should be displayed instead of their localized values - /// - /// **Value:** `true`/`false` (default: `false`) - case showStringKeys - - /// Controls whether pubkeys included in the logs should be truncated or not - /// - /// **Value:** `true`/`false` (default: `true` in debug builds, `false` otherwise) - case truncatePubkeysInLogs - - /// Controls whether the app communicates with mainnet or testnet by default - /// - /// **Value:** `"mainnet"`/`"testnet"` (default: `"mainnet"`) - case serviceNetwork - - /// Controls whether the app should trigger it's "Force Offline" behaviour (the network doesn't connect and all requests - /// fail after a 1 second delay with a serviceUnavailable error) - /// - /// **Value:** `true`/`false` (default: `false`) - case forceOffline - - /// Controls whether the app should offer the debug durations for disappearing messages (eg. `10s`, `30s`, etc.) - /// - /// **Value:** `true`/`false` (default: `false`) - case debugDisappearingMessageDurations - - /// Controls the number of messages that the CommunityPoller should try to retrieve every time it polls - /// - /// **Value:** `1-256` (default: `100`, a value of `0` will use the default) - case communityPollLimit - } - - for (key, value) in ProcessInfo.processInfo.environment { - guard let variable: EnvironmentVariable = EnvironmentVariable(rawValue: key) else { return } - - switch variable { - case .animationsEnabled: - dependencies.set(feature: .animationsEnabled, to: (value == "true")) - - guard value == "false" else { return } - - await UIView.setAnimationsEnabled(false) - - case .showStringKeys: - dependencies.set(feature: .showStringKeys, to: (value == "true")) - - case .truncatePubkeysInLogs: - dependencies.set(feature: .truncatePubkeysInLogs, to: (value == "true")) - - case .serviceNetwork: - let network: ServiceNetwork - - switch value { - case "testnet": network = .testnet - default: network = .mainnet - } - - await DeveloperSettingsViewModel.updateServiceNetwork(to: network, using: dependencies) - - case .forceOffline: - dependencies.set(feature: .forceOffline, to: (value == "true")) - - case .debugDisappearingMessageDurations: - dependencies.set(feature: .debugDisappearingMessageDurations, to: (value == "true")) - - case .communityPollLimit: - guard - let intValue: Int = Int(value), - intValue >= 1 && intValue < 256 - else { return } - - dependencies.set(feature: .communityPollLimit, to: intValue) - } - } -#endif - } -} diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index d5bd3b0d26..7ec201a480 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -260,10 +260,9 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl size: .hero, profile: state.profile, profileIcon: { - switch (state.serviceNetwork, state.forceOffline) { - case (.testnet, false): return .letter("T", false) // stringlint:ignore - case (.testnet, true): return .letter("T", true) // stringlint:ignore - default: return .none + switch (state.serviceNetwork, state.serviceNetwork.title.first) { + case (.mainnet, _), (_, .none): return .none + case (_, .some(let letter)): return .letter(letter, state.forceOffline) } }() ), diff --git a/Session/Shared/Views/SessionCell.swift b/Session/Shared/Views/SessionCell.swift index 4bcc1408f3..3360a31ecf 100644 --- a/Session/Shared/Views/SessionCell.swift +++ b/Session/Shared/Views/SessionCell.swift @@ -572,19 +572,19 @@ public class SessionCell: UITableViewCell { titleTextField.accessibilityLabel = info.title?.accessibility?.label subtitleLabel.isUserInteractionEnabled = (info.subtitle?.interaction == .copy) subtitleLabel.font = info.subtitle?.font + subtitleLabel.themeTextColor = info.styling.subtitleTintColor subtitleLabel.themeAttributedText = info.subtitle.map { subtitle -> ThemedAttributedString? in ThemedAttributedString(stringWithHTMLTags: subtitle.text, font: subtitle.font) } - subtitleLabel.themeTextColor = info.styling.subtitleTintColor subtitleLabel.textAlignment = (info.subtitle?.textAlignment ?? .left) subtitleLabel.accessibilityIdentifier = info.subtitle?.accessibility?.identifier subtitleLabel.accessibilityLabel = info.subtitle?.accessibility?.label subtitleLabel.isHidden = (info.subtitle == nil) expandableDescriptionLabel.font = info.description?.font ?? .systemFont(ofSize: 12) + expandableDescriptionLabel.themeTextColor = info.styling.descriptionTintColor expandableDescriptionLabel.themeAttributedText = info.description.map { description -> ThemedAttributedString? in ThemedAttributedString(stringWithHTMLTags: description.text, font: description.font) } - expandableDescriptionLabel.themeTextColor = info.styling.descriptionTintColor expandableDescriptionLabel.textAlignment = (info.description?.textAlignment ?? .left) expandableDescriptionLabel.accessibilityIdentifier = info.description?.accessibility?.identifier expandableDescriptionLabel.accessibilityLabel = info.description?.accessibility?.label diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift index 4f4b7da70c..0396e7d91b 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift @@ -54,10 +54,10 @@ extension PushNotificationAPI { try container.encode(serviceInfo, forKey: .serviceInfo) - // Use the correct APNS service based on the serviceNetwork (default to mainnet) - switch encoder.dependencies?[feature: .serviceNetwork] { - case .testnet: try container.encode(Service.sandbox, forKey: .service) - case .mainnet, .none: try container.encode(Service.apns, forKey: .service) + // Use the desired APNS service (default to apns) + switch encoder.dependencies?[feature: .pushNotificationService] { + case .sandbox: try container.encode(Service.sandbox, forKey: .service) + case .apns, .none: try container.encode(Service.apns, forKey: .service) } try super.encode(to: encoder) diff --git a/SessionNetworkingKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift index 6bd97c9f73..ee7c1d0644 100644 --- a/SessionNetworkingKit/LibSession/LibSession+Networking.swift +++ b/SessionNetworkingKit/LibSession/LibSession+Networking.swift @@ -472,12 +472,20 @@ actor LibSessionNetwork: NetworkType { var error: [CChar] = [CChar](repeating: 0, count: 256) var network: UnsafeMutablePointer? + var cDevnetNodes: [network_service_node] = [] var config: session_network_config = session_network_config_default() config.cache_refresh_using_legacy_endpoint = true - if dependencies[feature: .serviceNetwork] == .testnet { - config.netid = SESSION_NETWORK_TESTNET - config.enforce_subnet_diversity = false /// On testnet we can't do this as nodes share IPs + switch (dependencies[feature: .serviceNetwork], dependencies[feature: .devnetConfig], dependencies[feature: .devnetConfig].isValid) { + case (.mainnet, _, _): break + case (.testnet, _, _), (_, _, false): + config.netid = SESSION_NETWORK_TESTNET + config.enforce_subnet_diversity = false /// On testnet we can't do this as nodes share IPs + + case (.devnet, let devnetConfig, true): + config.netid = SESSION_NETWORK_DEVNET + config.enforce_subnet_diversity = false /// Devnet nodes likely share IPs as well + cDevnetNodes = [LibSession.Snode(devnetConfig).cSnode] } /// If it's not the main app then we want to run in "Single Path Mode" (no use creating extra paths in the extensions) @@ -486,11 +494,19 @@ actor LibSessionNetwork: NetworkType { } try cCachePath.withUnsafeBufferPointer { cachePtr in - config.cache_dir = cachePtr.baseAddress - - guard session_network_init(&network, &config, &error) else { - Log.error(.network, "Unable to create network object: \(String(cString: error))") - throw NetworkError.invalidState + try cDevnetNodes.withUnsafeBufferPointer { devnetNodesPtr in + config.cache_dir = cachePtr.baseAddress + + /// Only set the devnet pointers if we are in devnet mode + if config.netid == SESSION_NETWORK_DEVNET { + config.devnet_seed_nodes = devnetNodesPtr.baseAddress + config.devnet_seed_nodes_size = devnetNodesPtr.count + } + + guard session_network_init(&network, &config, &error) else { + Log.error(.network, "Unable to create network object: \(String(cString: error))") + throw NetworkError.invalidState + } } } @@ -812,6 +828,15 @@ extension LibSession { swarmId = cSnode.get(\.swarm_id) } + internal init(_ config: ServiceNetwork.DevnetConfiguration) { + self.ed25519PubkeyHex = config.pubkey + self.ip = config.ip + self.httpsPort = config.httpPort + self.omqPort = config.omqPort + self.version = "" + self.swarmId = 0 + } + internal init( ed25519PubkeyHex: String, ip: String, diff --git a/SessionShareExtension/ShareNavController.swift b/SessionShareExtension/ShareNavController.swift index 8160588d8c..92359cc47c 100644 --- a/SessionShareExtension/ShareNavController.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -770,7 +770,7 @@ private struct SAESNUIKitConfig: SNUIKit.ConfigType { func navBarSessionIcon() -> NavBarSessionIcon { switch (dependencies[feature: .serviceNetwork], dependencies[feature: .forceOffline]) { case (.mainnet, false): return NavBarSessionIcon() - case (.testnet, _), (.mainnet, true): + case (.testnet, _), (.devnet, _), (.mainnet, true): return NavBarSessionIcon( showDebugUI: true, serviceNetworkTitle: dependencies[feature: .serviceNetwork].title, diff --git a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift index 90be264afb..e60edeb422 100644 --- a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift +++ b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift @@ -549,7 +549,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { explanationLabel.isAccessibilityElement = true explanationLabel.accessibilityIdentifier = "Modal description" - explanationLabel.accessibilityLabel = explanationLabel.text + explanationLabel.accessibilityLabel = explanationLabel.text?.deformatted() } // MARK: - Error Handling diff --git a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift index ac2d7945a7..271ad0c782 100644 --- a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift +++ b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift @@ -125,6 +125,13 @@ public class Dependencies { // MARK: - Instance management + public func has(singleton: SingletonConfig) -> Bool { + let key: Dependencies.DependencyStorage.Key = DependencyStorage.Key.Variant.singleton + .key(singleton.identifier) + + return (_storage.performMap({ $0.instances[key]?.value(as: S.self) }) != nil) + } + public func warm(singleton: SingletonConfig) { _ = getOrCreate(singleton) } diff --git a/SessionUtilitiesKit/General/Feature+ServiceNetwork.swift b/SessionUtilitiesKit/General/Feature+ServiceNetwork.swift index 7ae5469b20..44514bcfd9 100644 --- a/SessionUtilitiesKit/General/Feature+ServiceNetwork.swift +++ b/SessionUtilitiesKit/General/Feature+ServiceNetwork.swift @@ -11,6 +11,10 @@ public extension FeatureStorage { identifier: "serviceNetwork", defaultOption: .mainnet ) + + static let devnetConfig: FeatureConfig = Dependencies.create( + identifier: "devnetConfig" + ) } // MARK: - ServiceNetwork Feature @@ -18,6 +22,7 @@ public extension FeatureStorage { public enum ServiceNetwork: Int, Sendable, FeatureOption, CaseIterable { case mainnet = 1 case testnet = 2 + case devnet = 3 // MARK: - Feature Option @@ -27,6 +32,7 @@ public enum ServiceNetwork: Int, Sendable, FeatureOption, CaseIterable { switch self { case .mainnet: return "Mainnet" case .testnet: return "Testnet" + case .devnet: return "Devnet" } } @@ -34,6 +40,93 @@ public enum ServiceNetwork: Int, Sendable, FeatureOption, CaseIterable { switch self { case .mainnet: return "This is the production service node network." case .testnet: return "This is the test service node network, it should be used for testing features which are currently in development and may be unstable." + case .devnet: return "This is a development service node network, it allows you to point your client at a custom service node network for testing." + } + } +} + +public extension ServiceNetwork { + struct DevnetConfiguration: Equatable, Codable, FeatureOption { + public typealias RawValue = String + + private struct Values: Equatable, Codable { + public let pubkey: String + public let ip: String + public let httpPort: UInt16 + public let omqPort: UInt16 + } + + public static let defaultOption: DevnetConfiguration = DevnetConfiguration( + pubkey: "", + ip: "", + httpPort: 0, + omqPort: 0 + ) + + public let title: String = "Devnet Configuration" + public let subtitle: String? = nil + private let values: Values + + public var pubkey: String { values.pubkey } + public var ip: String { values.ip } + public var httpPort: UInt16 { values.httpPort } + public var omqPort: UInt16 { values.omqPort } + public var isValid: Bool { + let pubkeyValid: Bool = ( + Hex.isValid(values.pubkey) && + values.pubkey.count == 64 + ) + let ipValid: Bool = ( + values.ip.split(separator: ".").count == 4 && + values.ip.split(separator: ".").allSatisfy({ part in + UInt8(part, radix: 10) != nil + }) + ) + + /// The `httpPort` and `omqPort` values are valid by default due to type safety + return (pubkeyValid && ipValid) + } + + /// This is needed to conform to `FeatureOption` so it can be saved to `UserDefaults` + public var rawValue: String { + (try? JSONEncoder().encode(values)).map { String(data: $0, encoding: .utf8) } ?? "" + } + + // MARK: - Initialization + + public init(pubkey: String, ip: String, httpPort: UInt16, omqPort: UInt16) { + self.values = Values(pubkey: pubkey, ip: ip, httpPort: httpPort, omqPort: omqPort) + } + + public init?(rawValue: String) { + guard + let data: Data = rawValue.data(using: .utf8), + let decodedValues: Values = try? JSONDecoder().decode(Values.self, from: data) + else { return nil } + + self.values = decodedValues + } + + // MARK: - Functions + + public func with( + pubkey: String? = nil, + ip: String? = nil, + httpPort: UInt16? = nil, + omqPort: UInt16? = nil + ) -> DevnetConfiguration { + return DevnetConfiguration( + pubkey: (pubkey ?? self.values.pubkey), + ip: (ip ?? self.values.ip), + httpPort: (httpPort ?? self.values.httpPort), + omqPort: (omqPort ?? self.values.omqPort) + ) + } + + // MARK: - Equality + + public static func == (lhs: DevnetConfiguration, rhs: DevnetConfiguration) -> Bool { + return (lhs.values == rhs.values) } } } From 4b41a29bb040b411dc2c67e0f333db5e41c06608 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 27 Aug 2025 15:56:56 +1000 Subject: [PATCH 27/59] Fixed an issue where the libSession loggers weren't setup correctly --- .../LibSession/LibSession.swift | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/SessionUtilitiesKit/LibSession/LibSession.swift b/SessionUtilitiesKit/LibSession/LibSession.swift index e787e7b4db..b0361ae41b 100644 --- a/SessionUtilitiesKit/LibSession/LibSession.swift +++ b/SessionUtilitiesKit/LibSession/LibSession.swift @@ -29,30 +29,32 @@ extension LibSession { Log.Category.create("config", defaultLevel: .info) Log.Category.create("network", defaultLevel: .info) - /// Subscribe for log level changes (this wil' emit an initial event which we can use to set the default log level) - ObservationBuilder.observe(.featureGroup(.allLogLevels), using: dependencies) { [dependencies] _ in - let currentLogLevels: [Log.Category: Log.Level] = dependencies[feature: .allLogLevels] - .currentValues(using: dependencies) - let currentGroupLogLevels: [Log.Group: Log.Level] = dependencies[feature: .allLogLevels] - .currentValues(using: dependencies) - let targetDefault: Log.Level? = min( - (currentLogLevels[.default] ?? .off), - (currentGroupLogLevels[.libSession] ?? .off) - ) - let cDefaultLevel: LOG_LEVEL = (targetDefault?.libSession ?? LOG_LEVEL_OFF) - session_logger_set_level_default(cDefaultLevel) - session_logger_reset_level(cDefaultLevel) - - /// Update all explicit log levels (we don't want to register a listener for each individual one so just re-apply all) - /// - /// If the conversation to the libSession `LOG_LEVEL` fails then it means we should use the default log level - currentLogLevels.forEach { (category: Log.Category, level: Log.Level) in - guard - let cCat: [CChar] = category.rawValue.cString(using: .utf8), - let cLogLevel: LOG_LEVEL = level.libSession - else { return } + /// Subscribe for log level changes (this will emit an initial event which we can use to set the default log level) + Task { + for await _ in dependencies.stream(feature: .allLogLevels) { + let currentLogLevels: [Log.Category: Log.Level] = dependencies[feature: .allLogLevels] + .currentValues(using: dependencies) + let currentGroupLogLevels: [Log.Group: Log.Level] = dependencies[feature: .allLogLevels] + .currentValues(using: dependencies) + let targetDefault: Log.Level? = min( + (currentLogLevels[.default] ?? .off), + (currentGroupLogLevels[.libSession] ?? .off) + ) + let cDefaultLevel: LOG_LEVEL = (targetDefault?.libSession ?? LOG_LEVEL_OFF) + session_logger_set_level_default(cDefaultLevel) + session_logger_reset_level(cDefaultLevel) - session_logger_set_level(cCat, cLogLevel) + /// Update all explicit log levels (we don't want to register a listener for each individual one so just re-apply all) + /// + /// If the conversation to the libSession `LOG_LEVEL` fails then it means we should use the default log level + currentLogLevels.forEach { (category: Log.Category, level: Log.Level) in + guard + let cCat: [CChar] = category.rawValue.cString(using: .utf8), + let cLogLevel: LOG_LEVEL = level.libSession + else { return } + + session_logger_set_level(cCat, cLogLevel) + } } } From 4bbee07a60b6ad34e16cbb62392ba150152579f0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 28 Aug 2025 16:31:42 +1000 Subject: [PATCH 28/59] Fixed a couple of networking bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added code to prevent the background poller building all paths (will only build required paths) • Fixed a bug where pollers wouldn't restart after returning from the background --- Session/Meta/AppDelegate.swift | 4 ++-- .../Sending & Receiving/Pollers/CommunityPoller.swift | 3 +-- .../Pollers/CurrentUserPoller.swift | 1 - .../Sending & Receiving/Pollers/GroupPoller.swift | 3 +-- .../Sending & Receiving/Pollers/PollerType.swift | 1 + .../LibSession/LibSession+Networking.swift | 11 +++++++---- SessionNetworkingKit/Types/Network.swift | 8 ++++++-- 7 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 1bff0f5a74..c5ed5daec8 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -166,7 +166,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD Task { await dependencies[singleton: .network].suspendNetworkAccess() dependencies[singleton: .storage].suspendDatabaseAccess() - Log.info(.cat, "completed network and database shutdowns.") + Log.info(.cat, "Completed network and database shutdowns.") Log.flush() } } @@ -274,7 +274,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD Log.info(.backgroundPoller, "Starting background fetch.") Task { dependencies[singleton: .storage].resumeDatabaseAccess() - await dependencies[singleton: .network].resumeNetworkAccess() + await dependencies[singleton: .network].resumeNetworkAccess(autoReconnect: false) } let queue: DispatchQueue = DispatchQueue(label: "com.session.backgroundPoll") diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift index a09bb0c4dc..fdaa4cd967 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift @@ -92,10 +92,9 @@ public actor CommunityPoller: PollerType { failureCount: Int, shouldStoreMessages: Bool, logStartAndStopCalls: Bool, - customAuthMethod: AuthenticationMethod? = nil, + customAuthMethod: AuthenticationMethod?, using dependencies: Dependencies ) { - let (stream, continuation) = AsyncStream.makeStream() self.dependencies = dependencies self.pollerName = pollerName self.destination = destination diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift index 93149aa1aa..e6cdb9793c 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift @@ -69,7 +69,6 @@ public final actor CurrentUserPoller: SwarmPollerType { customAuthMethod: AuthenticationMethod?, using dependencies: Dependencies ) { - let (stream, continuation) = AsyncStream.makeStream() self.dependencies = dependencies self.pollerName = pollerName self.destination = destination diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift index 8c9006ee28..29c40348e4 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift @@ -66,7 +66,6 @@ public actor GroupPoller: SwarmPollerType { customAuthMethod: AuthenticationMethod?, using dependencies: Dependencies ) { - let (stream, continuation) = AsyncStream.makeStream() self.dependencies = dependencies self.pollerName = pollerName self.destination = destination @@ -78,9 +77,9 @@ public actor GroupPoller: SwarmPollerType { ) self.namespaces = namespaces self.failureCount = failureCount - self.customAuthMethod = customAuthMethod self.shouldStoreMessages = shouldStoreMessages self.logStartAndStopCalls = logStartAndStopCalls + self.customAuthMethod = customAuthMethod } deinit { diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift index 4fbaab59e1..ec1cfcb87b 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift @@ -116,6 +116,7 @@ public extension PollerType { func stop() { pollTask?.cancel() + pollTask = nil if logStartAndStopCalls { Log.info(.poller, "Stopped \(pollerName).") diff --git a/SessionNetworkingKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift index ee7c1d0644..8492656413 100644 --- a/SessionNetworkingKit/LibSession/LibSession+Networking.swift +++ b/SessionNetworkingKit/LibSession/LibSession+Networking.swift @@ -30,6 +30,7 @@ actor LibSessionNetwork: NetworkType { private let dependenciesPtr: UnsafeMutableRawPointer private var network: UnsafeMutablePointer? = nil nonisolated private let internalNetworkStatus: CurrentValueAsyncStream = CurrentValueAsyncStream(.unknown) + private let singlePathMode: Bool public private(set) var isSuspended: Bool = false nonisolated public var networkStatus: AsyncStream { internalNetworkStatus.stream } @@ -43,9 +44,10 @@ actor LibSessionNetwork: NetworkType { // MARK: - Initialization - init(using dependencies: Dependencies) { + init(singlePathMode: Bool, using dependencies: Dependencies) { self.dependencies = dependencies self.dependenciesPtr = Unmanaged.passRetained(dependencies).toOpaque() + self.singlePathMode = singlePathMode self.syncDependencies = dependencies /// Create the network object @@ -427,14 +429,14 @@ actor LibSessionNetwork: NetworkType { } } - public func resumeNetworkAccess() async { + public func resumeNetworkAccess(autoReconnect: Bool) async { isSuspended = false syncState.update(isSuspended: false) Log.info(.network, "Network access resumed.") switch network { case .none: break - case .some(let network): session_network_resume(network) + case .some(let network): session_network_resume(network, autoReconnect) } } @@ -475,6 +477,7 @@ actor LibSessionNetwork: NetworkType { var cDevnetNodes: [network_service_node] = [] var config: session_network_config = session_network_config_default() config.cache_refresh_using_legacy_endpoint = true + config.onionreq_single_path_mode = singlePathMode switch (dependencies[feature: .serviceNetwork], dependencies[feature: .devnetConfig], dependencies[feature: .devnetConfig].isValid) { case (.mainnet, _, _): break @@ -1124,7 +1127,7 @@ public extension LibSession { public func setNetworkStatus(status: NetworkStatus) async {} public func suspendNetworkAccess() async {} - public func resumeNetworkAccess() async {} + public func resumeNetworkAccess(autoReconnect: Bool) async {} public func finishCurrentObservations() async {} public func clearCache() async {} } diff --git a/SessionNetworkingKit/Types/Network.swift b/SessionNetworkingKit/Types/Network.swift index 1e622c342b..ee2b3358ef 100644 --- a/SessionNetworkingKit/Types/Network.swift +++ b/SessionNetworkingKit/Types/Network.swift @@ -11,7 +11,7 @@ import SessionUtilitiesKit public extension Singleton { static let network: SingletonConfig = Dependencies.create( identifier: "network", - createInstance: { dependencies in LibSessionNetwork(using: dependencies) } + createInstance: { dependencies in LibSessionNetwork(singlePathMode: false, using: dependencies) } ) } @@ -50,12 +50,16 @@ public protocol NetworkType { func setNetworkStatus(status: NetworkStatus) async func suspendNetworkAccess() async - func resumeNetworkAccess() async + func resumeNetworkAccess(autoReconnect: Bool) async func finishCurrentObservations() async func clearCache() async } public extension NetworkType { + func resumeNetworkAccess() async { + await resumeNetworkAccess(autoReconnect: true) + } + func checkClientVersion(ed25519SecretKey: [UInt8]) async throws -> AppVersionResponse { return try await checkClientVersion(ed25519SecretKey: ed25519SecretKey).value } From 98bcadb4818798675e33071513d5bb4d5d0e361a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 3 Sep 2025 08:19:54 +1000 Subject: [PATCH 29/59] Fixed an issue where attachment files may not get deleted immediately --- .../Database/Models/Interaction.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index 43b0dfdcb7..c8f4d53c99 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -1458,6 +1458,21 @@ public extension Interaction { db.addAttachmentEvent(id: info.attachmentId, messageId: info.interactionId, type: .deleted) } + /// Add the garbage collection job to delete orphaned attachment files + if !attachments.isEmpty { + dependencies[singleton: .jobRunner].add( + db, + job: Job( + variant: .garbageCollection, + behaviour: .runOnce, + details: GarbageCollectionJob.Details( + typesToCollect: [.orphanedAttachmentFiles] + ) + ), + canStartJob: true + ) + } + /// Delete the reactions from the database _ = try Reaction .filter(interactionIds.contains(Reaction.Columns.interactionId)) From 78939449a0971caeea106ae733c49733aa850bfd Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 3 Sep 2025 08:24:50 +1000 Subject: [PATCH 30/59] Fixed build issues and bumped version number --- Session.xcodeproj/project.pbxproj | 48 ++++++----------------------- Session/Onboarding/Onboarding.swift | 4 +-- 2 files changed, 11 insertions(+), 41 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 4b4e41aed9..6b3a3dee2b 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -249,7 +249,6 @@ B8856D72256F1421001CE70E /* OWSWindowManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; B8856DE6256F15F2001CE70E /* String+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB3F255A580C00E217F9 /* String+SSK.swift */; }; B8856E09256F1676001CE70E /* UIDevice+featureSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF237255B6D65007E1867 /* UIDevice+featureSupport.swift */; }; - B886B4A72398B23E00211ABE /* (null) in Sources */ = {isa = PBXBuildFile; }; B886B4A92398BA1500211ABE /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B886B4A82398BA1500211ABE /* QRCode.swift */; }; B88FA7F2260C3EB10049422F /* OpenGroupSuggestionGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88FA7F1260C3EB10049422F /* OpenGroupSuggestionGrid.swift */; }; B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; }; @@ -690,7 +689,6 @@ FD49E2492B05C1D500FFBBB5 /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */; }; FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4B200D283492210034334B /* InsetLockableTableView.swift */; }; FD4BB22B2D63F20700D0DC3D /* MigrationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4BB22A2D63F20600D0DC3D /* MigrationHelper.swift */; }; - FD4BB22C2D63FA8600D0DC3D /* (null) in Sources */ = {isa = PBXBuildFile; }; FD4C4E9C2B02E2A300C72199 /* DisplayPictureError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C4E9B2B02E2A300C72199 /* DisplayPictureError.swift */; }; FD4C53AF2CC1D62E003B10F4 /* _035_ReworkRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C53AE2CC1D61E003B10F4 /* _035_ReworkRecipientState.swift */; }; FD52090328B4680F006098F6 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090228B4680F006098F6 /* RadioButton.swift */; }; @@ -986,9 +984,7 @@ FDC498B92AC15FE300EDD897 /* AppNotificationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC498B82AC15FE300EDD897 /* AppNotificationAction.swift */; }; FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC6D75F2862B3F600B04575 /* Dependencies.swift */; }; FDCC22D02E52E46000C77B1A /* GroupAuthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCC22CF2E52E45A00C77B1A /* GroupAuthData.swift */; }; - FDCC22D22E56E0BC00C77B1A /* StreamLifecycleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCC22D12E56E0B600C77B1A /* StreamLifecycleManager.swift */; }; FDCC22D42E5C0D9900C77B1A /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */; }; - FDCC22D62E5D2ADC00C77B1A /* CancellationAwareAsyncStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCC22D52E5D2AD700C77B1A /* CancellationAwareAsyncStream.swift */; }; FDCC22D82E5D3C1400C77B1A /* AsyncStream+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCC22D72E5D3C1000C77B1A /* AsyncStream+Utilities.swift */; }; FDCC22DB2E5E897800C77B1A /* DeveloperNetworkSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCC22DA2E5E897200C77B1A /* DeveloperNetworkSettingsViewModel.swift */; }; FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */; }; @@ -1085,12 +1081,6 @@ FDE755202C9BC1A6002A2623 /* CacheConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7551F2C9BC1A6002A2623 /* CacheConfig.swift */; }; FDE755222C9BC1BA002A2623 /* LibSessionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755212C9BC1BA002A2623 /* LibSessionError.swift */; }; FDE755242C9BC1D1002A2623 /* Publisher+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755232C9BC1D1002A2623 /* Publisher+Utilities.swift */; }; - FDEF57212C3CF03A00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57222C3CF03D00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57232C3CF04300131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57242C3CF04700131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57252C3CF04C00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57262C3CF05F00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; FDEF573E2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDEF573D2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift */; }; FDEF57712C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 in Resources */ = {isa = PBXBuildFile; fileRef = FDEF57702C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 */; }; FDF01FAD2A9ECC4200CAF969 /* SingletonConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF01FAC2A9ECC4200CAF969 /* SingletonConfig.swift */; }; @@ -4628,14 +4618,6 @@ path = SessionNetworkScreen; sourceTree = ""; }; - FD8A5B232DC05A0E004C689B /* Recovered References */ = { - isa = PBXGroup; - children = ( - 941375BE2D5196D10058F244 /* Number+Utilities.swift */, - ); - name = "Recovered References"; - sourceTree = ""; - }; FD8ECF7529340F4800C0D1BB /* LibSession */ = { isa = PBXGroup; children = ( @@ -5491,7 +5473,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; DefaultBuildSystemTypeForWorkspace = Original; - LastSwiftUpdateCheck = 1520; + LastSwiftUpdateCheck = 1630; LastTestingUpgradeCheck = 0600; LastUpgradeCheck = 1600; ORGANIZATIONNAME = "Rangeproof Pty Ltd"; @@ -6299,7 +6281,6 @@ FD2272C72C34EAF5004D8A6C /* ColumnExpressible.swift in Sources */, FDB3486E2BE8457F00B716C2 /* BackgroundTaskManager.swift in Sources */, 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */, - FDCC22D62E5D2ADC00C77B1A /* CancellationAwareAsyncStream.swift in Sources */, FD428B192B4B576F006D0888 /* AppContext.swift in Sources */, FD2272D42C34ECE1004D8A6C /* BencodeEncoder.swift in Sources */, FDE755052C9BB4EE002A2623 /* BencodeDecoder.swift in Sources */, @@ -6337,7 +6318,6 @@ FD3765EA2ADE37B400DC1489 /* Authentication.swift in Sources */, FDE755202C9BC1A6002A2623 /* CacheConfig.swift in Sources */, FD1A553E2E14BE11003761E4 /* PagedData.swift in Sources */, - FD4BB22C2D63FA8600D0DC3D /* (null) in Sources */, FDE755192C9BC169002A2623 /* UIImage+Utilities.swift in Sources */, C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */, FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */, @@ -6375,7 +6355,6 @@ FDB3DA862E1E1F0E00148F8D /* TaskCancellation.swift in Sources */, FDE754CC2C9BAF37002A2623 /* MediaUtils.swift in Sources */, FDE754DE2C9BAF8A002A2623 /* Crypto+SessionUtilitiesKit.swift in Sources */, - FDCC22D22E56E0BC00C77B1A /* StreamLifecycleManager.swift in Sources */, FDF2220F281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift in Sources */, FD848B9A28442CE6000E298B /* StorageError.swift in Sources */, FDE755222C9BC1BA002A2623 /* LibSessionError.swift in Sources */, @@ -6723,7 +6702,6 @@ B877E24626CA13BA0007970A /* CallVC+Camera.swift in Sources */, 454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */, FD3FAB5F2AE9BC2200DC5421 /* EditGroupViewModel.swift in Sources */, - FDEF57222C3CF03D00131302 /* (null) in Sources */, 7BA37AFB2AEB64CA002438F8 /* DisappearingMessageTimerView.swift in Sources */, FD12A8492AD63C4700EEBA0D /* SessionNavItem.swift in Sources */, FD12A83D2AD63BCC00EEBA0D /* EditableState.swift in Sources */, @@ -6753,7 +6731,6 @@ B886B4A92398BA1500211ABE /* QRCode.swift in Sources */, 34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */, FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */, - FDEF57262C3CF05F00131302 /* (null) in Sources */, 3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */, 7BA37AFD2AEF7C3D002438F8 /* VoiceMessageView_SwiftUI.swift in Sources */, 7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */, @@ -6797,7 +6774,6 @@ 7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */, FD71163F28E2C82C00B47552 /* SessionHeaderView.swift in Sources */, B877E24226CA12910007970A /* CallVC.swift in Sources */, - FDEF57232C3CF04300131302 /* (null) in Sources */, FDC498B92AC15FE300EDD897 /* AppNotificationAction.swift in Sources */, FD7443402D07A25C00862443 /* PushRegistrationManager.swift in Sources */, 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */, @@ -6831,7 +6807,6 @@ 7BAF54CF27ACCEEC003D12F8 /* GlobalSearchViewController.swift in Sources */, FD37EA1728AC5605003AE748 /* NotificationContentViewModel.swift in Sources */, FD3FAB612AEA194E00DC5421 /* UserListViewModel.swift in Sources */, - B886B4A72398B23E00211ABE /* (null) in Sources */, 94CD96322E1B88C20097754D /* ExpandingAttachmentsButton.swift in Sources */, 94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */, 4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */, @@ -6872,13 +6847,11 @@ 7B9F71D42852EEE2006DFE7B /* Emoji+Name.swift in Sources */, 4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */, C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */, - FDEF57242C3CF04700131302 /* (null) in Sources */, 9479981C2DD44ADC008F5CD5 /* ThreadNotificationSettingsViewModel.swift in Sources */, 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, 9422568C2C23F8C800C0FDBF /* DisplayNameScreen.swift in Sources */, 7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */, FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */, - FDEF57212C3CF03A00131302 /* (null) in Sources */, 7B9F71D32852EEE2006DFE7B /* Emoji.swift in Sources */, C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */, 3488F9362191CC4000E524CC /* MediaView.swift in Sources */, @@ -6918,7 +6891,6 @@ 940943402C7ED62300D9D2E0 /* StartupError.swift in Sources */, FD368A6A29DE9E30000DBF1E /* UIContextualAction+Utilities.swift in Sources */, 947D7FDE2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift in Sources */, - FDEF57252C3CF04C00131302 /* (null) in Sources */, 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */, FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */, FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */, @@ -8178,7 +8150,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 626; + CURRENT_PROJECT_VERSION = 627; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8218,7 +8190,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.2; + MARKETING_VERSION = 2.15.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-Werror=protocol"; OTHER_SWIFT_FLAGS = "-D DEBUG -Xfrontend -warn-long-expression-type-checking=100"; @@ -8259,7 +8231,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 626; + CURRENT_PROJECT_VERSION = 627; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8294,7 +8266,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.2; + MARKETING_VERSION = 2.15.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -8740,7 +8712,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 626; + CURRENT_PROJECT_VERSION = 627; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8779,7 +8751,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.2; + MARKETING_VERSION = 2.15.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-fobjc-arc-exceptions", @@ -8797,7 +8769,6 @@ FDC605732C71DC03009B3D45 /* Debug_Compile_LibSession */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; @@ -9327,7 +9298,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 626; + CURRENT_PROJECT_VERSION = 627; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -9360,7 +9331,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.14.2; + MARKETING_VERSION = 2.15.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -9379,7 +9350,6 @@ FDC605802C71DC14009B3D45 /* App_Store_Release_Compile_LibSession */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = NO; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 8b174f8fcf..ea6d77a213 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -133,7 +133,7 @@ extension Onboarding { public func loadInitialState() async throws { /// Try to load the users `ed25519SecretKey` from the general cache and generate the key pairs from it let ed25519SecretKey: [UInt8] = dependencies[cache: .general].ed25519SecretKey - let ed25519KeyPair: KeyPair = { + ed25519KeyPair = { guard !ed25519SecretKey.isEmpty, let ed25519Seed: Data = dependencies[singleton: .crypto].generate( @@ -146,7 +146,7 @@ extension Onboarding { return ed25519KeyPair }() - let x25519KeyPair: KeyPair = { + x25519KeyPair = { guard ed25519KeyPair != .empty, let x25519PublicKey: [UInt8] = dependencies[singleton: .crypto].generate( From e848640a865ce40da58b2cb7dd654133331b1854 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 3 Sep 2025 10:08:42 +1000 Subject: [PATCH 31/59] Fixed some issues with archiving `Compile_LibSessionUtil` builds --- Scripts/build_libSession_util.sh | 59 +++++++++++++++++++------ Session.xcodeproj/project.pbxproj | 72 ++++++++++++++++++++++++------- 2 files changed, 104 insertions(+), 27 deletions(-) diff --git a/Scripts/build_libSession_util.sh b/Scripts/build_libSession_util.sh index 4b5131ee0c..beea36f663 100755 --- a/Scripts/build_libSession_util.sh +++ b/Scripts/build_libSession_util.sh @@ -12,7 +12,6 @@ COMPILE_DIR="${TARGET_BUILD_DIR}/LibSessionUtil" INDEX_DIR="${DERIVED_DATA_PATH}/Index.noindex/Build/Products/Debug-${PLATFORM_NAME}" LAST_SUCCESSFUL_HASH_FILE="${TARGET_BUILD_DIR}/last_successful_source_tree.hash.log" LAST_BUILT_FRAMEWORK_SLICE_DIR_FILE="${TARGET_BUILD_DIR}/last_built_framework_slice_dir.log" -BUILT_LIB_FINAL_TIMESTAMP_FILE="${TARGET_BUILD_DIR}/libsession_util_built.timestamp" # Save original stdout and set trap for cleanup exec 3>&1 @@ -35,17 +34,35 @@ remove_locked_dir() { sync_headers() { local source_dir="$1" echo "- Syncing headers from ${source_dir}" - remove_locked_dir "${TARGET_BUILD_DIR}/include" - remove_locked_dir "${INDEX_DIR}/include" - # Ensure destination parent directories exist - mkdir -p "${TARGET_BUILD_DIR}/include" - mkdir -p "${INDEX_DIR}/include" - - rsync -rtc --delete --exclude='.DS_Store' "${source_dir}/" "${TARGET_BUILD_DIR}/include/" - rsync -rtc --delete --exclude='.DS_Store' "${source_dir}/" "${INDEX_DIR}/include/" + local destinations=( + "${TARGET_BUILD_DIR}/include" + "${INDEX_DIR}/include" + "${BUILT_PRODUCTS_DIR}/include" + "${CONFIGURATION_BUILD_DIR}/include" + ) + + for dest in "${destinations[@]}"; do + if [ -n "$dest" ]; then + remove_locked_dir "$dest" + mkdir -p "$dest" + rsync -rtc --delete --exclude='.DS_Store' "${source_dir}/" "$dest/" + echo " Synced to: $dest" + fi + done } +# Modify the platform detection to handle archive builds +if [ "${ACTION}" = "install" ] || [ "${CONFIGURATION}" = "Release" ]; then + # Archive builds typically use 'install' action + if [ -z "$PLATFORM_NAME" ]; then + # During archive, PLATFORM_NAME might not be set correctly + # Default to device build for archives + PLATFORM_NAME="iphoneos" + echo "Missing 'PLATFORM_NAME' value, manually set to ${PLATFORM_NAME}" + fi +fi + # Determine whether we want to build from source TARGET_ARCH_DIR="" @@ -61,6 +78,10 @@ fi if [ "${COMPILE_LIB_SESSION}" != "YES" ]; then echo "Using pre-packaged SessionUtil" sync_headers "${PRE_BUILT_FRAMEWORK_DIR}/${FRAMEWORK_DIR}/${TARGET_ARCH_DIR}/Headers/" + + # Create the placeholder in the FINAL products directory to satisfy dependency. + touch "${BUILT_PRODUCTS_DIR}/libsession-util.a" + echo "- Revert to SPM complete." exit 0 @@ -150,6 +171,16 @@ else fi if [ "${REQUIRES_BUILD}" == 1 ]; then + +# # Hide the SPM framework to prevent module conflicts +# if [ -d "${PRE_BUILT_FRAMEWORK_DIR}" ]; then +# # Temporarily rename the SPM framework to prevent it from being found +# mv "${PRE_BUILT_FRAMEWORK_DIR}" "${PRE_BUILT_FRAMEWORK_DIR}.disabled" 2>/dev/null || true +# +# # Store that we disabled it so we can restore if build fails +# echo "DISABLED" > "${TARGET_BUILD_DIR}/.spm_framework_disabled" +# fi + # Import settings from XCode (defaulting values if not present) VALID_SIM_ARCHS=(arm64 x86_64) VALID_DEVICE_ARCHS=(arm64) @@ -335,9 +366,6 @@ if [ "${REQUIRES_BUILD}" == 1 ]; then echo "- Saving successful build cache files" echo "${TARGET_ARCH_DIR}" > "${LAST_BUILT_FRAMEWORK_SLICE_DIR_FILE}" echo "${CURRENT_SOURCE_TREE_HASH}" > "${LAST_SUCCESSFUL_HASH_FILE}" - - echo "- Touching timestamp file to signal update to Xcode" - touch "${BUILT_LIB_FINAL_TIMESTAMP_FILE}" echo "- Build complete" fi @@ -347,6 +375,13 @@ echo "- Replacing build dir files" # Rsync the compiled ones (maintaining timestamps) rm -rf "${TARGET_BUILD_DIR}/libsession-util.a" rsync -rt "${COMPILE_DIR}/libsession-util.a" "${TARGET_BUILD_DIR}/libsession-util.a" + +if [ "${TARGET_BUILD_DIR}" != "${BUILT_PRODUCTS_DIR}" ]; then + echo "- TARGET_BUILD_DIR and BUILT_PRODUCTS_DIR are different. Copying library." + rm -f "${BUILT_PRODUCTS_DIR}/libsession-util.a" + rsync -rt "${COMPILE_DIR}/libsession-util.a" "${BUILT_PRODUCTS_DIR}/libsession-util.a" +fi + sync_headers "${COMPILE_DIR}/Headers/" echo "- Sync complete." diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 6b3a3dee2b..68c96a8678 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -718,9 +718,6 @@ FD65318B2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; FD65318C2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; FD65318D2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; - FD6673F62D7021E700041530 /* SessionUtil in Frameworks */ = {isa = PBXBuildFile; productRef = FD6673F52D7021E700041530 /* SessionUtil */; }; - FD6673F82D7021F200041530 /* SessionUtil in Frameworks */ = {isa = PBXBuildFile; productRef = FD6673F72D7021F200041530 /* SessionUtil */; }; - FD6673FA2D7021F800041530 /* SessionUtil in Frameworks */ = {isa = PBXBuildFile; productRef = FD6673F92D7021F800041530 /* SessionUtil */; }; FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */; }; FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FE2D77F9BE00041530 /* ScreenLock.swift */; }; FD6674002D77F9FD00041530 /* ScreenLockWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockWindow.swift */; }; @@ -2263,8 +2260,6 @@ FDC498B82AC15FE300EDD897 /* AppNotificationAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNotificationAction.swift; sourceTree = ""; }; FDC6D75F2862B3F600B04575 /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = ""; }; FDCC22CF2E52E45A00C77B1A /* GroupAuthData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupAuthData.swift; sourceTree = ""; }; - FDCC22D12E56E0B600C77B1A /* StreamLifecycleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamLifecycleManager.swift; sourceTree = ""; }; - FDCC22D52E5D2AD700C77B1A /* CancellationAwareAsyncStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellationAwareAsyncStream.swift; sourceTree = ""; }; FDCC22D72E5D3C1000C77B1A /* AsyncStream+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncStream+Utilities.swift"; sourceTree = ""; }; FDCC22DA2E5E897200C77B1A /* DeveloperNetworkSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperNetworkSettingsViewModel.swift; sourceTree = ""; }; FDCCC6E82ABA7402002BBEF5 /* EmojiGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiGenerator.swift; sourceTree = ""; }; @@ -2488,7 +2483,6 @@ files = ( C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */, 946F5A732D5DA3AC00A5ADCE /* Punycode in Frameworks */, - FD6673F82D7021F200041530 /* SessionUtil in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2496,7 +2490,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - FD6673F62D7021E700041530 /* SessionUtil in Frameworks */, FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */, FD6A38EF2C2A641200762359 /* DifferenceKit in Frameworks */, FD756BEB2D0181D700BD7199 /* GRDB in Frameworks */, @@ -2509,7 +2502,6 @@ buildActionMask = 2147483647; files = ( FD9BDE012A5D24EA005F1EBC /* SessionUIKit.framework in Frameworks */, - FD6673FA2D7021F800041530 /* SessionUtil in Frameworks */, FD2286732C38D43900BC06F7 /* DifferenceKit in Frameworks */, FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */, C3C2A70B25539E1E00C340D1 /* SessionNetworkingKit.framework in Frameworks */, @@ -3869,7 +3861,6 @@ FDE7214E287E50D50093DF33 /* Scripts */, D221A08C169C9E5E00537ABF /* Frameworks */, D221A08A169C9E5E00537ABF /* Products */, - FD8A5B232DC05A0E004C689B /* Recovered References */, ); sourceTree = ""; }; @@ -5284,7 +5275,6 @@ ); name = SessionNetworkingKit; packageProductDependencies = ( - FD6673F72D7021F200041530 /* SessionUtil */, ); productName = SessionSnodeKit; productReference = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; @@ -5312,7 +5302,6 @@ FD6A38EB2C2A63B500762359 /* KeychainSwift */, FD6A38EE2C2A641200762359 /* DifferenceKit */, FD756BEA2D0181D700BD7199 /* GRDB */, - FD6673F52D7021E700041530 /* SessionUtil */, ); productName = SessionUtilities; productReference = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; @@ -5335,7 +5324,6 @@ packageProductDependencies = ( FD6A39122C2A946A00762359 /* SwiftProtobuf */, FD2286722C38D43900BC06F7 /* DifferenceKit */, - FD6673F92D7021F800041530 /* SessionUtil */, ); productName = SessionMessagingKit; productReference = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; @@ -5972,7 +5960,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(TARGET_BUILD_DIR)/LibSessionUtil_BuildCache/libsession_util_built.timestamp", + "$(BUILT_PRODUCTS_DIR)/libsession-util.a", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -7782,6 +7770,11 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + SessionUtil, + ); PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -7855,6 +7848,11 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + SessionUtil, + ); PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -7907,6 +7905,11 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + SessionUtil, + ); PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilitiesKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -7980,6 +7983,11 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + SessionUtil, + ); PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilitiesKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -8033,6 +8041,11 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + SessionUtil, + ); PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionMessagingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -8106,6 +8119,11 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + SessionUtil, + ); PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionMessagingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -8314,7 +8332,6 @@ "@executable_path/Frameworks", ); LLVM_LTO = NO; - OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; @@ -8361,7 +8378,6 @@ "@executable_path/Frameworks", ); LLVM_LTO = NO; - OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; PROVISIONING_PROFILE = ""; @@ -8749,6 +8765,7 @@ GCC_WARN_UNUSED_VALUE = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LIBRARY_SEARCH_PATHS = "$(TARGET_BUILD_DIR)"; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; MARKETING_VERSION = 2.15.0; @@ -9059,6 +9076,10 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-lsession-util", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionMessagingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -9111,6 +9132,10 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-lsession-util", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -9163,6 +9188,10 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-lsession-util", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilitiesKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -9329,6 +9358,7 @@ GCC_WARN_UNUSED_VALUE = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LIBRARY_SEARCH_PATHS = "$(TARGET_BUILD_DIR)"; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; MARKETING_VERSION = 2.15.0; @@ -9753,6 +9783,10 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-lsession-util", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionMessagingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -9829,6 +9863,10 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-lsession-util", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -9905,6 +9943,10 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-lsession-util", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilitiesKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; From ca4c763042acce8cc1b5bfb82e12114dada3999d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 3 Sep 2025 12:21:36 +1000 Subject: [PATCH 32/59] Fixed a bug where we weren't excluding the prefix when getting a swarm for a pubkey --- Session.xcodeproj/project.pbxproj | 10 ++++++---- .../LibSession/LibSession+Networking.swift | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 68c96a8678..d846424aff 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -8168,7 +8168,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 627; + CURRENT_PROJECT_VERSION = 629; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8249,7 +8249,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 627; + CURRENT_PROJECT_VERSION = 629; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8728,7 +8728,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 627; + CURRENT_PROJECT_VERSION = 629; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8778,6 +8778,7 @@ SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INCLUDE_PATHS = "$(BUILT_PRODUCTS_DIR)/include/**"; SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; @@ -9327,7 +9328,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 627; + CURRENT_PROJECT_VERSION = 629; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -9371,6 +9372,7 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_INCLUDE_PATHS = "$(BUILT_PRODUCTS_DIR)/include/**"; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; diff --git a/SessionNetworkingKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift index 8492656413..4a68ed7d8a 100644 --- a/SessionNetworkingKit/LibSession/LibSession+Networking.swift +++ b/SessionNetworkingKit/LibSession/LibSession+Networking.swift @@ -232,13 +232,15 @@ actor LibSessionNetwork: NetworkType { .eraseToAnyPublisher() case .randomSnode(let swarmPublicKey): + let swarmSessionId: SessionId = try SessionId(from: swarmPublicKey) + guard (try? SessionId(from: swarmPublicKey)) != nil else { throw SessionIdError.invalidSessionId } guard body != nil else { throw NetworkError.invalidPreparedRequest } - guard let cSwarmPublicKey: [CChar] = swarmPublicKey.cString(using: .utf8) else { + guard let cSwarmPublicKey: [CChar] = swarmSessionId.publicKeyString.cString(using: .utf8) else { throw LibSessionError.invalidCConversion } From 5bed51e45d2e02d83aa330f51f1562da8661c28a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Sep 2025 08:39:38 +1000 Subject: [PATCH 33/59] Fixed an issue where combine streams which sent requests would never complete --- Session.xcodeproj/project.pbxproj | 8 ++--- .../DeveloperNetworkSettingsViewModel.swift | 17 ++++++--- .../Database/Models/Interaction.swift | 35 ++++++------------- .../Jobs/CheckForAppUpdatesJob.swift | 2 +- .../Errors/MessageReceiverError.swift | 4 ++- .../MessageReceiver+VisibleMessages.swift | 2 +- .../Sending & Receiving/MessageReceiver.swift | 2 +- .../LibSession/LibSession+Networking.swift | 27 ++++++++------ SessionNetworkingKit/Types/Network.swift | 1 + .../Types/RequestCategory.swift | 2 +- .../Combine/Publisher+Utilities.swift | 29 ++++++++++++--- SessionUtilitiesKit/Database/Models/Job.swift | 35 ------------------- .../Database/Models/JobDependencies.swift | 14 -------- .../Dependency Injection/Dependencies.swift | 6 ++-- SessionUtilitiesKit/JobRunner/JobRunner.swift | 19 +++++++--- 15 files changed, 92 insertions(+), 111 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index d846424aff..e22b70e63e 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -8168,7 +8168,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 629; + CURRENT_PROJECT_VERSION = 630; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8249,7 +8249,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 629; + CURRENT_PROJECT_VERSION = 630; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8728,7 +8728,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 629; + CURRENT_PROJECT_VERSION = 630; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -9328,7 +9328,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 629; + CURRENT_PROJECT_VERSION = 630; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; diff --git a/Session/Settings/DeveloperSettings/DeveloperNetworkSettingsViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperNetworkSettingsViewModel.swift index 116d699173..4576a9ee4a 100644 --- a/Session/Settings/DeveloperSettings/DeveloperNetworkSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettings/DeveloperNetworkSettingsViewModel.swift @@ -711,9 +711,12 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState /// Changing the network settings can result in data being cleared from the database so we should confirm that is desired before /// we make the changes - guard hasConfirmed && (networkEnvironmentChanged || pushServiceChanged) else { + guard hasConfirmed else { switch (networkEnvironmentChanged, pushServiceChanged) { - case (false, false): break /// Most likely just the `forceOffline` (or some new) change + case (false, false): + /// Most likely just the `forceOffline` (or some new) change + await self.saveChanges(hasConfirmed: true) + case (false, true): self.transitionToScreen( ConfirmationModal( @@ -810,9 +813,13 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState /// If the `forceOffline` value changed then apply the change if internalState.initialState.forceOffline != internalState.pendingState.forceOffline { dependencies.set(feature: .forceOffline, to: internalState.pendingState.forceOffline) - await dependencies[singleton: .network].setNetworkStatus( - status: internalState.pendingState.forceOffline ? .unknown : .disconnected - ) + + if !internalState.pendingState.forceOffline { + await dependencies[singleton: .network].resetNetworkStatus() + } + else { + await dependencies[singleton: .network].setNetworkStatus(status: .disconnected) + } } /// If the network environment changed then we should make those changes first (since they result in the database being cleared) diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index c8f4d53c99..1aad3bde79 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -1458,19 +1458,17 @@ public extension Interaction { db.addAttachmentEvent(id: info.attachmentId, messageId: info.interactionId, type: .deleted) } - /// Add the garbage collection job to delete orphaned attachment files + /// If we had attachments then we want to try to delete their associated files immediately (in the next run loop) as that's the + /// behaviour users would expect, if this fails for some reason then they will be cleaned up by the `GarbageCollectionJob` + /// but we should still try to handle it immediately if !attachments.isEmpty { - dependencies[singleton: .jobRunner].add( - db, - job: Job( - variant: .garbageCollection, - behaviour: .runOnce, - details: GarbageCollectionJob.Details( - typesToCollect: [.orphanedAttachmentFiles] - ) - ), - canStartJob: true - ) + let attachmentPaths: [String] = attachments.compactMap { + try? dependencies[singleton: .attachmentManager].path(for: $0.downloadUrl) + } + + DispatchQueue.global(qos: .background).async { + attachmentPaths.forEach { try? dependencies[singleton: .fileManager].removeItem(atPath: $0) } + } } /// Delete the reactions from the database @@ -1544,19 +1542,6 @@ public extension Interaction { interactionIds.forEach { id in db.addMessageEvent(id: id, threadId: threadId, type: .deleted) } - - /// If we had attachments then we want to try to delete their associated files immediately (in the next run loop) as that's the - /// behaviour users would expect, if this fails for some reason then they will be cleaned up by the `GarbageCollectionJob` - /// but we should still try to handle it immediately - if !attachments.isEmpty { - let attachmentPaths: [String] = attachments.compactMap { - try? dependencies[singleton: .attachmentManager].path(for: $0.downloadUrl) - } - - DispatchQueue.global(qos: .background).async { - attachmentPaths.forEach { try? dependencies[singleton: .fileManager].removeItem(atPath: $0) } - } - } } /// Whenever a message gets deleted we need to send an event to ensure the home screen updates correctly, this function manages diff --git a/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift b/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift index 14f83887c7..ecf2b3d93c 100644 --- a/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift +++ b/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift @@ -66,7 +66,7 @@ public enum CheckForAppUpdatesJob: JobExecutor { ) try? await dependencies[singleton: .storage].writeAsync { db in - try updatedJob.save(db) + try updatedJob.upsert(db) } success(updatedJob, false) diff --git a/SessionMessagingKit/Sending & Receiving/Errors/MessageReceiverError.swift b/SessionMessagingKit/Sending & Receiving/Errors/MessageReceiverError.swift index af8255fbce..2b7c85d0c9 100644 --- a/SessionMessagingKit/Sending & Receiving/Errors/MessageReceiverError.swift +++ b/SessionMessagingKit/Sending & Receiving/Errors/MessageReceiverError.swift @@ -26,13 +26,14 @@ public enum MessageReceiverError: Error, CustomStringConvertible { case duplicatedCall case missingRequiredAdminPrivileges case deprecatedMessage + case originalMessageNotFound public var isRetryable: Bool { switch self { case .duplicateMessage, .invalidMessage, .unknownMessage, .unknownEnvelopeType, .invalidSignature, .noData, .senderBlocked, .noThread, .selfSend, .decryptionFailed, .invalidConfigMessageHandling, .outdatedMessage, .ignorableMessage, .ignorableMessageRequestMessage, - .missingRequiredAdminPrivileges: + .missingRequiredAdminPrivileges, .originalMessageNotFound: return false default: return true @@ -112,6 +113,7 @@ public enum MessageReceiverError: Error, CustomStringConvertible { case .duplicatedCall: return "Duplicate call." case .missingRequiredAdminPrivileges: return "Handling this message requires admin privileges which the current user does not have." case .deprecatedMessage: return "This message type has been deprecated." + case .originalMessageNotFound: return "Original message not found." } } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index a5ddfb44bf..977c579f07 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -533,7 +533,7 @@ extension MessageReceiver { .fetchOne(db) guard let interactionId: Int64 = maybeInteractionId else { - throw StorageError.objectNotFound + throw MessageReceiverError.originalMessageNotFound } let sortId = Reaction.getSortId( diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 0ec0006b12..9b92370ff7 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -477,7 +477,7 @@ public enum MessageReceiver { .filter(Interaction.Columns.openGroupServerMessageId == openGroupMessageServerId) .asRequest(of: Info.self) .fetchOne(db) - else { throw MessageReceiverError.invalidMessage } + else { throw MessageReceiverError.originalMessageNotFound } // If the user locally deleted the message then we don't want to process reactions for it guard !interactionInfo.variant.isDeletedMessage else { return } diff --git a/SessionNetworkingKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift index 4a68ed7d8a..2c04405a38 100644 --- a/SessionNetworkingKit/LibSession/LibSession+Networking.swift +++ b/SessionNetworkingKit/LibSession/LibSession+Networking.swift @@ -224,6 +224,7 @@ actor LibSessionNetwork: NetworkType { return networkInstance .compactMap { $0 } + .first() .tryFlatMap { [dependencies] network -> AnyPublisher in switch destination { case .snode, .server, .serverUpload, .serverDownload, .cached: @@ -232,14 +233,10 @@ actor LibSessionNetwork: NetworkType { .eraseToAnyPublisher() case .randomSnode(let swarmPublicKey): + guard body != nil else { throw NetworkError.invalidPreparedRequest } + let swarmSessionId: SessionId = try SessionId(from: swarmPublicKey) - guard (try? SessionId(from: swarmPublicKey)) != nil else { - throw SessionIdError.invalidSessionId - } - guard body != nil else { - throw NetworkError.invalidPreparedRequest - } guard let cSwarmPublicKey: [CChar] = swarmSessionId.publicKeyString.cString(using: .utf8) else { throw LibSessionError.invalidCConversion } @@ -355,9 +352,6 @@ actor LibSessionNetwork: NetworkType { ) case .randomSnode(let swarmPublicKey): - guard (try? SessionId(from: swarmPublicKey)) != nil else { - throw SessionIdError.invalidSessionId - } guard body != nil else { throw NetworkError.invalidPreparedRequest } let swarm: Set = try await getSwarm(for: swarmPublicKey) @@ -404,6 +398,15 @@ actor LibSessionNetwork: NetworkType { ) } + public func resetNetworkStatus() async { + guard !isSuspended, let network = try? await getOrCreateNetwork() else { return } + + let status: NetworkStatus = NetworkStatus(status: session_network_get_status(network)) + + Log.info(.network, "Network status changed to: \(status)") + await internalNetworkStatus.send(status) + } + public func setNetworkStatus(status: NetworkStatus) async { guard status == .disconnected || !isSuspended else { Log.warn(.network, "Attempted to update network status to '\(status)' for suspended network, closing connections again.") @@ -414,7 +417,10 @@ actor LibSessionNetwork: NetworkType { } } - // Notify any subscribers + /// If we have set the `forceOffline` flag then don't allow non-disconnected status updates + guard status == .disconnected || !dependencies[feature: .forceOffline] else { return } + + /// Notify any subscribers Log.info(.network, "Network status changed to: \(status)") await internalNetworkStatus.send(status) } @@ -1127,6 +1133,7 @@ public extension LibSession { ) } + public func resetNetworkStatus() async {} public func setNetworkStatus(status: NetworkStatus) async {} public func suspendNetworkAccess() async {} public func resumeNetworkAccess(autoReconnect: Bool) async {} diff --git a/SessionNetworkingKit/Types/Network.swift b/SessionNetworkingKit/Types/Network.swift index ee2b3358ef..d4bf6c654f 100644 --- a/SessionNetworkingKit/Types/Network.swift +++ b/SessionNetworkingKit/Types/Network.swift @@ -48,6 +48,7 @@ public protocol NetworkType { func checkClientVersion(ed25519SecretKey: [UInt8]) async throws -> (info: ResponseInfoType, value: AppVersionResponse) + func resetNetworkStatus() async func setNetworkStatus(status: NetworkStatus) async func suspendNetworkAccess() async func resumeNetworkAccess(autoReconnect: Bool) async diff --git a/SessionNetworkingKit/Types/RequestCategory.swift b/SessionNetworkingKit/Types/RequestCategory.swift index c3bf81830c..a3e80f7b11 100644 --- a/SessionNetworkingKit/Types/RequestCategory.swift +++ b/SessionNetworkingKit/Types/RequestCategory.swift @@ -4,7 +4,7 @@ import Foundation import SessionUtil public extension Network { - enum RequestCategory { + enum RequestCategory: Codable { case standard case upload case download diff --git a/SessionUtilitiesKit/Combine/Publisher+Utilities.swift b/SessionUtilitiesKit/Combine/Publisher+Utilities.swift index e988982c7e..093f91e2ea 100644 --- a/SessionUtilitiesKit/Combine/Publisher+Utilities.swift +++ b/SessionUtilitiesKit/Combine/Publisher+Utilities.swift @@ -161,6 +161,25 @@ public extension Publisher { // MARK: - Convenience +private final class SubscriptionManager { + static let shared: SubscriptionManager = SubscriptionManager() + + private let lock: NSLock = NSLock() + private var subscriptions: [UUID: AnyCancellable] = [:] + + func store(_ subscription: AnyCancellable, for id: UUID) { + lock.lock() + defer { lock.unlock() } + subscriptions[id] = subscription + } + + func release(for id: UUID) { + lock.lock() + defer { lock.unlock() } + subscriptions[id] = nil + } +} + public extension Publisher { func sink(into subject: PassthroughSubject?, includeCompletions: Bool = false) -> AnyCancellable { guard let targetSubject: PassthroughSubject = subject else { return AnyCancellable {} } @@ -173,18 +192,18 @@ public extension Publisher { receiveCompletion: ((Subscribers.Completion) -> Void)? = nil, receiveValue: ((Output) -> Void)? = nil ) { - var retainCycle: Cancellable? = nil - retainCycle = self + let id: UUID = UUID() + let cancellable: AnyCancellable = self .sink( receiveCompletion: { result in receiveCompletion?(result) - // Redundant but without reading 'retainCycle' it will warn that the variable - // isn't used - if retainCycle != nil { retainCycle = nil } + SubscriptionManager.shared.release(for: id) }, receiveValue: (receiveValue ?? { _ in }) ) + + SubscriptionManager.shared.store(cancellable, for: id) } } diff --git a/SessionUtilitiesKit/Database/Models/Job.swift b/SessionUtilitiesKit/Database/Models/Job.swift index 1d6a0df046..0e667ab65f 100644 --- a/SessionUtilitiesKit/Database/Models/Job.swift +++ b/SessionUtilitiesKit/Database/Models/Job.swift @@ -11,25 +11,6 @@ public protocol UniqueHashable { public struct Job: Codable, Equatable, Hashable, Identifiable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "job" } - internal static let dependencyForeignKey = ForeignKey([Columns.id], to: [JobDependencies.Columns.dependantId]) - public static let dependantJobDependency = hasMany( - JobDependencies.self, - using: JobDependencies.jobForeignKey - ) - public static let dependancyJobDependency = hasMany( - JobDependencies.self, - using: JobDependencies.dependantForeignKey - ) - internal static let jobsThisJobDependsOn = hasMany( - Job.self, - through: dependantJobDependency, - using: JobDependencies.dependant - ) - internal static let jobsThatDependOnThisJob = hasMany( - Job.self, - through: dependancyJobDependency, - using: JobDependencies.job - ) public typealias Columns = CodingKeys public enum CodingKeys: String, CodingKey, ColumnExpression { @@ -244,22 +225,6 @@ public struct Job: Codable, Equatable, Hashable, Identifiable, FetchableRecord, /// a job directly which may need some special behaviour) public let transientData: Any? - /// The other jobs which this job is dependant on - /// - /// **Note:** When completing a job the dependencies **MUST** be cleared before the job is - /// deleted or it will automatically delete any dependant jobs - public var dependencies: QueryInterfaceRequest { - request(for: Job.jobsThisJobDependsOn) - } - - /// The other jobs which depend on this job - /// - /// **Note:** When completing a job the dependencies **MUST** be cleared before the job is - /// deleted or it will automatically delete any dependant jobs - public var dependantJobs: QueryInterfaceRequest { - request(for: Job.jobsThatDependOnThisJob) - } - // MARK: - Initialization fileprivate init( diff --git a/SessionUtilitiesKit/Database/Models/JobDependencies.swift b/SessionUtilitiesKit/Database/Models/JobDependencies.swift index ab929636f4..7dcf662a31 100644 --- a/SessionUtilitiesKit/Database/Models/JobDependencies.swift +++ b/SessionUtilitiesKit/Database/Models/JobDependencies.swift @@ -5,10 +5,6 @@ import GRDB public struct JobDependencies: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "jobDependencies" } - internal static let jobForeignKey = ForeignKey([Columns.jobId], to: [Job.Columns.id]) - internal static let dependantForeignKey = ForeignKey([Columns.dependantId], to: [Job.Columns.id]) - public static let job = belongsTo(Job.self, using: jobForeignKey) - public static let dependant = hasOne(Job.self, using: Job.dependencyForeignKey) public typealias Columns = CodingKeys public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { @@ -36,14 +32,4 @@ public struct JobDependencies: Codable, Equatable, Hashable, FetchableRecord, Pe self.jobId = jobId self.dependantId = dependantId } - - // MARK: - Relationships - - public var job: QueryInterfaceRequest { - request(for: JobDependencies.job) - } - - public var dependant: QueryInterfaceRequest { - request(for: JobDependencies.dependant) - } } diff --git a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift index c7e9ad88a8..06cb74537d 100644 --- a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift +++ b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift @@ -13,8 +13,8 @@ public class Dependencies { @ThreadSafeObject private static var cachedIsRTLRetriever: (requiresMainThread: Bool, retriever: () -> Bool) = (false, { false }) @ThreadSafeObject private var storage: DependencyStorage = DependencyStorage() - private typealias DependencyChange = (Dependencies.DependencyStorage.Key, DependencyStorage.Value?) - private let dependencyChangeStream: CancellationAwareAsyncStream = CancellationAwareAsyncStream() + fileprivate typealias DependencyChange = (Dependencies.DependencyStorage.Key, DependencyStorage.Value?) + fileprivate let dependencyChangeStream: CancellationAwareAsyncStream = CancellationAwareAsyncStream() // MARK: - Subscript Access @@ -471,7 +471,7 @@ public extension Dependencies { let observationTask = Task { [weak self] in guard let self else { return continuation.finish() } - for await (changedKey, changedValue) in self.dependecyChangeStream { + for await (changedKey, changedValue) in self.dependencyChangeStream.stream { guard changedKey == key else { continue } if let newInstance = changedValue?.value(as: T.self) { diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index a8c5afa608..1787682030 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -1705,7 +1705,12 @@ public final class JobQueue: Hashable { updates: { [dependencies] db -> [Job] in /// Retrieve the dependant jobs first (the `JobDependecies` table has cascading deletion when the original `Job` is /// removed so we need to retrieve these records before that happens) - let dependantJobs: [Job] = try job.dependantJobs.fetchAll(db) + let dependantJobIds: Set = try JobDependencies + .select(.jobId) + .filter(JobDependencies.Columns.dependantId == job.id) + .asRequest(of: Int64.self) + .fetchSet(db) + let dependantJobs: [Job] = try Job.fetchAll(db, ids: dependantJobIds) switch job.behaviour { case .runOnce, .runOnceNextLaunch, .runOnceAfterConfigSyncIgnoringPermanentFailure: @@ -1824,15 +1829,19 @@ public final class JobQueue: Hashable { // Get the max failure count for the job (a value of '-1' means it will retry indefinitely) let maxFailureCount: Int = (executorMap[job.variant]?.maxFailureCount ?? 0) let nextRunTimestamp: TimeInterval = (dependencies.dateNow.timeIntervalSince1970 + JobRunner.getRetryInterval(for: job)) - var dependantJobIds: [Int64] = [] + var dependantJobIds: Set = [] var failureText: String = "failed due to error: \(error)" dependencies[singleton: .storage].write { db in /// Retrieve a list of dependant jobs so we can clear them from the queue - dependantJobIds = try job.dependantJobs - .select(.id) + + /// Retrieve the dependant jobs first (the `JobDependecies` table has cascading deletion when the original `Job` is + /// removed so we need to retrieve these records before that happens) + dependantJobIds = try JobDependencies + .select(.jobId) + .filter(JobDependencies.Columns.dependantId == job.id) .asRequest(of: Int64.self) - .fetchAll(db) + .fetchSet(db) /// Delete/update the failed jobs and any dependencies let updatedFailureCount: UInt = (job.failureCount + 1) From 8d41ab44f900021050b19070231b9a1a98292939 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Sep 2025 11:03:14 +1000 Subject: [PATCH 34/59] Fixed a couple of issues found when testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fixed a couple of build issues with the Job changes • Fixed a crash which could happen on the loading screen when it fails to get the profile --- Session/Onboarding/LoadingScreen.swift | 22 +++++++++++-------- .../Jobs/ConfigMessageReceiveJob.swift | 17 ++++++++++---- .../MessageViewModel+DeletionActions.swift | 11 +++++----- SessionUtilitiesKit/Database/Models/Job.swift | 4 +++- SessionUtilitiesKit/JobRunner/JobRunner.swift | 7 +++--- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/Session/Onboarding/LoadingScreen.swift b/Session/Onboarding/LoadingScreen.swift index d6d3d3838f..3295344c0b 100644 --- a/Session/Onboarding/LoadingScreen.swift +++ b/Session/Onboarding/LoadingScreen.swift @@ -142,15 +142,19 @@ struct LoadingScreen: View { animationTimer = nil guard success else { - let viewController: SessionHostingViewController = SessionHostingViewController( - rootView: DisplayNameScreen(flow: viewModel.initialFlow, using: viewModel.dependencies) - ) - viewController.setUpNavBarSessionIcon() - if let navigationController = self.host.controller?.navigationController { - let updatedViewControllers: [UIViewController] = navigationController.viewControllers - .filter { !$0.isKind(of: SessionHostingViewController.self) } - .appending(viewController) - navigationController.setViewControllers(updatedViewControllers, animated: true) + Task(priority: .userInitiated) { + await MainActor.run { + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: DisplayNameScreen(flow: viewModel.initialFlow, using: viewModel.dependencies) + ) + viewController.setUpNavBarSessionIcon() + if let navigationController = self.host.controller?.navigationController { + let updatedViewControllers: [UIViewController] = navigationController.viewControllers + .filter { !$0.isKind(of: SessionHostingViewController.self) } + .appending(viewController) + navigationController.setViewControllers(updatedViewControllers, animated: true) + } + } } return } diff --git a/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift b/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift index f0ba83de12..73abfbbd1d 100644 --- a/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift +++ b/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift @@ -36,12 +36,21 @@ public enum ConfigMessageReceiveJob: JobExecutor { guard let jobId: Int64 = job.id else { return } dependencies[singleton: .storage].write { db in + let dependantJobIds: Set = try JobDependencies + .select(.jobId) + .filter(JobDependencies.Columns.dependantId == jobId) + .asRequest(of: Int64.self) + .fetchSet(db) + let targetJobIds: Set = try Job + .select(.id) + .filter(dependantJobIds.contains(Job.Columns.id)) + .filter(Job.Columns.variant == Job.Variant.messageReceive) + .asRequest(of: Int64.self) + .fetchSet(db) + try JobDependencies + .filter(targetJobIds.contains(JobDependencies.Columns.jobId)) .filter(JobDependencies.Columns.dependantId == jobId) - .joining( - required: JobDependencies.job - .filter(Job.Columns.variant == Job.Variant.messageReceive) - ) .deleteAll(db) } } diff --git a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift index d315c753c9..4530788793 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift @@ -124,7 +124,7 @@ public extension MessageViewModel.DeletionBehaviours { enum SelectedMessageState { case outgoingOnly case containsIncoming - case containsDeletedOrControlMessages + case containsLocalOnlyMessages /// Control, pending or deleted messages } /// If it's a legacy group and they have been deprecated then the user shouldn't be able to delete messages @@ -134,8 +134,9 @@ public extension MessageViewModel.DeletionBehaviours { let state: SelectedMessageState = { guard !cellViewModels.contains(where: { $0.variant.isDeletedMessage }) && - !cellViewModels.contains(where: { $0.variant.isInfoMessage }) - else { return .containsDeletedOrControlMessages } + !cellViewModels.contains(where: { $0.variant.isInfoMessage }) && + !cellViewModels.contains(where: { $0.state == .sending || $0.state == .failed }) + else { return .containsLocalOnlyMessages } return (cellViewModels.contains(where: { $0.variant == .standardIncoming }) ? .containsIncoming : @@ -171,8 +172,8 @@ public extension MessageViewModel.DeletionBehaviours { }() switch (state, isAdmin) { - /// User selects messages including a control message or “deleted” message - case (.containsDeletedOrControlMessages, _): + /// User selects messages including a control, pending or “deleted” message + case (.containsLocalOnlyMessages, _): return MessageViewModel.DeletionBehaviours( title: "deleteMessage" .putNumber(cellViewModels.count) diff --git a/SessionUtilitiesKit/Database/Models/Job.swift b/SessionUtilitiesKit/Database/Models/Job.swift index 0e667ab65f..ac92a12f58 100644 --- a/SessionUtilitiesKit/Database/Models/Job.swift +++ b/SessionUtilitiesKit/Database/Models/Job.swift @@ -574,7 +574,9 @@ extension Job { } if !includeJobsWithDependencies { - query = query.having(Job.jobsThisJobDependsOn.isEmpty) + let dependencySubquery: QueryInterfaceRequest = JobDependencies + .filter(JobDependencies.Columns.jobId == Job.Columns.id) + query = query.filter(!dependencySubquery.exists()) } return query diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index 1787682030..7d3c3574c0 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -1860,9 +1860,7 @@ public final class JobQueue: Hashable { // If the job permanently failed or we have performed all of our retry attempts // then delete the job and all of it's dependant jobs (it'll probably never succeed) - _ = try job.dependantJobs - .deleteAll(db) - + _ = try Job.deleteAll(db, ids: dependantJobIds) _ = try job.delete(db) return } @@ -1879,7 +1877,8 @@ public final class JobQueue: Hashable { // Update the failureCount and nextRunTimestamp on dependant jobs as well (update the // 'nextRunTimestamp' value to be 1ms later so when the queue gets regenerated they'll // come after the dependency) - try job.dependantJobs + try Job + .filter(ids: dependantJobIds) .updateAll( db, Job.Columns.failureCount.set(to: updatedFailureCount), From a728a95d5cfbb1443da1f0608aef8f0edcfec810 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 4 Sep 2025 11:24:36 +1000 Subject: [PATCH 35/59] Fixed an issue where group creation was broken --- Session.xcodeproj/project.pbxproj | 8 +- .../Jobs/ConfigurationSyncJob.swift | 21 +- .../Jobs/GroupLeavingJob.swift | 10 +- .../MessageSender+Groups.swift | 199 +++++++++--------- .../Authentication+SessionMessagingKit.swift | 4 +- 5 files changed, 133 insertions(+), 109 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index e22b70e63e..4f69a7b2ab 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -8168,7 +8168,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 630; + CURRENT_PROJECT_VERSION = 631; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8249,7 +8249,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 630; + CURRENT_PROJECT_VERSION = 631; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8728,7 +8728,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 630; + CURRENT_PROJECT_VERSION = 631; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -9328,7 +9328,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 630; + CURRENT_PROJECT_VERSION = 631; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; diff --git a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift index 7c9e26451b..d11f9ca7e4 100644 --- a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift +++ b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift @@ -93,9 +93,12 @@ public enum ConfigurationSyncJob: JobExecutor { AnyPublisher .lazy { () -> Network.PreparedRequest in - let authMethod: AuthenticationMethod = try Authentication.with( - swarmPublicKey: swarmPublicKey, - using: dependencies + let authMethod: AuthenticationMethod = try ( + additionalTransientData?.customAuthMethod ?? + Authentication.with( + swarmPublicKey: swarmPublicKey, + using: dependencies + ) ) return try SnodeAPI.preparedSequence( @@ -331,24 +334,28 @@ extension ConfigurationSyncJob { public let afterSequenceRequests: [any ErasedPreparedRequest] public let requireAllBatchResponses: Bool public let requireAllRequestsSucceed: Bool + public let customAuthMethod: AuthenticationMethod? init?( beforeSequenceRequests: [any ErasedPreparedRequest], afterSequenceRequests: [any ErasedPreparedRequest], requireAllBatchResponses: Bool, - requireAllRequestsSucceed: Bool + requireAllRequestsSucceed: Bool, + customAuthMethod: AuthenticationMethod? ) { guard !beforeSequenceRequests.isEmpty || !afterSequenceRequests.isEmpty || requireAllBatchResponses || - requireAllRequestsSucceed + requireAllRequestsSucceed || + customAuthMethod != nil else { return nil } self.beforeSequenceRequests = beforeSequenceRequests self.afterSequenceRequests = afterSequenceRequests self.requireAllBatchResponses = requireAllBatchResponses self.requireAllRequestsSucceed = requireAllRequestsSucceed + self.customAuthMethod = customAuthMethod } } } @@ -408,6 +415,7 @@ public extension ConfigurationSyncJob { afterSequenceRequests: [any ErasedPreparedRequest] = [], requireAllBatchResponses: Bool = false, requireAllRequestsSucceed: Bool = false, + customAuthMethod: AuthenticationMethod? = nil, using dependencies: Dependencies ) -> AnyPublisher { return Deferred { @@ -422,7 +430,8 @@ public extension ConfigurationSyncJob { beforeSequenceRequests: beforeSequenceRequests, afterSequenceRequests: afterSequenceRequests, requireAllBatchResponses: requireAllBatchResponses, - requireAllRequestsSucceed: requireAllRequestsSucceed + requireAllRequestsSucceed: requireAllRequestsSucceed, + customAuthMethod: customAuthMethod ) ) else { return resolver(Result.failure(NetworkError.parsingFailed)) } diff --git a/SessionMessagingKit/Jobs/GroupLeavingJob.swift b/SessionMessagingKit/Jobs/GroupLeavingJob.swift index 39fcd136d4..c951fdd419 100644 --- a/SessionMessagingKit/Jobs/GroupLeavingJob.swift +++ b/SessionMessagingKit/Jobs/GroupLeavingJob.swift @@ -66,10 +66,18 @@ public enum GroupLeavingJob: JobExecutor { return details.behaviour }() + /// There is a rare edge-case where a group could be created locally but the auth data wasn't saved to `libSession` + /// which results in the authentication data being invalid, as a result we want to try to retrieve the auth data regardless + /// of whether we are going to use it as this will throw an `invalidAuthentication` error that would allow deletion + /// even in this invalid state + let authMethod: AuthenticationMethod = try Authentication.with( + swarmPublicKey: threadId, + using: dependencies + ) + switch (finalBehaviour, isAdminUser, (isAdminUser && numAdminUsers == 1)) { case (.leave, _, false): let disappearingConfig: DisappearingMessagesConfiguration? = try? DisappearingMessagesConfiguration.fetchOne(db, id: threadId) - let authMethod: AuthenticationMethod = try Authentication.with(swarmPublicKey: threadId, using: dependencies) return .sendLeaveMessage(authMethod, disappearingConfig) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift index 492edbb550..cc577e0367 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift @@ -7,13 +7,14 @@ import SessionUtilitiesKit import SessionNetworkingKit extension MessageSender { - private typealias PreparedGroupData = ( - groupSessionId: SessionId, - groupState: [ConfigDump.Variant: LibSession.Config], - thread: SessionThread, - group: ClosedGroup, - members: [GroupMember] - ) + private struct PreparedGroupData { + let groupSessionId: SessionId + let identityKeyPair: KeyPair + let groupState: [ConfigDump.Variant: LibSession.Config] + let thread: SessionThread + let group: ClosedGroup + let members: [GroupMember] + } public static func createGroup( name: String, @@ -42,95 +43,98 @@ extension MessageSender { .map { Optional($0) } .eraseToAnyPublisher() } - .flatMap { (displayPictureInfo: DisplayPictureManager.UploadResult?) -> AnyPublisher in - dependencies[singleton: .storage].writePublisher { db -> PreparedGroupData in - /// Create and cache the libSession entries - let createdInfo: LibSession.CreatedGroupInfo = try LibSession.createGroup( - db, - name: name, - description: description, - displayPictureUrl: displayPictureInfo?.downloadUrl, - displayPictureEncryptionKey: displayPictureInfo?.encryptionKey, - members: members, - using: dependencies - ) - - /// Save the relevant objects to the database - let thread: SessionThread = try SessionThread.upsert( - db, - id: createdInfo.group.id, - variant: .group, - values: SessionThread.TargetValues( - creationDateTimestamp: .setTo(createdInfo.group.formationTimestamp), - shouldBeVisible: .setTo(true) - ), - using: dependencies - ) - try createdInfo.group.insert(db) - try createdInfo.members.forEach { try $0.insert(db) } - - /// Add a record of the initial invites going out (default to being read as we don't want the creator of the group - /// to see the "Unread Messages" banner above this control message) - _ = try? Interaction( + .flatMapStorageWritePublisher(using: dependencies) { (db: ObservingDatabase, displayPictureInfo: DisplayPictureManager.UploadResult?) -> PreparedGroupData in + /// Create and cache the libSession entries + let createdInfo: LibSession.CreatedGroupInfo = try LibSession.createGroup( + db, + name: name, + description: description, + displayPictureUrl: displayPictureInfo?.downloadUrl, + displayPictureEncryptionKey: displayPictureInfo?.encryptionKey, + members: members, + using: dependencies + ) + + /// Save the relevant objects to the database + let thread: SessionThread = try SessionThread.upsert( + db, + id: createdInfo.group.id, + variant: .group, + values: SessionThread.TargetValues( + creationDateTimestamp: .setTo(createdInfo.group.formationTimestamp), + shouldBeVisible: .setTo(true) + ), + using: dependencies + ) + try createdInfo.group.insert(db) + try createdInfo.members.forEach { try $0.insert(db) } + + /// Add a record of the initial invites going out (default to being read as we don't want the creator of the group + /// to see the "Unread Messages" banner above this control message) + _ = try? Interaction( + threadId: createdInfo.group.id, + threadVariant: .group, + authorId: userSessionId.hexString, + variant: .infoGroupMembersUpdated, + body: ClosedGroup.MessageInfo + .addedUsers( + hasCurrentUser: false, + names: sortedOtherMembers.map { id, profile in + profile?.displayName(for: .group) ?? + id.truncated() + }, + historyShared: false + ) + .infoString(using: dependencies), + timestampMs: Int64(createdInfo.group.formationTimestamp * 1000), + wasRead: true, + using: dependencies + ).inserted(db) + + /// Schedule the "members added" control message to be sent after the config sync completes + try dependencies[singleton: .jobRunner].add( + db, + job: Job( + variant: .messageSend, + behaviour: .runOnceAfterConfigSyncIgnoringPermanentFailure, threadId: createdInfo.group.id, - threadVariant: .group, - authorId: userSessionId.hexString, - variant: .infoGroupMembersUpdated, - body: ClosedGroup.MessageInfo - .addedUsers( - hasCurrentUser: false, - names: sortedOtherMembers.map { id, profile in - profile?.displayName(for: .group) ?? - id.truncated() - }, - historyShared: false - ) - .infoString(using: dependencies), - timestampMs: Int64(createdInfo.group.formationTimestamp * 1000), - wasRead: true, - using: dependencies - ).inserted(db) - - /// Schedule the "members added" control message to be sent after the config sync completes - try dependencies[singleton: .jobRunner].add( - db, - job: Job( - variant: .messageSend, - behaviour: .runOnceAfterConfigSyncIgnoringPermanentFailure, - threadId: createdInfo.group.id, - details: MessageSendJob.Details( - destination: .closedGroup(groupPublicKey: createdInfo.group.id), - message: GroupUpdateMemberChangeMessage( - changeType: .added, - memberSessionIds: sortedOtherMembers.map { id, _ in id }, - historyShared: false, - sentTimestampMs: UInt64(createdInfo.group.formationTimestamp * 1000), - authMethod: Authentication.groupAdmin( - groupSessionId: createdInfo.groupSessionId, - ed25519SecretKey: createdInfo.identityKeyPair.secretKey - ), - using: dependencies + details: MessageSendJob.Details( + destination: .closedGroup(groupPublicKey: createdInfo.group.id), + message: GroupUpdateMemberChangeMessage( + changeType: .added, + memberSessionIds: sortedOtherMembers.map { id, _ in id }, + historyShared: false, + sentTimestampMs: UInt64(createdInfo.group.formationTimestamp * 1000), + authMethod: Authentication.groupAdmin( + groupSessionId: createdInfo.groupSessionId, + ed25519SecretKey: createdInfo.identityKeyPair.secretKey ), - requiredConfigSyncVariant: .groupMembers - ) - ), - canStartJob: false - ) - - return ( - createdInfo.groupSessionId, - createdInfo.groupState, - thread, - createdInfo.group, - createdInfo.members - ) - } + using: dependencies + ), + requiredConfigSyncVariant: .groupMembers + ) + ), + canStartJob: false + ) + + return PreparedGroupData( + groupSessionId: createdInfo.groupSessionId, + identityKeyPair: createdInfo.identityKeyPair, + groupState: createdInfo.groupState, + thread: thread, + group: createdInfo.group, + members: createdInfo.members + ) } .flatMap { preparedGroupData -> AnyPublisher in ConfigurationSyncJob .run( swarmPublicKey: preparedGroupData.groupSessionId.hexString, requireAllRequestsSucceed: true, + customAuthMethod: Authentication.groupAdmin( + groupSessionId: preparedGroupData.groupSessionId, + ed25519SecretKey: preparedGroupData.identityKeyPair.secretKey + ), using: dependencies ) .flatMap { _ in @@ -172,12 +176,12 @@ extension MessageSender { .eraseToAnyPublisher() } .handleEvents( - receiveOutput: { groupSessionId, _, thread, group, groupMembers in + receiveOutput: { preparedGroupData in let userSessionId: SessionId = dependencies[cache: .general].sessionId // Start polling Task.detached(priority: .userInitiated) { [manager = dependencies[singleton: .groupPollerManager]] in - await manager.getOrCreatePoller(for: thread.id).startIfNeeded() + await manager.getOrCreatePoller(for: preparedGroupData.thread.id).startIfNeeded() } // Subscribe for push notifications (if PNs are enabled) @@ -187,7 +191,7 @@ extension MessageSender { token: Data(hex: token), swarmAuthentication: [ try? Authentication.with( - swarmPublicKey: groupSessionId.hexString, + swarmPublicKey: preparedGroupData.groupSessionId.hexString, using: dependencies ) ].compactMap { $0 }, @@ -198,7 +202,7 @@ extension MessageSender { dependencies[singleton: .storage].writeAsync { db in // Save jobs for sending group member invitations - groupMembers + preparedGroupData.members .filter { $0.profileId != userSessionId.hexString } .compactMap { member -> (GroupMember, GroupInviteMemberJob.Details)? in // Generate authData for the removed member @@ -206,8 +210,11 @@ extension MessageSender { let memberAuthInfo: Authentication.Info = try? dependencies.mutate(cache: .libSession, { cache in try dependencies[singleton: .crypto].tryGenerate( .memberAuthData( - config: cache.config(for: .groupKeys, sessionId: groupSessionId), - groupSessionId: groupSessionId, + config: cache.config( + for: .groupKeys, + sessionId: preparedGroupData.groupSessionId + ), + groupSessionId: preparedGroupData.groupSessionId, memberId: member.profileId ) ) @@ -225,7 +232,7 @@ extension MessageSender { db, job: Job( variant: .groupInviteMember, - threadId: thread.id, + threadId: preparedGroupData.thread.id, details: jobDetails ), canStartJob: true @@ -234,7 +241,7 @@ extension MessageSender { } } ) - .map { _, _, thread, _, _ in thread } + .map { $0.thread } .eraseToAnyPublisher() } diff --git a/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift b/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift index 61cbe319a9..cfe7b38491 100644 --- a/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift +++ b/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift @@ -185,13 +185,13 @@ public extension Authentication { } switch (authData.groupIdentityPrivateKey, authData.authData) { - case (.some(let privateKey), _): + case (.some(let privateKey), _) where !privateKey.isEmpty: return Authentication.groupAdmin( groupSessionId: sessionId, ed25519SecretKey: Array(privateKey) ) - case (_, .some(let authData)): + case (_, .some(let authData)) where !authData.isEmpty: return Authentication.groupMember( groupSessionId: sessionId, authData: authData From aae7bfe8da28c8382a51cdbd4e1355612e07aa88 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 5 Sep 2025 16:33:13 +1000 Subject: [PATCH 36/59] Updated mocking setup, working through fixing tests --- Session.xcodeproj/project.pbxproj | 683 ++++- .../xcshareddata/swiftpm/Package.resolved | 35 +- Session/Meta/AppDelegate.swift | 7 +- .../Settings.bundle/ThirdPartyLicenses.plist | 702 +++++ .../Notifications/NotificationPresenter.swift | 2 +- .../Types/Request+OpenGroupAPI.swift | 2 +- .../NotificationsManagerType.swift | 4 +- .../Types/NotificationCategory.swift | 2 +- .../Types/NotificationContent.swift | 6 +- .../Types/Request+PushNotificationAPI.swift | 2 +- .../Pollers/CommunityPoller.swift | 36 +- .../Pollers/GroupPoller.swift | 34 +- .../Pollers/PollerType.swift | 2 +- .../Utilities/ExtensionHelper.swift | 24 +- .../Utilities/Preferences+Sound.swift | 2 +- .../Utilities/Preferences.swift | 2 +- .../MessageReceiverGroupsSpec.swift | 2622 +++++++++-------- .../Pollers/CommunityPollerManagerSpec.swift | 235 ++ .../Pollers/CommunityPollerSpec.swift | 186 -- ...SMK.swift => ArgumentDescribing+SMK.swift} | 5 +- .../CommonSMKMockExtensions.swift | 166 -- .../_TestUtilities/MockCommunityPoller.swift | 14 - .../MockCommunityPollerCache.swift | 30 +- .../_TestUtilities/MockGroupPollerCache.swift | 23 +- .../_TestUtilities/MockLibSessionCache.swift | 14 +- .../MockNotificationsManager.swift | 4 +- .../_TestUtilities/MockPoller.swift | 86 +- .../_TestUtilities/MockSwarmPoller.swift | 78 - .../_TestUtilities/Mocked+SMK.swift | 247 ++ .../LibSession/LibSession+Networking.swift | 16 +- .../Models/DeleteAllBeforeRequest.swift | 78 - .../Models/DeleteAllBeforeResponse.swift | 57 - .../Models/DeleteAllMessagesRequest.swift | 12 +- .../Models/SnodeBatchRequest.swift | 63 - .../Models/SnodeRequest.swift | 50 - SessionNetworkingKit/Types/Destination.swift | 6 +- SessionNetworkingKit/Types/Network.swift | 16 +- .../Types/UpdatableTimestamp.swift | 7 - .../Models/SnodeRequestSpec.swift | 74 - .../Types/BatchRequestSpec.swift | 55 +- .../Types/PreparedRequestSendingSpec.swift | 63 +- .../Types/PreparedRequestSpec.swift | 4 +- .../Types/RequestSpec.swift | 14 +- .../CommonSSKMockExtensions.swift | 45 - .../_TestUtilities/MockNetwork.swift | 132 +- .../_TestUtilities/Mocked+SNK.swift | 53 + .../NSENotificationPresenter.swift | 2 +- ...eadDisappearingMessagesViewModelSpec.swift | 23 +- ...eadNotificationSettingsViewModelSpec.swift | 44 +- SessionUtilitiesKit/Database/Storage.swift | 2 +- .../LibSession/Types/ObservingDatabase.swift | 14 +- .../JobRunner/JobRunnerSpec.swift | 24 +- .../_TestUtilities/Mocked+SUK.swift | 99 + TestUtilities/ArgumentDescribing.swift | 142 + TestUtilities/MockError.swift | 36 + TestUtilities/MockFunction.swift | 80 + TestUtilities/MockFunctionBuilder.swift | 110 + TestUtilities/MockFunctionHandler.swift | 8 + TestUtilities/MockHandler.swift | 284 ++ TestUtilities/Mockable.swift | 28 + TestUtilities/Mocked.swift | 170 ++ .../Nimble/NimbleFailureReporter.swift | 12 + TestUtilities/Nimble/NimbleVerification.swift | 78 + TestUtilities/RecordedCall.swift | 8 + TestUtilities/TestFailureReporter.swift | 14 + TestUtilities/TextContext.swift | 28 + TestUtilities/Utilities/Async+Utilities.swift | 16 + _SharedTestUtilities/FixtureBase.swift | 104 + _SharedTestUtilities/Mock.swift | 96 +- _SharedTestUtilities/MockAppContext.swift | 45 +- _SharedTestUtilities/Mocked.swift | 153 - _SharedTestUtilities/NimbleExtensions.swift | 10 +- _SharedTestUtilities/Quick+TestContext.swift | 35 + _SharedTestUtilities/SynchronousStorage.swift | 38 +- _SharedTestUtilities/TestDependencies.swift | 50 +- 75 files changed, 5012 insertions(+), 2741 deletions(-) create mode 100644 SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift delete mode 100644 SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift rename SessionMessagingKitTests/_TestUtilities/{CustomArgSummaryDescribable+SMK.swift => ArgumentDescribing+SMK.swift} (94%) delete mode 100644 SessionMessagingKitTests/_TestUtilities/CommonSMKMockExtensions.swift delete mode 100644 SessionMessagingKitTests/_TestUtilities/MockCommunityPoller.swift delete mode 100644 SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift create mode 100644 SessionMessagingKitTests/_TestUtilities/Mocked+SMK.swift delete mode 100644 SessionNetworkingKit/Models/DeleteAllBeforeRequest.swift delete mode 100644 SessionNetworkingKit/Models/DeleteAllBeforeResponse.swift delete mode 100644 SessionNetworkingKit/Models/SnodeBatchRequest.swift delete mode 100644 SessionNetworkingKit/Models/SnodeRequest.swift delete mode 100644 SessionNetworkingKit/Types/UpdatableTimestamp.swift delete mode 100644 SessionNetworkingKitTests/Models/SnodeRequestSpec.swift delete mode 100644 SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift create mode 100644 SessionNetworkingKitTests/_TestUtilities/Mocked+SNK.swift create mode 100644 SessionUtilitiesKitTests/_TestUtilities/Mocked+SUK.swift create mode 100644 TestUtilities/ArgumentDescribing.swift create mode 100644 TestUtilities/MockError.swift create mode 100644 TestUtilities/MockFunction.swift create mode 100644 TestUtilities/MockFunctionBuilder.swift create mode 100644 TestUtilities/MockFunctionHandler.swift create mode 100644 TestUtilities/MockHandler.swift create mode 100644 TestUtilities/Mockable.swift create mode 100644 TestUtilities/Mocked.swift create mode 100644 TestUtilities/Nimble/NimbleFailureReporter.swift create mode 100644 TestUtilities/Nimble/NimbleVerification.swift create mode 100644 TestUtilities/RecordedCall.swift create mode 100644 TestUtilities/TestFailureReporter.swift create mode 100644 TestUtilities/TextContext.swift create mode 100644 TestUtilities/Utilities/Async+Utilities.swift create mode 100644 _SharedTestUtilities/FixtureBase.swift delete mode 100644 _SharedTestUtilities/Mocked.swift create mode 100644 _SharedTestUtilities/Quick+TestContext.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 4f69a7b2ab..2d528f1939 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -430,9 +430,6 @@ FD0559562E026E1B00DC48CE /* ObservingDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0559542E026CC900DC48CE /* ObservingDatabase.swift */; }; FD0606C32BCE13ED00C3816E /* MessageRequestFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0606C22BCE13ED00C3816E /* MessageRequestFooterView.swift */; }; FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */; }; - FD0969F92A69FFE700C5C365 /* Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0969F82A69FFE700C5C365 /* Mocked.swift */; }; - FD0969FA2A6A00B000C5C365 /* Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0969F82A69FFE700C5C365 /* Mocked.swift */; }; - FD0969FB2A6A00B100C5C365 /* Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0969F82A69FFE700C5C365 /* Mocked.swift */; }; FD09796B27F6C67500936362 /* Failable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796A27F6C67500936362 /* Failable.swift */; }; FD09796E27FA6D0000936362 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796D27FA6D0000936362 /* Contact.swift */; }; FD09797027FA6FF300936362 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796F27FA6FF300936362 /* Profile.swift */; }; @@ -483,6 +480,22 @@ FD1A55412E161AF6003761E4 /* Combine+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A55402E161AF3003761E4 /* Combine+Utilities.swift */; }; FD1A55432E179AED003761E4 /* ObservableKeyEvent+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A55422E179AE6003761E4 /* ObservableKeyEvent+Utilities.swift */; }; FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */; }; + FD1BDBD02E653625008EF998 /* Mockable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1BDBAC2E653200008EF998 /* Mockable.swift */; }; + FD1BDBD12E653625008EF998 /* MockHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1BDBB12E6532DB008EF998 /* MockHandler.swift */; }; + FD1BDBD32E653660008EF998 /* MockFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1BDBD22E65365E008EF998 /* MockFunction.swift */; }; + FD1BDBD72E653852008EF998 /* RecordedCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1BDBD62E65384F008EF998 /* RecordedCall.swift */; }; + FD1BDBD92E653868008EF998 /* MockError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1BDBD82E653866008EF998 /* MockError.swift */; }; + FD1BDBDB2E6538B4008EF998 /* MockFunctionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1BDBDA2E6538B0008EF998 /* MockFunctionBuilder.swift */; }; + FD1BDBDF2E655735008EF998 /* MockFunctionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1BDBDE2E655734008EF998 /* MockFunctionHandler.swift */; }; + FD1BDBE12E655B2A008EF998 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD1BDBE02E655B2A008EF998 /* Nimble */; }; + FD1BDBE32E655BB6008EF998 /* Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1BDBE22E655BB4008EF998 /* Mocked.swift */; }; + FD1BDBE52E655DDF008EF998 /* NimbleVerification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1BDBE42E655DDE008EF998 /* NimbleVerification.swift */; }; + FD1BDBE62E655EA8008EF998 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD1BDBC32E6535EE008EF998 /* TestUtilities.framework */; }; + FD1BDBEB2E655EAC008EF998 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD1BDBC32E6535EE008EF998 /* TestUtilities.framework */; }; + FD1BDBF02E655EB1008EF998 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD1BDBC32E6535EE008EF998 /* TestUtilities.framework */; }; + FD1BDBF52E655EB5008EF998 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD1BDBC32E6535EE008EF998 /* TestUtilities.framework */; }; + FD1BDBFB2E656539008EF998 /* TestFailureReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1BDBFA2E656538008EF998 /* TestFailureReporter.swift */; }; + FD1BDBFE2E656562008EF998 /* NimbleFailureReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1BDBFD2E656561008EF998 /* NimbleFailureReporter.swift */; }; FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; }; FD1D732E2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D732D2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift */; }; FD22726B2C32911C004D8A6C /* SendReadReceiptsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272532C32911A004D8A6C /* SendReadReceiptsJob.swift */; }; @@ -508,7 +521,6 @@ FD2272812C32911C004D8A6C /* UpdateProfilePictureJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22726A2C32911C004D8A6C /* UpdateProfilePictureJob.swift */; }; FD2272832C337830004D8A6C /* GroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272822C337830004D8A6C /* GroupPoller.swift */; }; FD2272A92C33E337004D8A6C /* ContentProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272952C33E335004D8A6C /* ContentProxy.swift */; }; - FD2272AA2C33E337004D8A6C /* UpdatableTimestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272962C33E335004D8A6C /* UpdatableTimestamp.swift */; }; FD2272AB2C33E337004D8A6C /* ValidatableResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272972C33E335004D8A6C /* ValidatableResponse.swift */; }; FD2272AC2C33E337004D8A6C /* IPv4.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272982C33E336004D8A6C /* IPv4.swift */; }; FD2272AD2C33E337004D8A6C /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272992C33E336004D8A6C /* Network.swift */; }; @@ -589,18 +601,16 @@ FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; }; FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */; }; FD2DD5902C6DD13C0073D9BE /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD2DD58F2C6DD13C0073D9BE /* DifferenceKit */; }; - FD336F602CAA28CF00C0B51B /* CommonSMKMockExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */; }; + FD336F602CAA28CF00C0B51B /* Mocked+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F562CAA28CF00C0B51B /* Mocked+SMK.swift */; }; FD336F612CAA28CF00C0B51B /* MockNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5C2CAA28CF00C0B51B /* MockNotificationsManager.swift */; }; - FD336F622CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */; }; + FD336F622CAA28CF00C0B51B /* ArgumentDescribing+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* ArgumentDescribing+SMK.swift */; }; FD336F632CAA28CF00C0B51B /* MockOGMCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5D2CAA28CF00C0B51B /* MockOGMCache.swift */; }; FD336F642CAA28CF00C0B51B /* MockCommunityPollerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */; }; FD336F652CAA28CF00C0B51B /* MockDisplayPictureCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */; }; - FD336F662CAA28CF00C0B51B /* MockSwarmPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5F2CAA28CF00C0B51B /* MockSwarmPoller.swift */; }; FD336F672CAA28CF00C0B51B /* MockGroupPollerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5A2CAA28CF00C0B51B /* MockGroupPollerCache.swift */; }; FD336F682CAA28CF00C0B51B /* MockPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5E2CAA28CF00C0B51B /* MockPoller.swift */; }; FD336F692CAA28CF00C0B51B /* MockLibSessionCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5B2CAA28CF00C0B51B /* MockLibSessionCache.swift */; }; - FD336F6C2CAA29C600C0B51B /* CommunityPollerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F6B2CAA29C200C0B51B /* CommunityPollerSpec.swift */; }; - FD336F6F2CAA37CB00C0B51B /* MockCommunityPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F6E2CAA37CB00C0B51B /* MockCommunityPoller.swift */; }; + FD336F6C2CAA29C600C0B51B /* CommunityPollerManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F6B2CAA29C200C0B51B /* CommunityPollerManagerSpec.swift */; }; FD336F702CABB96C00C0B51B /* BatchRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD01502D2CA24310005B08A1 /* BatchRequestSpec.swift */; }; FD336F712CABB97800C0B51B /* DestinationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD11E22F2CA4F498001BAF58 /* DestinationSpec.swift */; }; FD336F722CABB97800C0B51B /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD01502E2CA24310005B08A1 /* BatchResponseSpec.swift */; }; @@ -613,8 +623,8 @@ FD368A6A29DE9E30000DBF1E /* UIContextualAction+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */; }; FD3765DF2AD8F03100DC1489 /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; }; FD3765E02AD8F05100DC1489 /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; }; - FD3765E22AD8F53B00DC1489 /* CommonSSKMockExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E12AD8F53B00DC1489 /* CommonSSKMockExtensions.swift */; }; - FD3765E32AD8F56200DC1489 /* CommonSSKMockExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E12AD8F53B00DC1489 /* CommonSSKMockExtensions.swift */; }; + FD3765E22AD8F53B00DC1489 /* Mocked+SNK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E12AD8F53B00DC1489 /* Mocked+SNK.swift */; }; + FD3765E32AD8F56200DC1489 /* Mocked+SNK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E12AD8F53B00DC1489 /* Mocked+SNK.swift */; }; FD3765E72ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E62ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift */; }; FD3765E92ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E82ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift */; }; FD3765EA2ADE37B400DC1489 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD47E0AA2AA68EEA00A55E41 /* Authentication.swift */; }; @@ -679,8 +689,8 @@ FD481A962CAE0AE000ECC4CF /* MockAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481A932CAE0ADD00ECC4CF /* MockAppContext.swift */; }; FD481A972CAE0AE000ECC4CF /* MockAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481A932CAE0ADD00ECC4CF /* MockAppContext.swift */; }; FD481A992CB4CAAA00ECC4CF /* MockLibSessionCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5B2CAA28CF00C0B51B /* MockLibSessionCache.swift */; }; - FD481A9A2CB4CAE500ECC4CF /* CommonSMKMockExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */; }; - FD481A9B2CB4CAF100ECC4CF /* CustomArgSummaryDescribable+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */; }; + FD481A9A2CB4CAE500ECC4CF /* Mocked+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F562CAA28CF00C0B51B /* Mocked+SMK.swift */; }; + FD481A9B2CB4CAF100ECC4CF /* ArgumentDescribing+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* ArgumentDescribing+SMK.swift */; }; FD481A9C2CB4D58300ECC4CF /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; }; FD481AA32CB889AE00ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481AA22CB889A400ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift */; }; FD49E2462B05C1D500FFBBB5 /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */; }; @@ -694,7 +704,7 @@ FD52090328B4680F006098F6 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090228B4680F006098F6 /* RadioButton.swift */; }; FD52090528B4915F006098F6 /* PrivacySettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */; }; FD52CB5A2E12166F00A4DA70 /* OnboardingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52CB592E12166D00A4DA70 /* OnboardingSpec.swift */; }; - FD52CB5B2E123FBC00A4DA70 /* CommonSSKMockExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E12AD8F53B00DC1489 /* CommonSSKMockExtensions.swift */; }; + FD52CB5B2E123FBC00A4DA70 /* Mocked+SNK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E12AD8F53B00DC1489 /* Mocked+SNK.swift */; }; FD52CB5C2E12536400A4DA70 /* MockExtensionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD981BCC2DC81ABB00564172 /* MockExtensionHelper.swift */; }; FD52CB632E13B61700A4DA70 /* ObservableKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52CB622E13B61700A4DA70 /* ObservableKey.swift */; }; FD52CB652E13B6E900A4DA70 /* ObservationBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52CB642E13B6E600A4DA70 /* ObservationBuilder.swift */; }; @@ -734,6 +744,22 @@ FD6A393B2C2AD3A300762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393A2C2AD3A300762359 /* Nimble */; }; FD6A393D2C2AD3AC00762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393C2C2AD3AC00762359 /* Nimble */; }; FD6A39412C2AD3B600762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39402C2AD3B600762359 /* Nimble */; }; + FD6B925A2E66C997004463B5 /* ArgumentDescribing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92592E66C994004463B5 /* ArgumentDescribing.swift */; }; + FD6B925E2E695ACE004463B5 /* FixtureBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B925D2E695ACD004463B5 /* FixtureBase.swift */; }; + FD6B925F2E695ACE004463B5 /* FixtureBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B925D2E695ACD004463B5 /* FixtureBase.swift */; }; + FD6B92602E695ACE004463B5 /* FixtureBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B925D2E695ACD004463B5 /* FixtureBase.swift */; }; + FD6B92612E695ACE004463B5 /* FixtureBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B925D2E695ACD004463B5 /* FixtureBase.swift */; }; + FD6B92642E696EE0004463B5 /* Mocked+SUK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92632E696EDC004463B5 /* Mocked+SUK.swift */; }; + FD6B92652E697012004463B5 /* Mocked+SUK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92632E696EDC004463B5 /* Mocked+SUK.swift */; }; + FD6B92662E697012004463B5 /* Mocked+SUK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92632E696EDC004463B5 /* Mocked+SUK.swift */; }; + FD6B92672E697012004463B5 /* Mocked+SUK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92632E696EDC004463B5 /* Mocked+SUK.swift */; }; + FD6B926C2E6A7644004463B5 /* Async+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B926B2E6A763D004463B5 /* Async+Utilities.swift */; }; + FD6B92722E6AB045004463B5 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = FD6B92712E6AB045004463B5 /* Quick */; }; + FD6B92742E6AB097004463B5 /* TextContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92732E6AB095004463B5 /* TextContext.swift */; }; + FD6B92752E6AB25F004463B5 /* Quick+TestContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B926F2E6AB01F004463B5 /* Quick+TestContext.swift */; }; + FD6B92762E6AB25F004463B5 /* Quick+TestContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B926F2E6AB01F004463B5 /* Quick+TestContext.swift */; }; + FD6B92772E6AB25F004463B5 /* Quick+TestContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B926F2E6AB01F004463B5 /* Quick+TestContext.swift */; }; + FD6B92782E6AB25F004463B5 /* Quick+TestContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B926F2E6AB01F004463B5 /* Quick+TestContext.swift */; }; FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */; }; FD6D9CF92CA152B300F706A8 /* Session+SNUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754BB2C9B9A8E002A2623 /* Session+SNUIKit.swift */; }; FD6DA9CF2D015B440092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9CE2D015B440092085A /* Lucide */; }; @@ -882,7 +908,6 @@ FD9DD2732A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; }; FDA335F52D91157A007E0EB6 /* SessionImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA335F42D911576007E0EB6 /* SessionImageView.swift */; }; FDAA16762AC28A3B00DDBF77 /* UserDefaultsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAA16752AC28A3B00DDBF77 /* UserDefaultsType.swift */; }; - FDAA167B2AC28E2F00DDBF77 /* SnodeRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAA167A2AC28E2F00DDBF77 /* SnodeRequestSpec.swift */; }; FDAA167D2AC528A200DDBF77 /* Preferences+Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAA167C2AC528A200DDBF77 /* Preferences+Sound.swift */; }; FDAA167F2AC5290000DDBF77 /* Preferences+NotificationPreviewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAA167E2AC5290000DDBF77 /* Preferences+NotificationPreviewType.swift */; }; FDB11A4C2DCC527D00BEF49F /* NotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB11A4B2DCC527900BEF49F /* NotificationContent.swift */; }; @@ -919,7 +944,6 @@ FDB5DAE82A95D96C002C8721 /* MessageReceiver+Groups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAE72A95D96C002C8721 /* MessageReceiver+Groups.swift */; }; FDB5DAF32A96DD4F002C8721 /* PreparedRequest+Sending.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAF22A96DD4F002C8721 /* PreparedRequest+Sending.swift */; }; FDB5DB062A981C67002C8721 /* PreparedRequestSendingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DB052A981C67002C8721 /* PreparedRequestSendingSpec.swift */; }; - FDB5DB082A981F8B002C8721 /* Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0969F82A69FFE700C5C365 /* Mocked.swift */; }; FDB5DB092A981F8D002C8721 /* MockCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE272A67755C0000B97C /* MockCrypto.swift */; }; FDB5DB0B2A981F92002C8721 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; }; FDB5DB0C2A981F96002C8721 /* MockNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE312A67C38D0000B97C /* MockNetwork.swift */; }; @@ -1114,8 +1138,6 @@ FDF848C629405C5B007DCAE5 /* DeleteAllMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A429405C5A007DCAE5 /* DeleteAllMessagesRequest.swift */; }; FDF848C729405C5B007DCAE5 /* SendMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A529405C5A007DCAE5 /* SendMessageResponse.swift */; }; FDF848C829405C5B007DCAE5 /* ONSResolveRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A629405C5A007DCAE5 /* ONSResolveRequest.swift */; }; - FDF848C929405C5B007DCAE5 /* SnodeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A729405C5A007DCAE5 /* SnodeRequest.swift */; }; - FDF848CA29405C5B007DCAE5 /* DeleteAllBeforeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A829405C5A007DCAE5 /* DeleteAllBeforeRequest.swift */; }; FDF848CC29405C5B007DCAE5 /* SnodeReceivedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848AA29405C5A007DCAE5 /* SnodeReceivedMessage.swift */; }; FDF848CD29405C5B007DCAE5 /* GetNetworkTimestampResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848AB29405C5A007DCAE5 /* GetNetworkTimestampResponse.swift */; }; FDF848CE29405C5B007DCAE5 /* UpdateExpiryAllRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848AC29405C5A007DCAE5 /* UpdateExpiryAllRequest.swift */; }; @@ -1124,10 +1146,8 @@ FDF848D129405C5B007DCAE5 /* SnodeSwarmItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848AF29405C5A007DCAE5 /* SnodeSwarmItem.swift */; }; FDF848D229405C5B007DCAE5 /* LegacyGetMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B029405C5A007DCAE5 /* LegacyGetMessagesRequest.swift */; }; FDF848D329405C5B007DCAE5 /* UpdateExpiryAllResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B129405C5A007DCAE5 /* UpdateExpiryAllResponse.swift */; }; - FDF848D429405C5B007DCAE5 /* DeleteAllBeforeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B229405C5A007DCAE5 /* DeleteAllBeforeResponse.swift */; }; FDF848D529405C5B007DCAE5 /* DeleteAllMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B329405C5A007DCAE5 /* DeleteAllMessagesResponse.swift */; }; FDF848D629405C5B007DCAE5 /* SnodeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B429405C5A007DCAE5 /* SnodeMessage.swift */; }; - FDF848D729405C5B007DCAE5 /* SnodeBatchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B529405C5A007DCAE5 /* SnodeBatchRequest.swift */; }; FDF848D929405C5B007DCAE5 /* SnodeAuthenticatedRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B729405C5A007DCAE5 /* SnodeAuthenticatedRequestBody.swift */; }; FDF848DA29405C5B007DCAE5 /* GetMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B829405C5A007DCAE5 /* GetMessagesResponse.swift */; }; FDF848DB29405C5B007DCAE5 /* DeleteMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B929405C5A007DCAE5 /* DeleteMessagesResponse.swift */; }; @@ -1267,6 +1287,34 @@ remoteGlobalIDString = C33FD9AA255A548A00E217F9; remoteInfo = SignalUtilitiesKit; }; + FD1BDBE82E655EA8008EF998 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D221A080169C9E5E00537ABF /* Project object */; + proxyType = 1; + remoteGlobalIDString = FD1BDBC22E6535EE008EF998; + remoteInfo = TestUtilities; + }; + FD1BDBED2E655EAC008EF998 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D221A080169C9E5E00537ABF /* Project object */; + proxyType = 1; + remoteGlobalIDString = FD1BDBC22E6535EE008EF998; + remoteInfo = TestUtilities; + }; + FD1BDBF22E655EB1008EF998 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D221A080169C9E5E00537ABF /* Project object */; + proxyType = 1; + remoteGlobalIDString = FD1BDBC22E6535EE008EF998; + remoteInfo = TestUtilities; + }; + FD1BDBF72E655EB5008EF998 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D221A080169C9E5E00537ABF /* Project object */; + proxyType = 1; + remoteGlobalIDString = FD1BDBC22E6535EE008EF998; + remoteInfo = TestUtilities; + }; FD71160D28D00BAE00B47552 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D221A080169C9E5E00537ABF /* Project object */; @@ -1515,7 +1563,6 @@ 9409433F2C7ED62300D9D2E0 /* StartupError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupError.swift; sourceTree = ""; }; 941375BA2D5184B60058F244 /* HTTPHeader+SessionNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+SessionNetwork.swift"; sourceTree = ""; }; 941375BC2D5195F30058F244 /* KeyValueStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueStore.swift; sourceTree = ""; }; - 941375BE2D5196D10058F244 /* Number+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Number+Utilities.swift"; sourceTree = ""; }; 9422567D2C23F8BB00C0FDBF /* StartConversationScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartConversationScreen.swift; sourceTree = ""; }; 9422567E2C23F8BB00C0FDBF /* NewMessageScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewMessageScreen.swift; sourceTree = ""; }; 9422567F2C23F8BB00C0FDBF /* InviteAFriendScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InviteAFriendScreen.swift; sourceTree = ""; }; @@ -1801,7 +1848,6 @@ FD05594D2E012D1A00DC48CE /* _043_RenameAttachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _043_RenameAttachments.swift; sourceTree = ""; }; FD0559542E026CC900DC48CE /* ObservingDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservingDatabase.swift; sourceTree = ""; }; FD0606C22BCE13ED00C3816E /* MessageRequestFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestFooterView.swift; sourceTree = ""; }; - FD0969F82A69FFE700C5C365 /* Mocked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocked.swift; sourceTree = ""; }; FD09796A27F6C67500936362 /* Failable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Failable.swift; sourceTree = ""; }; FD09796D27FA6D0000936362 /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = ""; }; FD09796F27FA6FF300936362 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = ""; }; @@ -1854,6 +1900,18 @@ FD1A55402E161AF3003761E4 /* Combine+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Combine+Utilities.swift"; sourceTree = ""; }; FD1A55422E179AE6003761E4 /* ObservableKeyEvent+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKeyEvent+Utilities.swift"; sourceTree = ""; }; FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Utilities.swift"; sourceTree = ""; }; + FD1BDBAC2E653200008EF998 /* Mockable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mockable.swift; sourceTree = ""; }; + FD1BDBB12E6532DB008EF998 /* MockHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockHandler.swift; sourceTree = ""; }; + FD1BDBC32E6535EE008EF998 /* TestUtilities.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TestUtilities.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FD1BDBD22E65365E008EF998 /* MockFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFunction.swift; sourceTree = ""; }; + FD1BDBD62E65384F008EF998 /* RecordedCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordedCall.swift; sourceTree = ""; }; + FD1BDBD82E653866008EF998 /* MockError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockError.swift; sourceTree = ""; }; + FD1BDBDA2E6538B0008EF998 /* MockFunctionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFunctionBuilder.swift; sourceTree = ""; }; + FD1BDBDE2E655734008EF998 /* MockFunctionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFunctionHandler.swift; sourceTree = ""; }; + FD1BDBE22E655BB4008EF998 /* Mocked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocked.swift; sourceTree = ""; }; + FD1BDBE42E655DDE008EF998 /* NimbleVerification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbleVerification.swift; sourceTree = ""; }; + FD1BDBFA2E656538008EF998 /* TestFailureReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestFailureReporter.swift; sourceTree = ""; }; + FD1BDBFD2E656561008EF998 /* NimbleFailureReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbleFailureReporter.swift; sourceTree = ""; }; FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = ""; }; FD1D732D2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _029_BlockCommunityMessageRequests.swift; sourceTree = ""; }; FD2272532C32911A004D8A6C /* SendReadReceiptsJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendReadReceiptsJob.swift; sourceTree = ""; }; @@ -1879,7 +1937,6 @@ FD22726A2C32911C004D8A6C /* UpdateProfilePictureJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateProfilePictureJob.swift; sourceTree = ""; }; FD2272822C337830004D8A6C /* GroupPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPoller.swift; sourceTree = ""; }; FD2272952C33E335004D8A6C /* ContentProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentProxy.swift; sourceTree = ""; }; - FD2272962C33E335004D8A6C /* UpdatableTimestamp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatableTimestamp.swift; sourceTree = ""; }; FD2272972C33E335004D8A6C /* ValidatableResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidatableResponse.swift; sourceTree = ""; }; FD2272982C33E336004D8A6C /* IPv4.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPv4.swift; sourceTree = ""; }; FD2272992C33E336004D8A6C /* Network.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; @@ -1924,8 +1981,8 @@ FD29598C2A43BC0B00888A17 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronousStorage.swift; sourceTree = ""; }; FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = ""; }; - FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonSMKMockExtensions.swift; sourceTree = ""; }; - FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomArgSummaryDescribable+SMK.swift"; sourceTree = ""; }; + FD336F562CAA28CF00C0B51B /* Mocked+SMK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mocked+SMK.swift"; sourceTree = ""; }; + FD336F572CAA28CF00C0B51B /* ArgumentDescribing+SMK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ArgumentDescribing+SMK.swift"; sourceTree = ""; }; FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCommunityPollerCache.swift; sourceTree = ""; }; FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDisplayPictureCache.swift; sourceTree = ""; }; FD336F5A2CAA28CF00C0B51B /* MockGroupPollerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGroupPollerCache.swift; sourceTree = ""; }; @@ -1933,14 +1990,12 @@ FD336F5C2CAA28CF00C0B51B /* MockNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNotificationsManager.swift; sourceTree = ""; }; FD336F5D2CAA28CF00C0B51B /* MockOGMCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOGMCache.swift; sourceTree = ""; }; FD336F5E2CAA28CF00C0B51B /* MockPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPoller.swift; sourceTree = ""; }; - FD336F5F2CAA28CF00C0B51B /* MockSwarmPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSwarmPoller.swift; sourceTree = ""; }; - FD336F6B2CAA29C200C0B51B /* CommunityPollerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityPollerSpec.swift; sourceTree = ""; }; - FD336F6E2CAA37CB00C0B51B /* MockCommunityPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCommunityPoller.swift; sourceTree = ""; }; + FD336F6B2CAA29C200C0B51B /* CommunityPollerManagerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityPollerManagerSpec.swift; sourceTree = ""; }; FD3559452CC1FF140088F2A9 /* _034_AddMissingWhisperFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _034_AddMissingWhisperFlag.swift; sourceTree = ""; }; FD368A6729DE8F9B000DBF1E /* _026_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _026_AddFTSIfNeeded.swift; sourceTree = ""; }; FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Utilities.swift"; sourceTree = ""; }; FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSnodeAPICache.swift; sourceTree = ""; }; - FD3765E12AD8F53B00DC1489 /* CommonSSKMockExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonSSKMockExtensions.swift; sourceTree = ""; }; + FD3765E12AD8F53B00DC1489 /* Mocked+SNK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mocked+SNK.swift"; sourceTree = ""; }; FD3765E62ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnrevokeSubaccountRequest.swift; sourceTree = ""; }; FD3765E82ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnrevokeSubaccountResponse.swift; sourceTree = ""; }; FD3765F32ADE5A0800DC1489 /* AuthenticatedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatedRequest.swift; sourceTree = ""; }; @@ -2041,6 +2096,12 @@ FD66CB2B2BF344C600268FAB /* SessionMessageKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SessionMessageKit.xctestplan; sourceTree = ""; }; FD6A38F02C2A66B100762359 /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = ""; }; FD6A7A6C2818C61500035AC1 /* _005_SNK_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_SNK_SetupStandardJobs.swift; sourceTree = ""; }; + FD6B92592E66C994004463B5 /* ArgumentDescribing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArgumentDescribing.swift; sourceTree = ""; }; + FD6B925D2E695ACD004463B5 /* FixtureBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixtureBase.swift; sourceTree = ""; }; + FD6B92632E696EDC004463B5 /* Mocked+SUK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mocked+SUK.swift"; sourceTree = ""; }; + FD6B926B2E6A763D004463B5 /* Async+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Async+Utilities.swift"; sourceTree = ""; }; + FD6B926F2E6AB01F004463B5 /* Quick+TestContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Quick+TestContext.swift"; sourceTree = ""; }; + FD6B92732E6AB095004463B5 /* TextContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextContext.swift; sourceTree = ""; }; FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopSessionCallManager.swift; sourceTree = ""; }; FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _021_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = ""; }; @@ -2168,7 +2229,6 @@ FD9DD2702A72516D00ECB68E /* TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestExtensions.swift; sourceTree = ""; }; FDA335F42D911576007E0EB6 /* SessionImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionImageView.swift; sourceTree = ""; }; FDAA16752AC28A3B00DDBF77 /* UserDefaultsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsType.swift; sourceTree = ""; }; - FDAA167A2AC28E2F00DDBF77 /* SnodeRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeRequestSpec.swift; sourceTree = ""; }; FDAA167C2AC528A200DDBF77 /* Preferences+Sound.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Sound.swift"; sourceTree = ""; }; FDAA167E2AC5290000DDBF77 /* Preferences+NotificationPreviewType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+NotificationPreviewType.swift"; sourceTree = ""; }; FDB11A4B2DCC527900BEF49F /* NotificationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContent.swift; sourceTree = ""; }; @@ -2391,8 +2451,6 @@ FDF848A429405C5A007DCAE5 /* DeleteAllMessagesRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteAllMessagesRequest.swift; sourceTree = ""; }; FDF848A529405C5A007DCAE5 /* SendMessageResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendMessageResponse.swift; sourceTree = ""; }; FDF848A629405C5A007DCAE5 /* ONSResolveRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ONSResolveRequest.swift; sourceTree = ""; }; - FDF848A729405C5A007DCAE5 /* SnodeRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeRequest.swift; sourceTree = ""; }; - FDF848A829405C5A007DCAE5 /* DeleteAllBeforeRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteAllBeforeRequest.swift; sourceTree = ""; }; FDF848AA29405C5A007DCAE5 /* SnodeReceivedMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeReceivedMessage.swift; sourceTree = ""; }; FDF848AB29405C5A007DCAE5 /* GetNetworkTimestampResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetNetworkTimestampResponse.swift; sourceTree = ""; }; FDF848AC29405C5A007DCAE5 /* UpdateExpiryAllRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateExpiryAllRequest.swift; sourceTree = ""; }; @@ -2401,10 +2459,8 @@ FDF848AF29405C5A007DCAE5 /* SnodeSwarmItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeSwarmItem.swift; sourceTree = ""; }; FDF848B029405C5A007DCAE5 /* LegacyGetMessagesRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyGetMessagesRequest.swift; sourceTree = ""; }; FDF848B129405C5A007DCAE5 /* UpdateExpiryAllResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateExpiryAllResponse.swift; sourceTree = ""; }; - FDF848B229405C5A007DCAE5 /* DeleteAllBeforeResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteAllBeforeResponse.swift; sourceTree = ""; }; FDF848B329405C5A007DCAE5 /* DeleteAllMessagesResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteAllMessagesResponse.swift; sourceTree = ""; }; FDF848B429405C5A007DCAE5 /* SnodeMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeMessage.swift; sourceTree = ""; }; - FDF848B529405C5A007DCAE5 /* SnodeBatchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeBatchRequest.swift; sourceTree = ""; }; FDF848B729405C5A007DCAE5 /* SnodeAuthenticatedRequestBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAuthenticatedRequestBody.swift; sourceTree = ""; }; FDF848B829405C5A007DCAE5 /* GetMessagesResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetMessagesResponse.swift; sourceTree = ""; }; FDF848B929405C5A007DCAE5 /* DeleteMessagesResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteMessagesResponse.swift; sourceTree = ""; }; @@ -2558,6 +2614,16 @@ FD0150542CA24471005B08A1 /* Nimble in Frameworks */, FD0150522CA2446D005B08A1 /* Quick in Frameworks */, FD0150502CA24468005B08A1 /* SessionNetworkingKit.framework in Frameworks */, + FD1BDBF02E655EB1008EF998 /* TestUtilities.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FD1BDBC02E6535EE008EF998 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FD6B92722E6AB045004463B5 /* Quick in Frameworks */, + FD1BDBE12E655B2A008EF998 /* Nimble in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2568,6 +2634,7 @@ FD2DD5902C6DD13C0073D9BE /* DifferenceKit in Frameworks */, FD6A39322C2AD33E00762359 /* Quick in Frameworks */, FD6A393B2C2AD3A300762359 /* Nimble in Frameworks */, + FD1BDBE62E655EA8008EF998 /* TestUtilities.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2578,6 +2645,7 @@ FD6A39412C2AD3B600762359 /* Nimble in Frameworks */, FD6A39382C2AD36900762359 /* Quick in Frameworks */, FD83B9B327CF200A005E1583 /* SessionUtilitiesKit.framework in Frameworks */, + FD1BDBF52E655EB5008EF998 /* TestUtilities.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2588,6 +2656,7 @@ FD6A393D2C2AD3AC00762359 /* Nimble in Frameworks */, FD6A39342C2AD35F00762359 /* Quick in Frameworks */, FDC4389227B9FFC700C60D73 /* SessionMessagingKit.framework in Frameworks */, + FD1BDBEB2E655EAC008EF998 /* TestUtilities.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3853,6 +3922,7 @@ C3C2A5A0255385C100C340D1 /* SessionNetworkingKit */, C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */, C33FD9AC255A548A00E217F9 /* SignalUtilitiesKit */, + FD1BDBCD2E653614008EF998 /* TestUtilities */, FD83B9BC27CF2215005E1583 /* _SharedTestUtilities */, FD71160A28D00BAE00B47552 /* SessionTests */, FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */, @@ -3879,6 +3949,7 @@ FD83B9AF27CF200A005E1583 /* SessionUtilitiesKitTests.xctest */, FD71160928D00BAE00B47552 /* SessionTests.xctest */, FDB5DAFA2A981C42002C8721 /* SessionNetworkingKitTests.xctest */, + FD1BDBC32E6535EE008EF998 /* TestUtilities.framework */, ); name = Products; sourceTree = ""; @@ -4118,6 +4189,35 @@ path = Database; sourceTree = ""; }; + FD1BDBCD2E653614008EF998 /* TestUtilities */ = { + isa = PBXGroup; + children = ( + FD1BDBFC2E656559008EF998 /* Nimble */, + FD6B926A2E6A7635004463B5 /* Utilities */, + FD6B92592E66C994004463B5 /* ArgumentDescribing.swift */, + FD1BDBAC2E653200008EF998 /* Mockable.swift */, + FD1BDBE22E655BB4008EF998 /* Mocked.swift */, + FD1BDBD82E653866008EF998 /* MockError.swift */, + FD1BDBD22E65365E008EF998 /* MockFunction.swift */, + FD1BDBDA2E6538B0008EF998 /* MockFunctionBuilder.swift */, + FD1BDBDE2E655734008EF998 /* MockFunctionHandler.swift */, + FD1BDBB12E6532DB008EF998 /* MockHandler.swift */, + FD1BDBD62E65384F008EF998 /* RecordedCall.swift */, + FD6B92732E6AB095004463B5 /* TextContext.swift */, + FD1BDBFA2E656538008EF998 /* TestFailureReporter.swift */, + ); + path = TestUtilities; + sourceTree = ""; + }; + FD1BDBFC2E656559008EF998 /* Nimble */ = { + isa = PBXGroup; + children = ( + FD1BDBFD2E656561008EF998 /* NimbleFailureReporter.swift */, + FD1BDBE42E655DDE008EF998 /* NimbleVerification.swift */, + ); + path = Nimble; + sourceTree = ""; + }; FD2272842C33E28D004D8A6C /* SnodeAPI */ = { isa = PBXGroup; children = ( @@ -4186,7 +4286,7 @@ FD336F6A2CAA29BC00C0B51B /* Pollers */ = { isa = PBXGroup; children = ( - FD336F6B2CAA29C200C0B51B /* CommunityPollerSpec.swift */, + FD336F6B2CAA29C200C0B51B /* CommunityPollerManagerSpec.swift */, ); path = Pollers; sourceTree = ""; @@ -4196,7 +4296,7 @@ children = ( FD23CE312A67C38D0000B97C /* MockNetwork.swift */, FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */, - FD3765E12AD8F53B00DC1489 /* CommonSSKMockExtensions.swift */, + FD3765E12AD8F53B00DC1489 /* Mocked+SNK.swift */, ); path = _TestUtilities; sourceTree = ""; @@ -4322,6 +4422,22 @@ path = Models; sourceTree = ""; }; + FD6B92622E696ED3004463B5 /* _TestUtilities */ = { + isa = PBXGroup; + children = ( + FD6B92632E696EDC004463B5 /* Mocked+SUK.swift */, + ); + path = _TestUtilities; + sourceTree = ""; + }; + FD6B926A2E6A7635004463B5 /* Utilities */ = { + isa = PBXGroup; + children = ( + FD6B926B2E6A763D004463B5 /* Async+Utilities.swift */, + ); + path = Utilities; + sourceTree = ""; + }; FD7115F528C8150600B47552 /* Combine */ = { isa = PBXGroup; children = ( @@ -4526,6 +4642,7 @@ isa = PBXGroup; children = ( FD0150252CA23D5B005B08A1 /* SessionUtilitiesKit.xctestplan */, + FD6B92622E696ED3004463B5 /* _TestUtilities */, FD37EA1228AB3F60003AE748 /* Database */, FD83B9B927CF20A5005E1583 /* General */, FDDF074829DAB35200E5E8B5 /* JobRunner */, @@ -4549,8 +4666,8 @@ FD83B9BC27CF2215005E1583 /* _SharedTestUtilities */ = { isa = PBXGroup; children = ( + FD6B925D2E695ACD004463B5 /* FixtureBase.swift */, FD0150472CA243CB005B08A1 /* Mock.swift */, - FD0969F82A69FFE700C5C365 /* Mocked.swift */, FD481A932CAE0ADD00ECC4CF /* MockAppContext.swift */, FD23CE272A67755C0000B97C /* MockCrypto.swift */, FD3FAB6A2AF1B27800DC5421 /* MockFileManager.swift */, @@ -4565,6 +4682,7 @@ FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */, FD23EA6028ED0B260058676E /* CombineExtensions.swift */, FD0150272CA23DB7005B08A1 /* GRDBExtensions.swift */, + FD6B926F2E6AB01F004463B5 /* Quick+TestContext.swift */, FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */, ); path = _SharedTestUtilities; @@ -4690,7 +4808,6 @@ isa = PBXGroup; children = ( FDE754AB2C9B967D002A2623 /* FileUploadResponseSpec.swift */, - FDAA167A2AC28E2F00DDBF77 /* SnodeRequestSpec.swift */, ); path = Models; sourceTree = ""; @@ -4855,9 +4972,8 @@ FDC4389B27BA01E300C60D73 /* _TestUtilities */ = { isa = PBXGroup; children = ( - FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */, - FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */, - FD336F6E2CAA37CB00C0B51B /* MockCommunityPoller.swift */, + FD336F572CAA28CF00C0B51B /* ArgumentDescribing+SMK.swift */, + FD336F562CAA28CF00C0B51B /* Mocked+SMK.swift */, FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */, FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */, FD981BCC2DC81ABB00564172 /* MockExtensionHelper.swift */, @@ -4867,7 +4983,6 @@ FD336F5C2CAA28CF00C0B51B /* MockNotificationsManager.swift */, FD336F5D2CAA28CF00C0B51B /* MockOGMCache.swift */, FD336F5E2CAA28CF00C0B51B /* MockPoller.swift */, - FD336F5F2CAA28CF00C0B51B /* MockSwarmPoller.swift */, ); path = _TestUtilities; sourceTree = ""; @@ -5026,7 +5141,6 @@ FDD23ADD2E44501B0057E853 /* RequestCategory.swift */, FD2272A52C33E337004D8A6C /* ResponseInfo.swift */, FD3937092E4B04DB00571F17 /* SwarmDrainer.swift */, - FD2272962C33E335004D8A6C /* UpdatableTimestamp.swift */, FD2272972C33E335004D8A6C /* ValidatableResponse.swift */, ); path = Types; @@ -5035,12 +5149,10 @@ FDF8489929405C5A007DCAE5 /* Models */ = { isa = PBXGroup; children = ( - FDF848A729405C5A007DCAE5 /* SnodeRequest.swift */, FDF8489D29405C5A007DCAE5 /* SnodeResponse.swift */, FDF8489A29405C5A007DCAE5 /* SnodeRecursiveResponse.swift */, FDF848AF29405C5A007DCAE5 /* SnodeSwarmItem.swift */, FDF848B729405C5A007DCAE5 /* SnodeAuthenticatedRequestBody.swift */, - FDF848B529405C5A007DCAE5 /* SnodeBatchRequest.swift */, FDF8489B29405C5A007DCAE5 /* GetMessagesRequest.swift */, FDF848B029405C5A007DCAE5 /* LegacyGetMessagesRequest.swift */, FDF848B829405C5A007DCAE5 /* GetMessagesResponse.swift */, @@ -5051,8 +5163,6 @@ FDF848B929405C5A007DCAE5 /* DeleteMessagesResponse.swift */, FDF848A429405C5A007DCAE5 /* DeleteAllMessagesRequest.swift */, FDF848B329405C5A007DCAE5 /* DeleteAllMessagesResponse.swift */, - FDF848A829405C5A007DCAE5 /* DeleteAllBeforeRequest.swift */, - FDF848B229405C5A007DCAE5 /* DeleteAllBeforeResponse.swift */, FDF8489F29405C5A007DCAE5 /* UpdateExpiryRequest.swift */, FDF848AE29405C5A007DCAE5 /* UpdateExpiryResponse.swift */, FDF848AC29405C5A007DCAE5 /* UpdateExpiryAllRequest.swift */, @@ -5159,6 +5269,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FD1BDBBE2E6535EE008EF998 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ @@ -5368,6 +5485,28 @@ productReference = D221A089169C9E5E00537ABF /* Session.app */; productType = "com.apple.product-type.application"; }; + FD1BDBC22E6535EE008EF998 /* TestUtilities */ = { + isa = PBXNativeTarget; + buildConfigurationList = FD1BDBC72E6535EE008EF998 /* Build configuration list for PBXNativeTarget "TestUtilities" */; + buildPhases = ( + FD1BDBBE2E6535EE008EF998 /* Headers */, + FD1BDBBF2E6535EE008EF998 /* Sources */, + FD1BDBC02E6535EE008EF998 /* Frameworks */, + FD1BDBC12E6535EE008EF998 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TestUtilities; + packageProductDependencies = ( + FD1BDBE02E655B2A008EF998 /* Nimble */, + FD6B92712E6AB045004463B5 /* Quick */, + ); + productName = TestUtilities; + productReference = FD1BDBC32E6535EE008EF998 /* TestUtilities.framework */; + productType = "com.apple.product-type.framework"; + }; FD71160828D00BAE00B47552 /* SessionTests */ = { isa = PBXNativeTarget; buildConfigurationList = FD71160F28D00BAE00B47552 /* Build configuration list for PBXNativeTarget "SessionTests" */; @@ -5380,6 +5519,7 @@ ); dependencies = ( FD71160E28D00BAE00B47552 /* PBXTargetDependency */, + FD1BDBE92E655EA8008EF998 /* PBXTargetDependency */, ); name = SessionTests; packageProductDependencies = ( @@ -5403,6 +5543,7 @@ ); dependencies = ( FD83B9B527CF200A005E1583 /* PBXTargetDependency */, + FD1BDBF82E655EB5008EF998 /* PBXTargetDependency */, ); name = SessionUtilitiesKitTests; packageProductDependencies = ( @@ -5425,6 +5566,7 @@ ); dependencies = ( FDB5DB002A981C43002C8721 /* PBXTargetDependency */, + FD1BDBF32E655EB1008EF998 /* PBXTargetDependency */, ); name = SessionNetworkingKitTests; productName = SessionSnodeKitTests; @@ -5443,6 +5585,7 @@ ); dependencies = ( FDC4389427B9FFC700C60D73 /* PBXTargetDependency */, + FD1BDBEE2E655EAC008EF998 /* PBXTargetDependency */, ); name = SessionMessagingKitTests; packageProductDependencies = ( @@ -5551,6 +5694,9 @@ }; }; }; + FD1BDBC22E6535EE008EF998 = { + CreatedOnToolsVersion = 16.3; + }; FD71160828D00BAE00B47552 = { CreatedOnToolsVersion = 13.4.1; TestTargetID = D221A088169C9E5E00537ABF; @@ -5605,6 +5751,7 @@ FDC4388D27B9FFC700C60D73 /* SessionMessagingKitTests */, FDB5DAF92A981C42002C8721 /* SessionNetworkingKitTests */, FD83B9AE27CF200A005E1583 /* SessionUtilitiesKitTests */, + FD1BDBC22E6535EE008EF998 /* TestUtilities */, ); }; /* End PBXProject section */ @@ -5729,6 +5876,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FD1BDBC12E6535EE008EF998 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; FD71160728D00BAE00B47552 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -6189,7 +6343,6 @@ FD2272AB2C33E337004D8A6C /* ValidatableResponse.swift in Sources */, FD2272B72C33E337004D8A6C /* Request.swift in Sources */, FD2272B92C33E337004D8A6C /* ResponseInfo.swift in Sources */, - FDF848D729405C5B007DCAE5 /* SnodeBatchRequest.swift in Sources */, FDF848CE29405C5B007DCAE5 /* UpdateExpiryAllRequest.swift in Sources */, FDF848C229405C5A007DCAE5 /* OxenDaemonRPCRequest.swift in Sources */, FDF848DC29405C5B007DCAE5 /* RevokeSubaccountRequest.swift in Sources */, @@ -6204,7 +6357,6 @@ FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */, FDB5DAF32A96DD4F002C8721 /* PreparedRequest+Sending.swift in Sources */, FDF848C629405C5B007DCAE5 /* DeleteAllMessagesRequest.swift in Sources */, - FDF848D429405C5B007DCAE5 /* DeleteAllBeforeResponse.swift in Sources */, FD47E0B52AA6D7AA00A55E41 /* Request+SnodeAPI.swift in Sources */, FD5E93D12C100FD70038C25A /* FileUploadResponse.swift in Sources */, FDF848D629405C5B007DCAE5 /* SnodeMessage.swift in Sources */, @@ -6226,7 +6378,6 @@ FDF848CC29405C5B007DCAE5 /* SnodeReceivedMessage.swift in Sources */, FDF848C129405C5A007DCAE5 /* UpdateExpiryRequest.swift in Sources */, FDF848C729405C5B007DCAE5 /* SendMessageResponse.swift in Sources */, - FDF848CA29405C5B007DCAE5 /* DeleteAllBeforeRequest.swift in Sources */, FD2272AF2C33E337004D8A6C /* JSON.swift in Sources */, FD2272D62C34ED6A004D8A6C /* RetryWithDependencies.swift in Sources */, FDF848D229405C5B007DCAE5 /* LegacyGetMessagesRequest.swift in Sources */, @@ -6245,13 +6396,11 @@ FD2286682C37DA3B00BC06F7 /* LibSession+Networking.swift in Sources */, FD2272A92C33E337004D8A6C /* ContentProxy.swift in Sources */, FDF848C829405C5B007DCAE5 /* ONSResolveRequest.swift in Sources */, - FDF848C929405C5B007DCAE5 /* SnodeRequest.swift in Sources */, FDF848CF29405C5B007DCAE5 /* SendMessageRequest.swift in Sources */, FD2272BE2C34B710004D8A6C /* Publisher+Utilities.swift in Sources */, FD3765E72ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift in Sources */, FD2272BC2C33E337004D8A6C /* URLResponse+Utilities.swift in Sources */, FD2272B52C33E337004D8A6C /* BatchResponse.swift in Sources */, - FD2272AA2C33E337004D8A6C /* UpdatableTimestamp.swift in Sources */, FD3765E92ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift in Sources */, FD5E93D22C12B0580038C25A /* AppVersionResponse.swift in Sources */, 941375BB2D5184C20058F244 /* HTTPHeader+SessionNetwork.swift in Sources */, @@ -6903,6 +7052,27 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + FD1BDBBF2E6535EE008EF998 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FD1BDBE52E655DDF008EF998 /* NimbleVerification.swift in Sources */, + FD1BDBE32E655BB6008EF998 /* Mocked.swift in Sources */, + FD1BDBDF2E655735008EF998 /* MockFunctionHandler.swift in Sources */, + FD1BDBD92E653868008EF998 /* MockError.swift in Sources */, + FD1BDBDB2E6538B4008EF998 /* MockFunctionBuilder.swift in Sources */, + FD6B92742E6AB097004463B5 /* TextContext.swift in Sources */, + FD1BDBD32E653660008EF998 /* MockFunction.swift in Sources */, + FD1BDBD72E653852008EF998 /* RecordedCall.swift in Sources */, + FD1BDBD02E653625008EF998 /* Mockable.swift in Sources */, + FD1BDBFE2E656562008EF998 /* NimbleFailureReporter.swift in Sources */, + FD1BDBFB2E656539008EF998 /* TestFailureReporter.swift in Sources */, + FD6B925A2E66C997004463B5 /* ArgumentDescribing.swift in Sources */, + FD1BDBD12E653625008EF998 /* MockHandler.swift in Sources */, + FD6B926C2E6A7644004463B5 /* Async+Utilities.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; FD71160528D00BAE00B47552 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -6911,6 +7081,7 @@ FD481A9C2CB4D58300ECC4CF /* MockSnodeAPICache.swift in Sources */, FD52CB5A2E12166F00A4DA70 /* OnboardingSpec.swift in Sources */, FD71161728D00DA400B47552 /* ThreadSettingsViewModelSpec.swift in Sources */, + FD6B92652E697012004463B5 /* Mocked+SUK.swift in Sources */, FD481A972CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */, FD23CE292A6775650000B97C /* MockCrypto.swift in Sources */, @@ -6919,16 +7090,16 @@ FD71161528D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift in Sources */, FD01503A2CA24328005B08A1 /* MockJobRunner.swift in Sources */, FD23EA5E28ED00FD0058676E /* NimbleExtensions.swift in Sources */, - FD481A9B2CB4CAF100ECC4CF /* CustomArgSummaryDescribable+SMK.swift in Sources */, + FD481A9B2CB4CAF100ECC4CF /* ArgumentDescribing+SMK.swift in Sources */, FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */, + FD6B925E2E695ACE004463B5 /* FixtureBase.swift in Sources */, FD71161A28D00E1100B47552 /* NotificationContentViewModelSpec.swift in Sources */, FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */, - FD52CB5B2E123FBC00A4DA70 /* CommonSSKMockExtensions.swift in Sources */, + FD52CB5B2E123FBC00A4DA70 /* Mocked+SNK.swift in Sources */, FD52CB5C2E12536400A4DA70 /* MockExtensionHelper.swift in Sources */, 9499E68B2DF92F4E00091434 /* ThreadNotificationSettingsViewModelSpec.swift in Sources */, FD01504B2CA243CB005B08A1 /* Mock.swift in Sources */, - FD0969FA2A6A00B000C5C365 /* Mocked.swift in Sources */, - FD481A9A2CB4CAE500ECC4CF /* CommonSMKMockExtensions.swift in Sources */, + FD481A9A2CB4CAE500ECC4CF /* Mocked+SMK.swift in Sources */, FD3FAB6C2AF1B28B00DC5421 /* MockFileManager.swift in Sources */, FD23EA6128ED0B260058676E /* CombineExtensions.swift in Sources */, FD9DD2712A72516D00ECB68E /* TestExtensions.swift in Sources */, @@ -6938,6 +7109,7 @@ FDB11A572DD17D0600BEF49F /* MockLogger.swift in Sources */, FD49E2462B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, FD01502C2CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, + FD6B92752E6AB25F004463B5 /* Quick+TestContext.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6946,6 +7118,7 @@ buildActionMask = 2147483647; files = ( FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */, + FD6B92782E6AB25F004463B5 /* Quick+TestContext.swift in Sources */, FD01504E2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift in Sources */, FD65318D2AA025C500DFEEAA /* TestDependencies.swift in Sources */, FD49E2492B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, @@ -6957,6 +7130,7 @@ FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */, FDB11A592DD17D0600BEF49F /* MockLogger.swift in Sources */, FD8A5B302DC18D61004C689B /* GeneralCacheSpec.swift in Sources */, + FD6B92612E695ACE004463B5 /* FixtureBase.swift in Sources */, FDD23AEE2E459E470057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */, FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FD0150402CA2433D005B08A1 /* BencodeDecoderSpec.swift in Sources */, @@ -6964,6 +7138,7 @@ FD0150422CA2433D005B08A1 /* VersionSpec.swift in Sources */, FD42ECD42E32FF2E002D03EA /* StringUtilitiesSpec.swift in Sources */, FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */, + FD6B92642E696EE0004463B5 /* Mocked+SUK.swift in Sources */, FD0150482CA243CB005B08A1 /* Mock.swift in Sources */, FDFE75B42ABD46B600655640 /* MockUserDefaults.swift in Sources */, FD0150392CA24328005B08A1 /* MockJobRunner.swift in Sources */, @@ -6973,7 +7148,6 @@ FD0B77B229B82B7A009169BA /* ArrayUtilitiesSpec.swift in Sources */, FD23CE262A676B5B0000B97C /* DependenciesSpec.swift in Sources */, FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */, - FD0969FB2A6A00B100C5C365 /* Mocked.swift in Sources */, FD0150292CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, FDD23AEF2E459EC90057E853 /* _012_AddJobPriority.swift in Sources */, FD481A942CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, @@ -6986,9 +7160,8 @@ files = ( FDB5DB062A981C67002C8721 /* PreparedRequestSendingSpec.swift in Sources */, FD49E2482B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, - FDB5DB082A981F8B002C8721 /* Mocked.swift in Sources */, FD336F702CABB96C00C0B51B /* BatchRequestSpec.swift in Sources */, - FD3765E22AD8F53B00DC1489 /* CommonSSKMockExtensions.swift in Sources */, + FD3765E22AD8F53B00DC1489 /* Mocked+SNK.swift in Sources */, FDB5DB142A981FAE002C8721 /* CombineExtensions.swift in Sources */, FD0150382CA24328005B08A1 /* MockJobRunner.swift in Sources */, FD481A952CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, @@ -6996,15 +7169,16 @@ FDB11A582DD17D0600BEF49F /* MockLogger.swift in Sources */, FDB5DB112A981FA6002C8721 /* TestExtensions.swift in Sources */, FDB5DB092A981F8D002C8721 /* MockCrypto.swift in Sources */, - FDAA167B2AC28E2F00DDBF77 /* SnodeRequestSpec.swift in Sources */, FD65318C2AA025C500DFEEAA /* TestDependencies.swift in Sources */, FDB5DB102A981FA3002C8721 /* TestConstants.swift in Sources */, FDB5DB0C2A981F96002C8721 /* MockNetwork.swift in Sources */, + FD6B92672E697012004463B5 /* Mocked+SUK.swift in Sources */, FD336F712CABB97800C0B51B /* DestinationSpec.swift in Sources */, FD336F722CABB97800C0B51B /* BatchResponseSpec.swift in Sources */, FD336F732CABB97800C0B51B /* HeaderSpec.swift in Sources */, FD336F742CABB97800C0B51B /* PreparedRequestSpec.swift in Sources */, FD336F752CABB97800C0B51B /* RequestSpec.swift in Sources */, + FD6B92602E695ACE004463B5 /* FixtureBase.swift in Sources */, FD336F762CABB97800C0B51B /* BencodeResponseSpec.swift in Sources */, FDB5DB0B2A981F92002C8721 /* MockGeneralCache.swift in Sources */, FD0150492CA243CB005B08A1 /* Mock.swift in Sources */, @@ -7012,6 +7186,7 @@ FD0150282CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, FDB5DB152A981FB0002C8721 /* SynchronousStorage.swift in Sources */, FDE754AC2C9B967E002A2623 /* FileUploadResponseSpec.swift in Sources */, + FD6B92772E6AB25F004463B5 /* Quick+TestContext.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7032,20 +7207,19 @@ FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */, FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */, FD23CE2A2A6775660000B97C /* MockCrypto.swift in Sources */, - FD336F6F2CAA37CB00C0B51B /* MockCommunityPoller.swift in Sources */, FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */, + FD6B925F2E695ACE004463B5 /* FixtureBase.swift in Sources */, FD481A962CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, FDB11A562DD17C3300BEF49F /* MockLogger.swift in Sources */, FD0150452CA243BB005B08A1 /* LibSessionUtilSpec.swift in Sources */, FD981BCD2DC81ABF00564172 /* MockExtensionHelper.swift in Sources */, - FD336F602CAA28CF00C0B51B /* CommonSMKMockExtensions.swift in Sources */, + FD336F602CAA28CF00C0B51B /* Mocked+SMK.swift in Sources */, FD336F612CAA28CF00C0B51B /* MockNotificationsManager.swift in Sources */, - FD336F622CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift in Sources */, + FD336F622CAA28CF00C0B51B /* ArgumentDescribing+SMK.swift in Sources */, FD336F632CAA28CF00C0B51B /* MockOGMCache.swift in Sources */, FD481A922CAD17DE00ECC4CF /* LibSessionGroupMembersSpec.swift in Sources */, FD336F642CAA28CF00C0B51B /* MockCommunityPollerCache.swift in Sources */, FD336F652CAA28CF00C0B51B /* MockDisplayPictureCache.swift in Sources */, - FD336F662CAA28CF00C0B51B /* MockSwarmPoller.swift in Sources */, FD336F672CAA28CF00C0B51B /* MockGroupPollerCache.swift in Sources */, FD336F682CAA28CF00C0B51B /* MockPoller.swift in Sources */, FD336F692CAA28CF00C0B51B /* MockLibSessionCache.swift in Sources */, @@ -7056,7 +7230,8 @@ FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */, FD65318B2AA025C500DFEEAA /* TestDependencies.swift in Sources */, FD49E2472B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, - FD3765E32AD8F56200DC1489 /* CommonSSKMockExtensions.swift in Sources */, + FD6B92662E697012004463B5 /* Mocked+SUK.swift in Sources */, + FD3765E32AD8F56200DC1489 /* Mocked+SNK.swift in Sources */, FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */, FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */, FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */, @@ -7065,14 +7240,14 @@ FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */, FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */, - FD0969F92A69FFE700C5C365 /* Mocked.swift in Sources */, FD481A902CAD16F100ECC4CF /* LibSessionGroupInfoSpec.swift in Sources */, FDE754A92C9B964D002A2623 /* MessageSenderGroupsSpec.swift in Sources */, FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */, + FD6B92762E6AB25F004463B5 /* Quick+TestContext.swift in Sources */, FD3FAB672AF0C47000DC5421 /* DisplayPictureDownloadJobSpec.swift in Sources */, FD72BDA42BE3690B00CF6CF6 /* CryptoSMKSpec.swift in Sources */, FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */, - FD336F6C2CAA29C600C0B51B /* CommunityPollerSpec.swift in Sources */, + FD336F6C2CAA29C600C0B51B /* CommunityPollerManagerSpec.swift in Sources */, FD481AA32CB889AE00ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift in Sources */, FDFD645D27F273F300808CA1 /* MockGeneralCache.swift in Sources */, FD01502A2CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, @@ -7170,6 +7345,26 @@ target = C33FD9AA255A548A00E217F9 /* SignalUtilitiesKit */; targetProxy = C3D90A7025773A44002C9DF5 /* PBXContainerItemProxy */; }; + FD1BDBE92E655EA8008EF998 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FD1BDBC22E6535EE008EF998 /* TestUtilities */; + targetProxy = FD1BDBE82E655EA8008EF998 /* PBXContainerItemProxy */; + }; + FD1BDBEE2E655EAC008EF998 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FD1BDBC22E6535EE008EF998 /* TestUtilities */; + targetProxy = FD1BDBED2E655EAC008EF998 /* PBXContainerItemProxy */; + }; + FD1BDBF32E655EB1008EF998 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FD1BDBC22E6535EE008EF998 /* TestUtilities */; + targetProxy = FD1BDBF22E655EB1008EF998 /* PBXContainerItemProxy */; + }; + FD1BDBF82E655EB5008EF998 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FD1BDBC22E6535EE008EF998 /* TestUtilities */; + targetProxy = FD1BDBF72E655EB5008EF998 /* PBXContainerItemProxy */; + }; FD71160E28D00BAE00B47552 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = D221A088169C9E5E00537ABF /* Session */; @@ -7270,7 +7465,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -7339,7 +7533,6 @@ SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; }; @@ -7388,7 +7581,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -7460,7 +7652,6 @@ SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; }; @@ -7509,7 +7700,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -7583,7 +7773,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -7642,7 +7831,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -7723,7 +7911,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -7780,7 +7967,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -7859,7 +8045,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -7915,7 +8100,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -7994,7 +8178,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -8051,7 +8234,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -8130,7 +8312,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -8215,6 +8396,7 @@ SDKROOT = iphoneos; STRIP_INSTALLED_PRODUCT = NO; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; @@ -8293,7 +8475,6 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; @@ -8340,7 +8521,6 @@ RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; SWIFT_OBJC_INTERFACE_HEADER_NAME = "Session-Swift.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; TEST_AFTER_BUILD = YES; WRAPPER_EXTENSION = app; @@ -8390,6 +8570,274 @@ }; name = App_Store_Release; }; + FD1BDBC82E6535EE008EF998 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Rangeproof Pty Ltd. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.getsession.TestUtilities; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + FD1BDBC92E6535EE008EF998 /* Debug_Compile_LibSession */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Rangeproof Pty Ltd. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.getsession.TestUtilities; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug_Compile_LibSession; + }; + FD1BDBCA2E6535EE008EF998 /* App_Store_Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Rangeproof Pty Ltd. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.getsession.TestUtilities; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = App_Store_Release; + }; + FD1BDBCB2E6535EE008EF998 /* App_Store_Release_Compile_LibSession */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Rangeproof Pty Ltd. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.getsession.TestUtilities; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_MODULE = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = App_Store_Release_Compile_LibSession; + }; FD2272502C32910F004D8A6C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -8438,7 +8886,6 @@ PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Session.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Session"; }; @@ -8495,7 +8942,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Session.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Session"; VALIDATE_PRODUCT = YES; @@ -8529,7 +8975,6 @@ PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionUtilitiesKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -8585,7 +9030,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -8636,7 +9080,6 @@ PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionMessagingKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -8692,7 +9135,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -8779,6 +9221,7 @@ STRIP_INSTALLED_PRODUCT = NO; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_INCLUDE_PATHS = "$(BUILT_PRODUCTS_DIR)/include/**"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; @@ -8824,7 +9267,6 @@ RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; SWIFT_OBJC_INTERFACE_HEADER_NAME = "Session-Swift.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; TEST_AFTER_BUILD = YES; WRAPPER_EXTENSION = app; @@ -8874,7 +9316,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 1; }; name = Debug_Compile_LibSession; @@ -8922,7 +9363,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 1; }; name = Debug_Compile_LibSession; @@ -8979,7 +9419,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -9030,7 +9469,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -9086,7 +9524,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -9142,7 +9579,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -9198,7 +9634,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -9231,7 +9666,6 @@ PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Session.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Session"; }; @@ -9263,7 +9697,6 @@ PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionMessagingKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug_Compile_LibSession; @@ -9294,7 +9727,6 @@ PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionUtilitiesKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug_Compile_LibSession; @@ -9373,7 +9805,6 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_INCLUDE_PATHS = "$(BUILT_PRODUCTS_DIR)/include/**"; - SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; @@ -9485,7 +9916,6 @@ SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; }; @@ -9558,7 +9988,6 @@ SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; }; @@ -9639,7 +10068,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -9715,7 +10143,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -9795,7 +10222,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -9875,7 +10301,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -9955,7 +10380,6 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -10013,7 +10437,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Session.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Session"; VALIDATE_PRODUCT = YES; @@ -10070,7 +10493,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -10126,7 +10548,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -10234,6 +10655,17 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = App_Store_Release; }; + FD1BDBC72E6535EE008EF998 /* Build configuration list for PBXNativeTarget "TestUtilities" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FD1BDBC82E6535EE008EF998 /* Debug */, + FD1BDBC92E6535EE008EF998 /* Debug_Compile_LibSession */, + FD1BDBCA2E6535EE008EF998 /* App_Store_Release */, + FD1BDBCB2E6535EE008EF998 /* App_Store_Release_Compile_LibSession */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = App_Store_Release; + }; FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionNetworkingKitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -10350,7 +10782,7 @@ repositoryURL = "https://github.com/Quick/Quick.git"; requirement = { kind = exactVersion; - version = 7.5.0; + version = 7.6.2; }; }; FD6A39392C2AD3A300762359 /* XCRemoteSwiftPackageReference "Nimble" */ = { @@ -10358,7 +10790,7 @@ repositoryURL = "https://github.com/Quick/Nimble.git"; requirement = { kind = exactVersion; - version = 13.3.0; + version = 13.7.1; }; }; FD6DA9D52D017F480092085A /* XCRemoteSwiftPackageReference "session-grdb-swift" */ = { @@ -10395,6 +10827,11 @@ package = FD6A39392C2AD3A300762359 /* XCRemoteSwiftPackageReference "Nimble" */; productName = Nimble; }; + FD1BDBE02E655B2A008EF998 /* Nimble */ = { + isa = XCSwiftPackageProductDependency; + package = FD6A39392C2AD3A300762359 /* XCRemoteSwiftPackageReference "Nimble" */; + productName = Nimble; + }; FD22866E2C38D42300BC06F7 /* DifferenceKit */ = { isa = XCSwiftPackageProductDependency; package = FD6A38ED2C2A641200762359 /* XCRemoteSwiftPackageReference "DifferenceKit" */; @@ -10420,21 +10857,6 @@ package = FD6A38ED2C2A641200762359 /* XCRemoteSwiftPackageReference "DifferenceKit" */; productName = DifferenceKit; }; - FD6673F52D7021E700041530 /* SessionUtil */ = { - isa = XCSwiftPackageProductDependency; - package = FD6673F42D7021E700041530 /* XCRemoteSwiftPackageReference "libsession-util-spm" */; - productName = SessionUtil; - }; - FD6673F72D7021F200041530 /* SessionUtil */ = { - isa = XCSwiftPackageProductDependency; - package = FD6673F42D7021E700041530 /* XCRemoteSwiftPackageReference "libsession-util-spm" */; - productName = SessionUtil; - }; - FD6673F92D7021F800041530 /* SessionUtil */ = { - isa = XCSwiftPackageProductDependency; - package = FD6673F42D7021E700041530 /* XCRemoteSwiftPackageReference "libsession-util-spm" */; - productName = SessionUtil; - }; FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */ = { isa = XCSwiftPackageProductDependency; package = FD6A38E72C2A630E00762359 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */; @@ -10490,6 +10912,11 @@ package = FD6A39392C2AD3A300762359 /* XCRemoteSwiftPackageReference "Nimble" */; productName = Nimble; }; + FD6B92712E6AB045004463B5 /* Quick */ = { + isa = XCSwiftPackageProductDependency; + package = FD6A39302C2AD33E00762359 /* XCRemoteSwiftPackageReference "Quick" */; + productName = Quick; + }; FD6DA9CE2D015B440092085A /* Lucide */ = { isa = XCSwiftPackageProductDependency; productName = Lucide; diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 40c1f21016..c6d1f1017c 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Quick/Nimble.git", "state" : { - "revision" : "1c49fc1243018f81a7ea99cb5e0985b00096e9f4", - "version" : "13.3.0" + "revision" : "7795df4fff1a9cd231fe4867ae54f4dc5f5734f9", + "version" : "13.7.1" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Quick/Quick.git", "state" : { - "revision" : "26529ff2209c40ae50fd642b031f930d9d68ea02", - "version" : "7.5.0" + "revision" : "1163a1b1b114a657c7432b63dd1f92ce99fe11a6", + "version" : "7.6.2" } }, { @@ -109,6 +109,24 @@ "version" : "0.473.0" } }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3", + "version" : "1.6.1" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", @@ -118,6 +136,15 @@ "version" : "1.6.1" } }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "bbadd4b853a33fd78c4ae977d17bb2af15eb3f2a", + "version" : "1.1.0" + } + }, { "identity" : "swift-protobuf", "kind" : "remoteSourceControl", diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 978e58872e..80a0f4f93b 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -471,8 +471,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// /// **Note:** This **MUST** be called before `dependencies[singleton: .appReadiness].setAppReady()` is /// called otherwise a user tapping on a notification may not open the conversation showing the message - dependencies[singleton: .extensionHelper].willLoadMessages() - Task(priority: .medium) { [dependencies] in do { try await dependencies[singleton: .extensionHelper].loadMessages() } catch { Log.error(.cat, "Failed to load messages from extensions: \(error)") } @@ -705,9 +703,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD self?.enableBackgroundRefreshIfNecessary() dependencies[singleton: .jobRunner].appDidBecomeActive() - await self?.startPollersIfNeeded() - - /// Fetch the Session Network info in the background + /// Kick off polling and fetch the Session Network info in the background + Task { await self?.startPollersIfNeeded() } Task { await dependencies[singleton: .sessionNetworkApiClient].fetchInfoInBackground() } if dependencies[singleton: .appContext].isMainApp { diff --git a/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist b/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist index 3848270117..93f837c372 100644 --- a/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist +++ b/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist @@ -1664,6 +1664,491 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. Title session-lucide + + License + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +## Runtime Library Exception to the Apache 2.0 License: ## + + + As an exception, if you use this Software to compile your source code and + portions of this Software are embedded into the binary product as a result, + you may redistribute such product without providing attribution as would + otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. + + Title + swift-algorithms + + + License + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +## Runtime Library Exception to the Apache 2.0 License: ## + + + As an exception, if you use this Software to compile your source code and + portions of this Software are embedded into the binary product as a result, + you may redistribute such product without providing attribution as would + otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. + + Title + swift-argument-parser + + + License + .gitignore +**/.gitignore +.licenseignore +.gitattributes +.git-blame-ignore-revs +.mailfilter +.mailmap +.spi.yml +.swift-format +.editorconfig +.github/* +*.md +*.mdoc +*.txt +*.yml +*.yaml +*.json +*.png +*.bash +*.cmake +*.cmake.in +Package.swift +**/Package.swift +Package@*.swift +**/Package@*.swift +Package.resolved +**/Package.resolved +.unacceptablelanguageignore +**/Snapshots/* + + Title + swift-argument-parser - . + + + License + @@===----------------------------------------------------------------------===@@ +@@ +@@ This source file is part of the Swift Argument Parser open source project +@@ +@@ Copyright (c) YEARS Apple Inc. and the Swift project authors +@@ Licensed under Apache License v2.0 with Runtime Library Exception +@@ +@@ See https://swift.org/LICENSE.txt for license information +@@ +@@===----------------------------------------------------------------------===@@ + + Title + swift-argument-parser - . + License @@ -2076,6 +2561,223 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. See the License for the specific language governing permissions and limitations under the License. + + +## Runtime Library Exception to the Apache 2.0 License: ## + + + As an exception, if you use this Software to compile your source code and + portions of this Software are embedded into the binary product as a result, + you may redistribute such product without providing attribution as would + otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. + + Title + swift-numerics + + + License + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ## Runtime Library Exception to the Apache 2.0 License: ## diff --git a/Session/Notifications/NotificationPresenter.swift b/Session/Notifications/NotificationPresenter.swift index c335c898bd..5f2e9f0bf5 100644 --- a/Session/Notifications/NotificationPresenter.swift +++ b/Session/Notifications/NotificationPresenter.swift @@ -193,7 +193,7 @@ public class NotificationPresenter: NSObject, UNUserNotificationCenterDelegate, } } - public func notificationUserInfo(threadId: String, threadVariant: SessionThread.Variant) -> [String: Any] { + public func notificationUserInfo(threadId: String, threadVariant: SessionThread.Variant) -> [String: AnyHashable] { return [ NotificationUserInfoKey.threadId: threadId, NotificationUserInfoKey.threadVariantRaw: threadVariant.rawValue diff --git a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift index 05a7fda686..e096467c5e 100644 --- a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift @@ -24,7 +24,7 @@ public extension Request where Endpoint == OpenGroupAPI.Endpoint { self = try Request( endpoint: endpoint, - destination: try .server( + destination: .server( method: method, server: server, queryParameters: queryParameters, diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift index b844d336dc..e803c76b4b 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift @@ -31,7 +31,7 @@ public protocol NotificationsManagerType { mentionsOnly: Bool, mutedUntil: TimeInterval? ) - func notificationUserInfo(threadId: String, threadVariant: SessionThread.Variant) -> [String: Any] + func notificationUserInfo(threadId: String, threadVariant: SessionThread.Variant) -> [String: AnyHashable] func notificationShouldPlaySound(applicationState: UIApplication.State) -> Bool func notifyForFailedSend( @@ -443,7 +443,7 @@ public struct NoopNotificationsManager: NotificationsManagerType, NoopDependency public func updateSettings(threadId: String, threadVariant: SessionThread.Variant, mentionsOnly: Bool, mutedUntil: TimeInterval?) { } - public func notificationUserInfo(threadId: String, threadVariant: SessionThread.Variant) -> [String: Any] { + public func notificationUserInfo(threadId: String, threadVariant: SessionThread.Variant) -> [String: AnyHashable] { return [:] } diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/NotificationCategory.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/NotificationCategory.swift index 3d340c63e7..3786203383 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/NotificationCategory.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/NotificationCategory.swift @@ -4,7 +4,7 @@ import Foundation -public enum NotificationCategory: CaseIterable { +public enum NotificationCategory: CaseIterable, Equatable { case incomingMessage case errorMessage case threadlessErrorMessage diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/NotificationContent.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/NotificationContent.swift index 2557384fb4..f215a8988b 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/NotificationContent.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/NotificationContent.swift @@ -3,7 +3,7 @@ import UIKit import UserNotifications -public struct NotificationContent { +public struct NotificationContent: Equatable { public let threadId: String? public let threadVariant: SessionThread.Variant? public let identifier: String @@ -12,7 +12,7 @@ public struct NotificationContent { public let body: String? public let delay: TimeInterval? public let sound: Preferences.Sound - public let userInfo: [AnyHashable: Any] + public let userInfo: [AnyHashable: AnyHashable] public let applicationState: UIApplication.State // MARK: - Init @@ -26,7 +26,7 @@ public struct NotificationContent { body: String? = nil, delay: TimeInterval? = nil, sound: Preferences.Sound = .none, - userInfo: [AnyHashable: Any] = [:], + userInfo: [AnyHashable: AnyHashable] = [:], applicationState: UIApplication.State ) { self.threadId = threadId diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift index 1cb0fb6465..6a751db9ce 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift @@ -17,7 +17,7 @@ public extension Request where Endpoint == PushNotificationAPI.Endpoint { ) throws { self = try Request( endpoint: endpoint, - destination: try .server( + destination: .server( method: method, server: PushNotificationAPI.server, queryParameters: queryParameters, diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift index fdaa4cd967..1616a5dc22 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift @@ -634,25 +634,23 @@ actor CommunityPollerManager: CommunityPollerManagerType { // MARK: - Functions public func startAllPollers() async { - Task { - let communityInfo: [CommunityPoller.Info] = try await dependencies[singleton: .storage].readAsync { db in - // The default room promise creates an OpenGroup with an empty `roomToken` value, - // we don't want to start a poller for this as the user hasn't actually joined a room - try OpenGroup - .select( - OpenGroup.Columns.server, - max(OpenGroup.Columns.pollFailureCount).forKey(CommunityPoller.Info.Columns.pollFailureCount) - ) - .filter(OpenGroup.Columns.isActive == true) - .filter(OpenGroup.Columns.roomToken != "") - .group(OpenGroup.Columns.server) - .asRequest(of: CommunityPoller.Info.self) - .fetchAll(db) - } - - for info in communityInfo { - await getOrCreatePoller(for: info).startIfNeeded() - } + let communityInfo: [CommunityPoller.Info] = ((try? await dependencies[singleton: .storage].readAsync { db in + // The default room promise creates an OpenGroup with an empty `roomToken` value, + // we don't want to start a poller for this as the user hasn't actually joined a room + try OpenGroup + .select( + OpenGroup.Columns.server, + max(OpenGroup.Columns.pollFailureCount).forKey(CommunityPoller.Info.Columns.pollFailureCount) + ) + .filter(OpenGroup.Columns.isActive == true) + .filter(OpenGroup.Columns.roomToken != "") + .group(OpenGroup.Columns.server) + .asRequest(of: CommunityPoller.Info.self) + .fetchAll(db) + }) ?? []) + + for info in communityInfo { + await getOrCreatePoller(for: info).startIfNeeded() } } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift index 29c40348e4..37090f3a41 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift @@ -215,26 +215,24 @@ public actor GroupPollerManager: GroupPollerManagerType { // MARK: - Functions public func startAllPollers() async { - Task { - let groupPublicKeys: Set = try await dependencies[singleton: .storage].readAsync { db in - try ClosedGroup - .select(.threadId) - .filter(ClosedGroup.Columns.shouldPoll == true) - .filter( - ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString - ) - .asRequest(of: String.self) - .fetchSet(db) - } - - for swarmPublicKey in groupPublicKeys { - await getOrCreatePoller(for: swarmPublicKey).startIfNeeded() - } + let groupPublicKeys: Set = ((try? await dependencies[singleton: .storage].readAsync { db in + try ClosedGroup + .select(.threadId) + .filter(ClosedGroup.Columns.shouldPoll == true) + .filter( + ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && + ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString + ) + .asRequest(of: String.self) + .fetchSet(db) + }) ?? []) + + for swarmPublicKey in groupPublicKeys { + await getOrCreatePoller(for: swarmPublicKey).startIfNeeded() } } - @discardableResult public func getOrCreatePoller(for swarmPublicKey: String) async -> any SwarmPollerType { + @discardableResult public func getOrCreatePoller(for swarmPublicKey: String) async -> any PollerType { guard let poller: GroupPoller = pollers[swarmPublicKey.lowercased()] else { let poller: GroupPoller = GroupPoller( pollerName: "Closed group poller with public key: \(swarmPublicKey)", // stringlint:ignore @@ -270,7 +268,7 @@ public actor GroupPollerManager: GroupPollerManagerType { public protocol GroupPollerManagerType { func startAllPollers() async - @discardableResult func getOrCreatePoller(for swarmPublicKey: String) async -> any SwarmPollerType + @discardableResult func getOrCreatePoller(for swarmPublicKey: String) async -> any PollerType func stopAndRemovePoller(for swarmPublicKey: String) async func stopAndRemoveAllPollers() async } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift index ec1cfcb87b..1074ff2759 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift @@ -15,7 +15,7 @@ public extension Log.Category { // MARK: - PollerDestination -public enum PollerDestination: Sendable { +public enum PollerDestination: Sendable, Equatable { case swarm(String) case server(String) diff --git a/SessionMessagingKit/Utilities/ExtensionHelper.swift b/SessionMessagingKit/Utilities/ExtensionHelper.swift index a91e73cfca..19900fffc2 100644 --- a/SessionMessagingKit/Utilities/ExtensionHelper.swift +++ b/SessionMessagingKit/Utilities/ExtensionHelper.swift @@ -44,7 +44,6 @@ public class ExtensionHelper: ExtensionHelperType { // stringlint:ignore_stop private let dependencies: Dependencies - private lazy var messagesLoadedStream: CurrentValueAsyncStream = CurrentValueAsyncStream(false) // MARK: - Initialization @@ -727,19 +726,11 @@ public class ExtensionHelper: ExtensionHelperType { try write(data: messageAsData, to: targetPath) } - public func willLoadMessages() { - /// We want to synchronously reset the `messagesLoadedStream` value to `false` - let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) - Task { - await messagesLoadedStream.send(false) - semaphore.signal() - } - semaphore.wait() - } - public func loadMessages() async throws { typealias MessageData = (namespace: SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?) + try Task.checkCancellation() + /// Retrieve all conversation file paths /// /// This will ignore any hidden files (just in case) and will also insert the current users conversation (ie. `Note to Self`) at @@ -763,8 +754,14 @@ public class ExtensionHelper: ExtensionHelperType { try await dependencies[singleton: .storage].writeAsync { [weak self, dependencies] db in guard let this = self else { return } + /// Stop processing if the task got cancelled + guard !Task.isCancelled else { return } + /// Process each conversation individually conversationHashes.forEach { conversationHash in + /// Stop processing if the task got cancelled + guard !Task.isCancelled else { return } + /// Retrieve and process any config messages /// /// For config message changes we want to load in every config for a conversation and process them all at once @@ -897,14 +894,12 @@ public class ExtensionHelper: ExtensionHelperType { } Log.info(.cat, "Finished: Successfully processed \(successStandardCount)/\(successStandardCount + failureStandardCount) standard messages, \(successConfigCount)/\(failureConfigCount) config messages.") - await messagesLoadedStream.send(true) } @discardableResult public func waitUntilMessagesAreLoaded(timeout: DispatchTimeInterval) async -> Bool { return await withThrowingTaskGroup(of: Bool.self) { [weak self] group in group.addTask { - guard await self?.messagesLoadedStream.currentValue != true else { return true } - _ = await self?.messagesLoadedStream.stream.first { $0 == true } + try? await self?.loadMessages() return true } group.addTask { @@ -1012,7 +1007,6 @@ public protocol ExtensionHelperType { isUnread: Bool, isMessageRequest: Bool ) throws - func willLoadMessages() func loadMessages() async throws @discardableResult func waitUntilMessagesAreLoaded(timeout: DispatchTimeInterval) async -> Bool } diff --git a/SessionMessagingKit/Utilities/Preferences+Sound.swift b/SessionMessagingKit/Utilities/Preferences+Sound.swift index 566d82414d..3e7184d002 100644 --- a/SessionMessagingKit/Utilities/Preferences+Sound.swift +++ b/SessionMessagingKit/Utilities/Preferences+Sound.swift @@ -15,7 +15,7 @@ private extension Log.Category { // MARK: - Preferences public extension Preferences { - enum Sound: Int, Sendable, Codable, Differentiable, ThreadSafeType { + enum Sound: Int, Sendable, Codable, Equatable, Differentiable, ThreadSafeType { public static var defaultiOSIncomingRingtone: Sound = .opening public static var defaultNotificationSound: Sound = .note diff --git a/SessionMessagingKit/Utilities/Preferences.swift b/SessionMessagingKit/Utilities/Preferences.swift index 3fd1d96279..9ecd18d950 100644 --- a/SessionMessagingKit/Utilities/Preferences.swift +++ b/SessionMessagingKit/Utilities/Preferences.swift @@ -107,7 +107,7 @@ public extension KeyValueStore.IntKey { } public enum Preferences { - public struct NotificationSettings { + public struct NotificationSettings: Equatable { public let previewType: Preferences.NotificationPreviewType public let sound: Preferences.Sound public let mentionsOnly: Bool diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index f731e18ecf..53efb1153d 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -8,418 +8,93 @@ import Nimble import SessionUtil import SessionUtilitiesKit import SessionUIKit +import TestUtilities @testable import SessionNetworkingKit @testable import SessionMessagingKit -class MessageReceiverGroupsSpec: QuickSpec { +class MessageReceiverGroupsSpec: AsyncSpec { override class func spec() { - // MARK: Configuration + @TestState var fixture: MessageReceiverGroupsTestFixture! - let groupSeed: Data = Data(hex: "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210") - @TestState var groupKeyPair: KeyPair! = Crypto(using: .any).generate(.ed25519KeyPair(seed: Array(groupSeed))) - @TestState var groupId: SessionId! = SessionId(.group, hex: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece") - @TestState var groupSecretKey: Data! = Data(hex: - "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210" + - "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece" - ) - @TestState var dependencies: TestDependencies! = TestDependencies { dependencies in - dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) - dependencies.forceSynchronous = true + beforeEach { + fixture = try await MessageReceiverGroupsTestFixture.create() } - @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( - customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, - using: dependencies, - initialData: { db in - try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) - try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - - try Profile( - id: "05\(TestConstants.publicKey)", - name: "TestCurrentUser" - ).insert(db) - } - ) - @TestState(defaults: .standard, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults( - initialSetup: { userDefaults in - userDefaults.when { $0.string(forKey: .any) }.thenReturn(nil) - } - ) - @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( - initialSetup: { jobRunner in - jobRunner - .when { $0.jobInfoFor(jobs: .any, state: .any, variant: .any) } - .thenReturn([:]) - jobRunner - .when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) } - .thenReturn(nil) - jobRunner - .when { $0.upsert(.any, job: .any, canStartJob: .any) } - .thenReturn(nil) - jobRunner - .when { $0.manuallyTriggerResult(.any, result: .any) } - .thenReturn(()) - } - ) - @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( - initialSetup: { network in - network - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } - .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1"))) - network - .when { $0.getSwarm(for: .any) } - .thenReturn([ - LibSession.Snode( - ip: "1.1.1.1", - quicPort: 1, - ed25519PubkeyHex: TestConstants.edPublicKey - ), - LibSession.Snode( - ip: "1.1.1.1", - quicPort: 2, - ed25519PubkeyHex: TestConstants.edPublicKey - ), - LibSession.Snode( - ip: "1.1.1.1", - quicPort: 3, - ed25519PubkeyHex: TestConstants.edPublicKey - ) - ]) - } - ) - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( - initialSetup: { crypto in - crypto - .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } - .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) - crypto - .when { $0.generate(.signatureSubaccount(config: .any, verificationBytes: .any, memberAuthData: .any)) } - .thenReturn(Authentication.Signature.subaccount( - subaccount: "TestSubAccount".bytes, - subaccountSig: "TestSubAccountSignature".bytes, - signature: "TestSignature".bytes - )) - crypto - .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } - .thenReturn(true) - crypto.when { $0.generate(.ed25519KeyPair(seed: .any)) }.thenReturn(groupKeyPair) - crypto - .when { $0.verify(.memberAuthData(groupSessionId: .any, ed25519SecretKey: .any, memberAuthData: .any)) } - .thenReturn(true) - crypto - .when { $0.generate(.hash(message: .any, key: .any, length: .any)) } - .thenReturn("TestHash".bytes) - } - ) - @TestState(singleton: .keychain, in: dependencies) var mockKeychain: MockKeychain! = MockKeychain( - initialSetup: { keychain in - keychain - .when { - try $0.migrateLegacyKeyIfNeeded( - legacyKey: .any, - legacyService: .any, - toKey: .pushNotificationEncryptionKey - ) - } - .thenReturn(()) - keychain - .when { - try $0.getOrGenerateEncryptionKey( - forKey: .any, - length: .any, - cat: .any, - legacyKey: .any, - legacyService: .any - ) - } - .thenReturn(Data([1, 2, 3])) - keychain - .when { try $0.data(forKey: .pushNotificationEncryptionKey) } - .thenReturn(Data((0..! - _ = user_groups_init(&conf, &secretKey, nil, 0, nil) - - return .userGroups(conf) - }() - @TestState var convoInfoVolatileConfig: LibSession.Config! = { - var conf: UnsafeMutablePointer! - _ = convo_info_volatile_init(&conf, &secretKey, nil, 0, nil) - - return .convoInfoVolatile(conf) - }() - @TestState var groupInfoConf: UnsafeMutablePointer! = { - var conf: UnsafeMutablePointer! - _ = groups_info_init(&conf, &groupEdPK, &groupEdSK, nil, 0, nil) - - return conf - }() - @TestState var groupMembersConf: UnsafeMutablePointer! = { - var conf: UnsafeMutablePointer! - _ = groups_members_init(&conf, &groupEdPK, &groupEdSK, nil, 0, nil) - - return conf - }() - @TestState var groupKeysConf: UnsafeMutablePointer! = { - var conf: UnsafeMutablePointer! - _ = groups_keys_init(&conf, &secretKey, &groupEdPK, &groupEdSK, groupInfoConf, groupMembersConf, nil, 0, nil) - - return conf - }() - @TestState var groupInfoConfig: LibSession.Config! = .groupInfo(groupInfoConf) - @TestState var groupMembersConfig: LibSession.Config! = .groupMembers(groupMembersConf) - @TestState var groupKeysConfig: LibSession.Config! = .groupKeys( - groupKeysConf, - info: groupInfoConf, - members: groupMembersConf - ) - @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { - $0.defaultInitialSetup( - configs: [ - .userGroups: userGroupsConfig, - .convoInfoVolatile: convoInfoVolatileConfig, - .groupInfo: groupInfoConfig, - .groupMembers: groupMembersConfig, - .groupKeys: groupKeysConfig - ] - ) - } - ) - @TestState(cache: .snodeAPI, in: dependencies) var mockSnodeAPICache: MockSnodeAPICache! = MockSnodeAPICache( - initialSetup: { $0.defaultInitialSetup() } - ) - @TestState var mockSwarmPoller: MockSwarmPoller! = MockSwarmPoller( - initialSetup: { cache in - cache.when { $0.startIfNeeded() }.thenReturn(()) - cache.when { $0.receivedPollResponse }.thenReturn(Just([]).eraseToAnyPublisher()) - } - ) - @TestState(cache: .groupPollers, in: dependencies) var mockGroupPollersCache: MockGroupPollerCache! = MockGroupPollerCache( - initialSetup: { cache in - cache.when { $0.startAllPollers() }.thenReturn(()) - cache.when { $0.getOrCreatePoller(for: .any) }.thenReturn(mockSwarmPoller) - cache.when { $0.stopAndRemovePoller(for: .any) }.thenReturn(()) - cache.when { $0.stopAndRemoveAllPollers() }.thenReturn(()) - } - ) - @TestState(singleton: .notificationsManager, in: dependencies) var mockNotificationsManager: MockNotificationsManager! = MockNotificationsManager( - initialSetup: { $0.defaultInitialSetup() } - ) - @TestState(singleton: .appContext, in: dependencies) var mockAppContext: MockAppContext! = MockAppContext( - initialSetup: { appContext in - appContext.when { $0.isMainApp }.thenReturn(false) - } - ) - @TestState(singleton: .extensionHelper, in: dependencies) var mockExtensionHelper: MockExtensionHelper! = MockExtensionHelper( - initialSetup: { extensionHelper in - extensionHelper - .when { try $0.removeDedupeRecord(threadId: .any, uniqueIdentifier: .any) } - .thenReturn(()) - extensionHelper - .when { try $0.upsertLastClearedRecord(threadId: .any) } - .thenReturn(()) - } - ) - - // MARK: -- Messages - @TestState var inviteMessage: GroupUpdateInviteMessage! = { - let result: GroupUpdateInviteMessage = GroupUpdateInviteMessage( - inviteeSessionIdHexString: "TestId", - groupSessionId: groupId, - groupName: "TestGroup", - memberAuthData: Data([1, 2, 3]), - profile: nil, - adminSignature: .standard(signature: "TestSignature".bytes) - ) - result.sender = "051111111111111111111111111111111111111111111111111111111111111111" - result.sentTimestampMs = 1234567890000 - - return result - }() - @TestState var promoteMessage: GroupUpdatePromoteMessage! = { - let result: GroupUpdatePromoteMessage = GroupUpdatePromoteMessage( - groupIdentitySeed: groupSeed, - groupName: "TestGroup", - sentTimestampMs: 1234567890000 - ) - result.sender = "051111111111111111111111111111111111111111111111111111111111111111" - - return result - }() - @TestState var infoChangedMessage: GroupUpdateInfoChangeMessage! = { - let result: GroupUpdateInfoChangeMessage = GroupUpdateInfoChangeMessage( - changeType: .name, - updatedName: "TestGroup Rename", - updatedExpiration: nil, - adminSignature: .standard(signature: "TestSignature".bytes) - ) - result.sender = "051111111111111111111111111111111111111111111111111111111111111111" - result.sentTimestampMs = 1234567800000 - - return result - }() - @TestState var memberChangedMessage: GroupUpdateMemberChangeMessage! = { - let result: GroupUpdateMemberChangeMessage = GroupUpdateMemberChangeMessage( - changeType: .added, - memberSessionIds: ["051111111111111111111111111111111111111111111111111111111111111112"], - historyShared: false, - adminSignature: .standard(signature: "TestSignature".bytes) - ) - result.sender = "051111111111111111111111111111111111111111111111111111111111111111" - result.sentTimestampMs = 1234567800000 - - return result - }() - @TestState var memberLeftMessage: GroupUpdateMemberLeftMessage! = { - let result: GroupUpdateMemberLeftMessage = GroupUpdateMemberLeftMessage() - result.sender = "051111111111111111111111111111111111111111111111111111111111111112" - result.sentTimestampMs = 1234567800000 - - return result - }() - @TestState var memberLeftNotificationMessage: GroupUpdateMemberLeftNotificationMessage! = { - let result: GroupUpdateMemberLeftNotificationMessage = GroupUpdateMemberLeftNotificationMessage() - result.sender = "051111111111111111111111111111111111111111111111111111111111111112" - result.sentTimestampMs = 1234567800000 - - return result - }() - @TestState var inviteResponseMessage: GroupUpdateInviteResponseMessage! = { - let result: GroupUpdateInviteResponseMessage = GroupUpdateInviteResponseMessage( - isApproved: true, - profile: VisibleMessage.VMProfile(displayName: "TestOtherMember"), - sentTimestampMs: 1234567800000 - ) - result.sender = "051111111111111111111111111111111111111111111111111111111111111112" - - return result - }() - @TestState var deleteMessage: Data! = try! LibSessionMessage.groupKicked( - memberId: "05\(TestConstants.publicKey)", - groupKeysGen: 1 - ).1 - @TestState var deleteContentMessage: GroupUpdateDeleteMemberContentMessage! = { - let result: GroupUpdateDeleteMemberContentMessage = GroupUpdateDeleteMemberContentMessage( - memberSessionIds: ["051111111111111111111111111111111111111111111111111111111111111112"], - messageHashes: [], - adminSignature: .standard(signature: "TestSignature".bytes) - ) - result.sender = "051111111111111111111111111111111111111111111111111111111111111112" - result.sentTimestampMs = 1234567800000 - - return result - }() - @TestState var visibleMessageProto: SNProtoContent! = { - let proto = SNProtoContent.builder() - proto.setSigTimestamp((1234568890 - (60 * 10)) * 1000) - - let dataMessage = SNProtoDataMessage.builder() - dataMessage.setBody("Test") - proto.setDataMessage(try! dataMessage.build()) - return try? proto.build() - }() - @TestState var visibleMessage: VisibleMessage! = { - let result = VisibleMessage( - sender: "051111111111111111111111111111111111111111111111111111111111111112", - sentTimestampMs: ((1234568890 - (60 * 10)) * 1000), - text: "Test" - ) - result.receivedTimestampMs = (1234568890 * 1000) - return result - }() // MARK: - a MessageReceiver dealing with Groups describe("a MessageReceiver dealing with Groups") { // MARK: -- when receiving a group invitation context("when receiving a group invitation") { // MARK: ---- throws if the admin signature fails to verify - it("throws if the admin signature fails to verify") { - mockCrypto + itTracked("throws if the admin signature fails to verify") { + fixture.mockCrypto .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(false) - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: inviteMessage, + message: fixture.inviteMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let threads: [SessionThread]? = mockStorage.read { db in try SessionThread.fetchAll(db) } + let threads: [SessionThread]? = fixture.mockStorage.read { db in try SessionThread.fetchAll(db) } expect(threads).to(beEmpty()) } // MARK: ---- with profile information context("with profile information") { // MARK: ------ updates the profile name - it("updates the profile name") { - inviteMessage.profile = VisibleMessage.VMProfile(displayName: "TestName") + itTracked("updates the profile name") { + fixture.inviteMessage.profile = VisibleMessage.VMProfile(displayName: "TestName") - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: inviteMessage, + message: fixture.inviteMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let profiles: [Profile]? = mockStorage.read { db in try Profile.fetchAll(db) } + let profiles: [Profile]? = fixture.mockStorage.read { db in try Profile.fetchAll(db) } expect(profiles?.map { $0.name }.sorted()).to(equal(["TestCurrentUser", "TestName"])) } // MARK: ------ with a profile picture context("with a profile picture") { // MARK: ------ schedules and starts a displayPictureDownload job if running the main app - it("schedules and starts a displayPictureDownload job if running the main app") { - mockAppContext.when { $0.isMainApp }.thenReturn(true) + itTracked("schedules and starts a displayPictureDownload job if running the main app") { + await fixture.mockAppContext.when { $0.isMainApp }.thenReturn(true) - inviteMessage.profile = VisibleMessage.VMProfile( + fixture.inviteMessage.profile = VisibleMessage.VMProfile( displayName: "TestName", profileKey: Data((0.. = mockStorage.write { db in + fixture.mockStorage.write { db in _ = try SessionThread.upsert( db, - id: groupId.hexString, + id: fixture.groupId.hexString, variant: .group, values: SessionThread.TargetValues( creationDateTimestamp: .setTo(0), shouldBeVisible: .useExisting ), - using: dependencies + using: fixture.dependencies ) try ClosedGroup( - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, name: "Test", formationTimestamp: 0, shouldPoll: nil, - groupIdentityPrivateKey: groupSecretKey, + groupIdentityPrivateKey: fixture.groupSecretKey, invited: nil ).upsert(db) - let result = try PushNotificationAPI.preparedSubscribe( - db, - token: Data([5, 4, 3, 2, 1]), - sessionIds: [groupId], - using: dependencies - ) // Remove the debug group so it can be created during the actual test - try ClosedGroup.filter(id: groupId.hexString).deleteAll(db) - try SessionThread.filter(id: groupId.hexString).deleteAll(db) - - return result + try ClosedGroup.filter(id: fixture.groupId.hexString).deleteAll(db) + try SessionThread.filter(id: fixture.groupId.hexString).deleteAll(db) }! - mockUserDefaults + fixture.mockUserDefaults .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } .thenReturn(false) - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: inviteMessage, + message: fixture.inviteMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - expect(mockNetwork) - .toNot(call { network in - network.send( - expectedRequest.body, - to: expectedRequest.destination, - requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout - ) - }) + expect(fixture.mockNetwork).toNot(call { network in + network.send( + endpoint: PushNotificationAPI.Endpoint.subscribe, + destination: .server( + method: .post, + server: PushNotificationAPI.server, + queryParameters: [:], + headers: [:], + x25519PublicKey: PushNotificationAPI.serverPublicKey + ), + body: try! JSONEncoder(using: fixture.dependencies).encode( + PushNotificationAPI.SubscribeRequest( + subscriptions: [ + PushNotificationAPI.SubscribeRequest.Subscription( + namespaces: [ + .groupMessages, + .configGroupKeys, + .configGroupInfo, + .configGroupMembers, + .revokedRetrievableGroupMessages + ], + includeMessageData: true, + serviceInfo: PushNotificationAPI.ServiceInfo( + token: Data([5, 4, 3, 2, 1]).toHexString() + ), + notificationsEncryptionKey: Data([1, 2, 3]), + authMethod: try! Authentication.with( + swarmPublicKey: fixture.groupId.hexString, + using: fixture.dependencies + ), + timestamp: 1234567890 + ) + ] + ) + ), + category: .standard, + requestTimeout: Network.defaultTimeout, + overallTimeout: nil + ) + }) } } // MARK: ------ and push notifications are enabled context("and push notifications are enabled") { beforeEach { - mockUserDefaults + fixture.mockUserDefaults .when { $0.string(forKey: UserDefaults.StringKey.deviceToken.rawValue) } .thenReturn(Data([5, 4, 3, 2, 1]).toHexString()) - mockUserDefaults + fixture.mockUserDefaults .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } .thenReturn(true) } // MARK: -------- subscribes for push notifications - it("subscribes for push notifications") { - let expectedRequest: Network.PreparedRequest = mockStorage.write { db in + itTracked("subscribes for push notifications") { + fixture.mockStorage.write { db in _ = try SessionThread.upsert( db, - id: groupId.hexString, + id: fixture.groupId.hexString, variant: .group, values: SessionThread.TargetValues( creationDateTimestamp: .setTo(0), shouldBeVisible: .useExisting ), - using: dependencies + using: fixture.dependencies ) try ClosedGroup( - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, name: "Test", formationTimestamp: 0, shouldPoll: nil, - authData: inviteMessage.memberAuthData, + authData: fixture.inviteMessage.memberAuthData, invited: nil ).upsert(db) - let result = try PushNotificationAPI.preparedSubscribe( - db, - token: Data(hex: Data([5, 4, 3, 2, 1]).toHexString()), - sessionIds: [groupId], - using: dependencies - ) // Remove the debug group so it can be created during the actual test - try ClosedGroup.filter(id: groupId.hexString).deleteAll(db) - try SessionThread.filter(id: groupId.hexString).deleteAll(db) - - return result + try ClosedGroup.filter(id: fixture.groupId.hexString).deleteAll(db) + try SessionThread.filter(id: fixture.groupId.hexString).deleteAll(db) }! - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: inviteMessage, + message: fixture.inviteMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - expect(mockNetwork) + expect(fixture.mockNetwork) .to(call(.exactly(times: 1), matchingParameters: .all) { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, - requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + endpoint: PushNotificationAPI.Endpoint.subscribe, + destination: .server( + method: .post, + server: PushNotificationAPI.server, + queryParameters: [:], + headers: [:], + x25519PublicKey: PushNotificationAPI.serverPublicKey + ), + body: try! JSONEncoder(using: fixture.dependencies).encode( + PushNotificationAPI.SubscribeRequest( + subscriptions: [ + PushNotificationAPI.SubscribeRequest.Subscription( + namespaces: [ + .groupMessages, + .configGroupKeys, + .configGroupInfo, + .configGroupMembers, + .revokedRetrievableGroupMessages + ], + includeMessageData: true, + serviceInfo: PushNotificationAPI.ServiceInfo( + token: Data([5, 4, 3, 2, 1]).toHexString() + ), + notificationsEncryptionKey: Data([1, 2, 3]), + authMethod: try! Authentication.with( + swarmPublicKey: fixture.groupId.hexString, + using: fixture.dependencies + ), + timestamp: 1234567890 + ) + ] + ) + ), + category: .standard, + requestTimeout: Network.defaultTimeout, + overallTimeout: nil ) }) } @@ -969,53 +691,53 @@ class MessageReceiverGroupsSpec: QuickSpec { } // MARK: ---- adds the invited control message if the thread does not exist - it("adds the invited control message if the thread does not exist") { - mockStorage.write { db in + itTracked("adds the invited control message if the thread does not exist") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: inviteMessage, + message: fixture.inviteMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) } + let interactions: [Interaction]? = fixture.mockStorage.read { db in try Interaction.fetchAll(db) } expect(interactions?.count).to(equal(1)) expect(interactions?.first?.body) .to(equal("{\"invited\":{\"_0\":\"0511...1111\",\"_1\":\"TestGroup\"}}")) } // MARK: ---- does not add the invited control message if the thread already exists - it("does not add the invited control message if the thread already exists") { - mockStorage.write { db in + itTracked("does not add the invited control message if the thread already exists") { + fixture.mockStorage.write { db in try SessionThread.upsert( db, - id: groupId.hexString, + id: fixture.groupId.hexString, variant: .group, values: SessionThread.TargetValues( creationDateTimestamp: .setTo(1234567890), shouldBeVisible: .setTo(true) ), - using: dependencies + using: fixture.dependencies ) } - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: inviteMessage, + message: fixture.inviteMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) } + let interactions: [Interaction]? = fixture.mockStorage.read { db in try Interaction.fetchAll(db) } expect(interactions?.count).to(equal(0)) } } @@ -1027,11 +749,11 @@ class MessageReceiverGroupsSpec: QuickSpec { beforeEach { var cMemberId: [CChar] = "05\(TestConstants.publicKey)".cString(using: .utf8)! var member: config_group_member = config_group_member() - _ = groups_members_get_or_construct(groupMembersConf, &member, &cMemberId) + _ = groups_members_get_or_construct(fixture.groupMembersConfig.conf, &member, &cMemberId) member.set(\.name, to: "TestName") - groups_members_set(groupMembersConf, &member) + groups_members_set(fixture.groupMembersConfig.conf, &member) - mockStorage.write { db in + fixture.mockStorage.write { db in try Contact( id: "051111111111111111111111111111111111111111111111111111111111111111", isTrusted: true, @@ -1044,17 +766,17 @@ class MessageReceiverGroupsSpec: QuickSpec { ).insert(db) try SessionThread.upsert( db, - id: groupId.hexString, + id: fixture.groupId.hexString, variant: .group, values: SessionThread.TargetValues( creationDateTimestamp: .setTo(1234567890), shouldBeVisible: .setTo(true) ), - using: dependencies + using: fixture.dependencies ) try ClosedGroup( - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, name: "TestGroup", formationTimestamp: 1234567890, shouldPoll: true, @@ -1066,19 +788,19 @@ class MessageReceiverGroupsSpec: QuickSpec { } // MARK: ---- fails if it cannot convert the group seed to a groupIdentityKeyPair - it("fails if it cannot convert the group seed to a groupIdentityKeyPair") { - mockCrypto.when { $0.generate(.ed25519KeyPair(seed: .any)) }.thenReturn(nil) + itTracked("fails if it cannot convert the group seed to a groupIdentityKeyPair") { + fixture.mockCrypto.when { $0.generate(.ed25519KeyPair(seed: .any)) }.thenReturn(nil) - mockStorage.write { db in + fixture.mockStorage.write { db in result = Result(catching: { try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: promoteMessage, + message: fixture.promoteMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) }) } @@ -1087,36 +809,36 @@ class MessageReceiverGroupsSpec: QuickSpec { } // MARK: ---- updates the GROUP_KEYS state correctly - it("updates the GROUP_KEYS state correctly") { - mockCrypto + itTracked("updates the GROUP_KEYS state correctly") { + fixture.mockCrypto .when { $0.generate(.ed25519KeyPair(seed: .any)) } .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [4, 5, 6])) - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: promoteMessage, + message: fixture.promoteMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - expect(mockLibSessionCache).to(call(.exactly(times: 1), matchingParameters: .all) { + expect(fixture.mockLibSessionCache).to(call(.exactly(times: 1), matchingParameters: .all) { try $0.loadAdminKey( - groupIdentitySeed: groupSeed, + groupIdentitySeed: fixture.groupSeed, groupSessionId: SessionId(.group, publicKey: [1, 2, 3]) ) }) } // MARK: ---- replaces the memberAuthData with the admin key in the database - it("replaces the memberAuthData with the admin key in the database") { - mockStorage.write { db in + itTracked("replaces the memberAuthData with the admin key in the database") { + fixture.mockStorage.write { db in try ClosedGroup( - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, name: "TestGroup", formationTimestamp: 1234567890, shouldPoll: true, @@ -1126,21 +848,21 @@ class MessageReceiverGroupsSpec: QuickSpec { ).upsert(db) } - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: promoteMessage, + message: fixture.promoteMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let groups: [ClosedGroup]? = mockStorage.read { db in try ClosedGroup.fetchAll(db) } + let groups: [ClosedGroup]? = fixture.mockStorage.read { db in try ClosedGroup.fetchAll(db) } expect(groups?.count).to(equal(1)) - expect(groups?.first?.groupIdentityPrivateKey).to(equal(Data(groupKeyPair.secretKey))) + expect(groups?.first?.groupIdentityPrivateKey).to(equal(Data(fixture.groupKeyPair.secretKey))) expect(groups?.first?.authData).to(beNil()) } } @@ -1148,74 +870,74 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: -- when receiving an info changed message context("when receiving an info changed message") { beforeEach { - mockStorage.write { db in + fixture.mockStorage.write { db in try SessionThread.upsert( db, - id: groupId.hexString, + id: fixture.groupId.hexString, variant: .group, values: SessionThread.TargetValues( creationDateTimestamp: .setTo(1234567890), shouldBeVisible: .setTo(true) ), - using: dependencies + using: fixture.dependencies ) } } // MARK: ---- throws if there is no sender - it("throws if there is no sender") { - infoChangedMessage.sender = nil + itTracked("throws if there is no sender") { + fixture.infoChangedMessage.sender = nil - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: infoChangedMessage, + message: fixture.infoChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) }.to(throwError(MessageReceiverError.invalidMessage)) } } // MARK: ---- throws if there is no timestamp - it("throws if there is no timestamp") { - infoChangedMessage.sentTimestampMs = nil + itTracked("throws if there is no timestamp") { + fixture.infoChangedMessage.sentTimestampMs = nil - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: infoChangedMessage, + message: fixture.infoChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) }.to(throwError(MessageReceiverError.invalidMessage)) } } // MARK: ---- throws if the admin signature fails to verify - it("throws if the admin signature fails to verify") { - mockCrypto + itTracked("throws if the admin signature fails to verify") { + fixture.mockCrypto .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(false) - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: infoChangedMessage, + message: fixture.infoChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) }.to(throwError(MessageReceiverError.invalidMessage)) } @@ -1224,25 +946,25 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: ---- for a name change context("for a name change") { // MARK: ------ creates the correct control message - it("creates the correct control message") { - mockStorage.write { db in + itTracked("creates the correct control message") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: infoChangedMessage, + message: fixture.infoChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( ClosedGroup.MessageInfo .updatedName("TestGroup Rename") - .infoString(using: dependencies) + .infoString(using: fixture.dependencies) )) } } @@ -1250,36 +972,36 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: ---- for a display picture change context("for a display picture change") { beforeEach { - infoChangedMessage = GroupUpdateInfoChangeMessage( + fixture.infoChangedMessage = GroupUpdateInfoChangeMessage( changeType: .avatar, updatedName: nil, updatedExpiration: nil, adminSignature: .standard(signature: "TestSignature".bytes) ) - infoChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" - infoChangedMessage.sentTimestampMs = 1234567800000 + fixture.infoChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" + fixture.infoChangedMessage.sentTimestampMs = 1234567800000 } // MARK: ------ creates the correct control message - it("creates the correct control message") { - mockStorage.write { db in + itTracked("creates the correct control message") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: infoChangedMessage, + message: fixture.infoChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( ClosedGroup.MessageInfo .updatedDisplayPicture - .infoString(using: dependencies) + .infoString(using: fixture.dependencies) )) } } @@ -1287,42 +1009,42 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: ---- for a disappearing message setting change context("for a disappearing message setting change") { beforeEach { - infoChangedMessage = GroupUpdateInfoChangeMessage( + fixture.infoChangedMessage = GroupUpdateInfoChangeMessage( changeType: .disappearingMessages, updatedName: nil, updatedExpiration: 3600, adminSignature: .standard(signature: "TestSignature".bytes) ) - infoChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" - infoChangedMessage.sentTimestampMs = 1234567800000 + fixture.infoChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" + fixture.infoChangedMessage.sentTimestampMs = 1234567800000 } // MARK: ------ creates the correct control message - it("creates the correct control message") { - mockStorage.write { db in + itTracked("creates the correct control message") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: infoChangedMessage, + message: fixture.infoChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( DisappearingMessagesConfiguration( - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, isEnabled: true, durationSeconds: 3600, type: .disappearAfterSend ).messageInfoString( threadVariant: .group, - senderName: infoChangedMessage.sender, - using: dependencies + senderName: fixture.infoChangedMessage.sender, + using: fixture.dependencies ) )) expect(interaction?.expiresInSeconds).to(beNil()) @@ -1333,114 +1055,114 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: -- when receiving a member changed message context("when receiving a member changed message") { beforeEach { - mockStorage.write { db in + fixture.mockStorage.write { db in try SessionThread.upsert( db, - id: groupId.hexString, + id: fixture.groupId.hexString, variant: .group, values: SessionThread.TargetValues( creationDateTimestamp: .setTo(1234567890), shouldBeVisible: .setTo(true) ), - using: dependencies + using: fixture.dependencies ) } } // MARK: ---- throws if there is no sender - it("throws if there is no sender") { - memberChangedMessage.sender = nil + itTracked("throws if there is no sender") { + fixture.memberChangedMessage.sender = nil - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberChangedMessage, + message: fixture.memberChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) }.to(throwError(MessageReceiverError.invalidMessage)) } } // MARK: ---- throws if there is no timestamp - it("throws if there is no timestamp") { - memberChangedMessage.sentTimestampMs = nil + itTracked("throws if there is no timestamp") { + fixture.memberChangedMessage.sentTimestampMs = nil - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberChangedMessage, + message: fixture.memberChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) }.to(throwError(MessageReceiverError.invalidMessage)) } } // MARK: ---- throws if the admin signature fails to verify - it("throws if the admin signature fails to verify") { - mockCrypto + itTracked("throws if the admin signature fails to verify") { + fixture.mockCrypto .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(false) - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberChangedMessage, + message: fixture.memberChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) }.to(throwError(MessageReceiverError.invalidMessage)) } } // MARK: ---- correctly retrieves the member name if present - it("correctly retrieves the member name if present") { - mockStorage.write { db in + itTracked("correctly retrieves the member name if present") { + fixture.mockStorage.write { db in try Profile( id: "051111111111111111111111111111111111111111111111111111111111111112", name: "TestOtherProfile" ).insert(db) } - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberChangedMessage, + message: fixture.memberChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( ClosedGroup.MessageInfo .addedUsers(hasCurrentUser: false, names: ["TestOtherProfile"], historyShared: false) - .infoString(using: dependencies) + .infoString(using: fixture.dependencies) )) } // MARK: ---- for adding members context("for adding members") { // MARK: ------ creates the correct control message for a single member - it("creates the correct control message for a single member") { - memberChangedMessage = GroupUpdateMemberChangeMessage( + itTracked("creates the correct control message for a single member") { + fixture.memberChangedMessage = GroupUpdateMemberChangeMessage( changeType: .added, memberSessionIds: [ "051111111111111111111111111111111111111111111111111111111111111112" @@ -1448,22 +1170,22 @@ class MessageReceiverGroupsSpec: QuickSpec { historyShared: false, adminSignature: .standard(signature: "TestSignature".bytes) ) - memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" - memberChangedMessage.sentTimestampMs = 1234567800000 + fixture.memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" + fixture.memberChangedMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberChangedMessage, + message: fixture.memberChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( ClosedGroup.MessageInfo @@ -1472,13 +1194,13 @@ class MessageReceiverGroupsSpec: QuickSpec { names: ["0511...1112"], historyShared: false ) - .infoString(using: dependencies) + .infoString(using: fixture.dependencies) )) } // MARK: ------ creates the correct control message for two members - it("creates the correct control message for two members") { - memberChangedMessage = GroupUpdateMemberChangeMessage( + itTracked("creates the correct control message for two members") { + fixture.memberChangedMessage = GroupUpdateMemberChangeMessage( changeType: .added, memberSessionIds: [ "051111111111111111111111111111111111111111111111111111111111111112", @@ -1487,22 +1209,22 @@ class MessageReceiverGroupsSpec: QuickSpec { historyShared: false, adminSignature: .standard(signature: "TestSignature".bytes) ) - memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" - memberChangedMessage.sentTimestampMs = 1234567800000 + fixture.memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" + fixture.memberChangedMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberChangedMessage, + message: fixture.memberChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( ClosedGroup.MessageInfo @@ -1511,13 +1233,13 @@ class MessageReceiverGroupsSpec: QuickSpec { names: ["0511...1112", "0511...1113"], historyShared: false ) - .infoString(using: dependencies) + .infoString(using: fixture.dependencies) )) } // MARK: ------ creates the correct control message for many members - it("creates the correct control message for many members") { - memberChangedMessage = GroupUpdateMemberChangeMessage( + itTracked("creates the correct control message for many members") { + fixture.memberChangedMessage = GroupUpdateMemberChangeMessage( changeType: .added, memberSessionIds: [ "051111111111111111111111111111111111111111111111111111111111111112", @@ -1527,22 +1249,22 @@ class MessageReceiverGroupsSpec: QuickSpec { historyShared: false, adminSignature: .standard(signature: "TestSignature".bytes) ) - memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" - memberChangedMessage.sentTimestampMs = 1234567800000 + fixture.memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" + fixture.memberChangedMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberChangedMessage, + message: fixture.memberChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( ClosedGroup.MessageInfo @@ -1551,7 +1273,7 @@ class MessageReceiverGroupsSpec: QuickSpec { names: ["0511...1112", "0511...1113", "0511...1114"], historyShared: false ) - .infoString(using: dependencies) + .infoString(using: fixture.dependencies) )) } } @@ -1559,8 +1281,8 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: ---- for removing members context("for removing members") { // MARK: ------ creates the correct control message for a single member - it("creates the correct control message for a single member") { - memberChangedMessage = GroupUpdateMemberChangeMessage( + itTracked("creates the correct control message for a single member") { + fixture.memberChangedMessage = GroupUpdateMemberChangeMessage( changeType: .removed, memberSessionIds: [ "051111111111111111111111111111111111111111111111111111111111111112" @@ -1568,33 +1290,33 @@ class MessageReceiverGroupsSpec: QuickSpec { historyShared: false, adminSignature: .standard(signature: "TestSignature".bytes) ) - memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" - memberChangedMessage.sentTimestampMs = 1234567800000 + fixture.memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" + fixture.memberChangedMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberChangedMessage, + message: fixture.memberChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( ClosedGroup.MessageInfo .removedUsers(hasCurrentUser: false, names: ["0511...1112"]) - .infoString(using: dependencies) + .infoString(using: fixture.dependencies) )) } // MARK: ------ creates the correct control message for two members - it("creates the correct control message for two members") { - memberChangedMessage = GroupUpdateMemberChangeMessage( + itTracked("creates the correct control message for two members") { + fixture.memberChangedMessage = GroupUpdateMemberChangeMessage( changeType: .removed, memberSessionIds: [ "051111111111111111111111111111111111111111111111111111111111111112", @@ -1603,33 +1325,33 @@ class MessageReceiverGroupsSpec: QuickSpec { historyShared: false, adminSignature: .standard(signature: "TestSignature".bytes) ) - memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" - memberChangedMessage.sentTimestampMs = 1234567800000 + fixture.memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" + fixture.memberChangedMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberChangedMessage, + message: fixture.memberChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( ClosedGroup.MessageInfo .removedUsers(hasCurrentUser: false, names: ["0511...1112", "0511...1113"]) - .infoString(using: dependencies) + .infoString(using: fixture.dependencies) )) } // MARK: ------ creates the correct control message for many members - it("creates the correct control message for many members") { - memberChangedMessage = GroupUpdateMemberChangeMessage( + itTracked("creates the correct control message for many members") { + fixture.memberChangedMessage = GroupUpdateMemberChangeMessage( changeType: .removed, memberSessionIds: [ "051111111111111111111111111111111111111111111111111111111111111112", @@ -1639,27 +1361,27 @@ class MessageReceiverGroupsSpec: QuickSpec { historyShared: false, adminSignature: .standard(signature: "TestSignature".bytes) ) - memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" - memberChangedMessage.sentTimestampMs = 1234567800000 + fixture.memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" + fixture.memberChangedMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberChangedMessage, + message: fixture.memberChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( ClosedGroup.MessageInfo .removedUsers(hasCurrentUser: false, names: ["0511...1112", "0511...1113", "0511...1114"]) - .infoString(using: dependencies) + .infoString(using: fixture.dependencies) )) } } @@ -1667,8 +1389,8 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: ---- for promoting members context("for promoting members") { // MARK: ------ creates the correct control message for a single member - it("creates the correct control message for a single member") { - memberChangedMessage = GroupUpdateMemberChangeMessage( + itTracked("creates the correct control message for a single member") { + fixture.memberChangedMessage = GroupUpdateMemberChangeMessage( changeType: .promoted, memberSessionIds: [ "051111111111111111111111111111111111111111111111111111111111111112" @@ -1676,33 +1398,33 @@ class MessageReceiverGroupsSpec: QuickSpec { historyShared: false, adminSignature: .standard(signature: "TestSignature".bytes) ) - memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" - memberChangedMessage.sentTimestampMs = 1234567800000 + fixture.memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" + fixture.memberChangedMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberChangedMessage, + message: fixture.memberChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( ClosedGroup.MessageInfo .promotedUsers(hasCurrentUser: false, names: ["0511...1112"]) - .infoString(using: dependencies) + .infoString(using: fixture.dependencies) )) } // MARK: ------ creates the correct control message for two members - it("creates the correct control message for two members") { - memberChangedMessage = GroupUpdateMemberChangeMessage( + itTracked("creates the correct control message for two members") { + fixture.memberChangedMessage = GroupUpdateMemberChangeMessage( changeType: .promoted, memberSessionIds: [ "051111111111111111111111111111111111111111111111111111111111111112", @@ -1711,33 +1433,33 @@ class MessageReceiverGroupsSpec: QuickSpec { historyShared: false, adminSignature: .standard(signature: "TestSignature".bytes) ) - memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" - memberChangedMessage.sentTimestampMs = 1234567800000 + fixture.memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" + fixture.memberChangedMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberChangedMessage, + message: fixture.memberChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( ClosedGroup.MessageInfo .promotedUsers(hasCurrentUser: false, names: ["0511...1112", "0511...1113"]) - .infoString(using: dependencies) + .infoString(using: fixture.dependencies) )) } // MARK: ------ creates the correct control message for many members - it("creates the correct control message for many members") { - memberChangedMessage = GroupUpdateMemberChangeMessage( + itTracked("creates the correct control message for many members") { + fixture.memberChangedMessage = GroupUpdateMemberChangeMessage( changeType: .promoted, memberSessionIds: [ "051111111111111111111111111111111111111111111111111111111111111112", @@ -1747,27 +1469,27 @@ class MessageReceiverGroupsSpec: QuickSpec { historyShared: false, adminSignature: .standard(signature: "TestSignature".bytes) ) - memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" - memberChangedMessage.sentTimestampMs = 1234567800000 + fixture.memberChangedMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" + fixture.memberChangedMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberChangedMessage, + message: fixture.memberChangedMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( ClosedGroup.MessageInfo .promotedUsers(hasCurrentUser: false, names: ["0511...1112", "0511...1113", "0511...1114"]) - .infoString(using: dependencies) + .infoString(using: fixture.dependencies) )) } } @@ -1776,71 +1498,71 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: -- when receiving a member left message context("when receiving a member left message") { beforeEach { - mockStorage.write { db in + fixture.mockStorage.write { db in try SessionThread.upsert( db, - id: groupId.hexString, + id: fixture.groupId.hexString, variant: .group, values: SessionThread.TargetValues( creationDateTimestamp: .setTo(1234567890), shouldBeVisible: .setTo(true) ), - using: dependencies + using: fixture.dependencies ) } } // MARK: ---- does not create a control message - it("does not create a control message") { - mockStorage.write { db in + itTracked("does not create a control message") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberLeftMessage, + message: fixture.memberLeftMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) } + let interactions: [Interaction]? = fixture.mockStorage.read { db in try Interaction.fetchAll(db) } expect(interactions).to(beEmpty()) } // MARK: ---- throws if there is no sender - it("throws if there is no sender") { - memberLeftMessage.sender = nil + itTracked("throws if there is no sender") { + fixture.memberLeftMessage.sender = nil - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberLeftMessage, + message: fixture.memberLeftMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) }.to(throwError(MessageReceiverError.invalidMessage)) } } // MARK: ---- throws if there is no timestamp - it("throws if there is no timestamp") { - memberLeftMessage.sentTimestampMs = nil + itTracked("throws if there is no timestamp") { + fixture.memberLeftMessage.sentTimestampMs = nil - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberLeftMessage, + message: fixture.memberLeftMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) }.to(throwError(MessageReceiverError.invalidMessage)) } @@ -1852,23 +1574,23 @@ class MessageReceiverGroupsSpec: QuickSpec { // Only update members if they already exist in the group var cMemberId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111112".cString(using: .utf8)! var groupMember: config_group_member = config_group_member() - _ = groups_members_get_or_construct(groupMembersConf, &groupMember, &cMemberId) + _ = groups_members_get_or_construct(fixture.groupMembersConfig.conf, &groupMember, &cMemberId) groupMember.set(\.name, to: "TestOtherName") - groups_members_set(groupMembersConf, &groupMember) + groups_members_set(fixture.groupMembersConfig.conf, &groupMember) - mockStorage.write { db in + fixture.mockStorage.write { db in try ClosedGroup( - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, name: "TestGroup", formationTimestamp: 1234567890, shouldPoll: true, - groupIdentityPrivateKey: groupSecretKey, + groupIdentityPrivateKey: fixture.groupSecretKey, authData: nil, invited: false ).upsert(db) try GroupMember( - groupId: groupId.hexString, + groupId: fixture.groupId.hexString, profileId: "051111111111111111111111111111111111111111111111111111111111111112", role: .standard, roleStatus: .accepted, @@ -1878,65 +1600,65 @@ class MessageReceiverGroupsSpec: QuickSpec { } // MARK: ------ flags the member for removal keeping their messages - it("flags the member for removal keeping their messages") { - mockStorage.write { db in + itTracked("flags the member for removal keeping their messages") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberLeftMessage, + message: fixture.memberLeftMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } var cMemberId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111112".cString(using: .utf8)! var groupMember: config_group_member = config_group_member() - expect(groups_members_get(groupMembersConf, &groupMember, &cMemberId)).to(beTrue()) + expect(groups_members_get(fixture.groupMembersConfig.conf, &groupMember, &cMemberId)).to(beTrue()) expect(groupMember.removed).to(equal(1)) } // MARK: ------ flags the GroupMember as pending removal - it("flags the GroupMember as pending removal") { - mockStorage.write { db in + itTracked("flags the GroupMember as pending removal") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberLeftMessage, + message: fixture.memberLeftMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let members: [GroupMember]? = mockStorage.read { db in try GroupMember.fetchAll(db) } + let members: [GroupMember]? = fixture.mockStorage.read { db in try GroupMember.fetchAll(db) } expect(members?.count).to(equal(1)) expect(members?.first?.roleStatus).to(equal(.pendingRemoval)) } // MARK: ------ schedules a job to process the pending removal - it("schedules a job to process the pending removal") { - mockStorage.write { db in + itTracked("schedules a job to process the pending removal") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberLeftMessage, + message: fixture.memberLeftMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - expect(mockJobRunner) + expect(fixture.mockJobRunner) .to(call(matchingParameters: .all) { $0.add( .any, job: Job( variant: .processPendingGroupMemberRemovals, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, details: ProcessPendingGroupMemberRemovalsJob.Details( changeTimestampMs: 1234567800000 ) @@ -1947,29 +1669,29 @@ class MessageReceiverGroupsSpec: QuickSpec { } // MARK: ------ does not schedule a member change control message to be sent - it("does not schedule a member change control message to be sent") { - mockStorage.write { db in + itTracked("does not schedule a member change control message to be sent") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberLeftMessage, + message: fixture.memberLeftMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - expect(mockJobRunner) + expect(fixture.mockJobRunner) .toNot(call(.exactly(times: 1), matchingParameters: .all) { $0.add( .any, job: Job( variant: .messageSend, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, interactionId: nil, details: MessageSendJob.Details( - destination: .closedGroup(groupPublicKey: groupId.hexString), + destination: .closedGroup(groupPublicKey: fixture.groupId.hexString), message: try! GroupUpdateMemberChangeMessage( changeType: .removed, memberSessionIds: [ @@ -1978,10 +1700,10 @@ class MessageReceiverGroupsSpec: QuickSpec { historyShared: false, sentTimestampMs: 1234567800000, authMethod: Authentication.groupAdmin( - groupSessionId: groupId, - ed25519SecretKey: Array(groupSecretKey) + groupSessionId: fixture.groupId, + ed25519SecretKey: Array(fixture.groupSecretKey) ), - using: dependencies + using: fixture.dependencies ) ) ), @@ -1995,70 +1717,70 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: -- when receiving a member left notification message context("when receiving a member left notification message") { beforeEach { - mockStorage.write { db in + fixture.mockStorage.write { db in try SessionThread.upsert( db, - id: groupId.hexString, + id: fixture.groupId.hexString, variant: .group, values: SessionThread.TargetValues( creationDateTimestamp: .setTo(1234567890), shouldBeVisible: .setTo(true) ), - using: dependencies + using: fixture.dependencies ) } } // MARK: ---- creates the correct control message - it("creates the correct control message") { - mockStorage.write { db in + itTracked("creates the correct control message") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberLeftNotificationMessage, + message: fixture.memberLeftNotificationMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( ClosedGroup.MessageInfo .memberLeft(wasCurrentUser: false, name: "0511...1112") - .infoString(using: dependencies) + .infoString(using: fixture.dependencies) )) } // MARK: ---- correctly retrieves the member name if present - it("correctly retrieves the member name if present") { - mockStorage.write { db in + itTracked("correctly retrieves the member name if present") { + fixture.mockStorage.write { db in try Profile( id: "051111111111111111111111111111111111111111111111111111111111111112", name: "TestOtherProfile" ).insert(db) } - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: memberLeftNotificationMessage, + message: fixture.memberLeftNotificationMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interaction: Interaction? = mockStorage.read { db in try Interaction.fetchOne(db) } + let interaction: Interaction? = fixture.mockStorage.read { db in try Interaction.fetchOne(db) } expect(interaction?.timestampMs).to(equal(1234567800000)) expect(interaction?.body).to(equal( ClosedGroup.MessageInfo .memberLeft(wasCurrentUser: false, name: "TestOtherProfile") - .infoString(using: dependencies) + .infoString(using: fixture.dependencies) )) } } @@ -2066,73 +1788,73 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: -- when receiving an invite response message context("when receiving an invite response message") { beforeEach { - mockStorage.write { db in + fixture.mockStorage.write { db in try SessionThread.upsert( db, - id: groupId.hexString, + id: fixture.groupId.hexString, variant: .group, values: SessionThread.TargetValues( creationDateTimestamp: .setTo(1234567890), shouldBeVisible: .setTo(true) ), - using: dependencies + using: fixture.dependencies ) } } // MARK: ---- throws if there is no sender - it("throws if there is no sender") { - inviteResponseMessage.sender = nil + itTracked("throws if there is no sender") { + fixture.inviteResponseMessage.sender = nil - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: inviteResponseMessage, + message: fixture.inviteResponseMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) }.to(throwError(MessageReceiverError.invalidMessage)) } } // MARK: ---- throws if there is no timestamp - it("throws if there is no timestamp") { - inviteResponseMessage.sentTimestampMs = nil + itTracked("throws if there is no timestamp") { + fixture.inviteResponseMessage.sentTimestampMs = nil - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: inviteResponseMessage, + message: fixture.inviteResponseMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) }.to(throwError(MessageReceiverError.invalidMessage)) } } // MARK: ---- updates the profile information in the database if provided - it("updates the profile information in the database if provided") { - mockStorage.write { db in + itTracked("updates the profile information in the database if provided") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: inviteResponseMessage, + message: fixture.inviteResponseMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let profiles: [Profile]? = mockStorage.read { db in try Profile.fetchAll(db) } + let profiles: [Profile]? = fixture.mockStorage.read { db in try Profile.fetchAll(db) } expect(profiles?.map { $0.id }).to(equal([ "05\(TestConstants.publicKey)", "051111111111111111111111111111111111111111111111111111111111111112" @@ -2146,18 +1868,18 @@ class MessageReceiverGroupsSpec: QuickSpec { // Only update members if they already exist in the group var cMemberId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111112".cString(using: .utf8)! var groupMember: config_group_member = config_group_member() - _ = groups_members_get_or_construct(groupMembersConf, &groupMember, &cMemberId) + _ = groups_members_get_or_construct(fixture.groupMembersConfig.conf, &groupMember, &cMemberId) groupMember.set(\.name, to: "TestOtherMember") groupMember.invited = 1 - groups_members_set(groupMembersConf, &groupMember) + groups_members_set(fixture.groupMembersConfig.conf, &groupMember) - mockStorage.write { db in + fixture.mockStorage.write { db in try ClosedGroup( - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, name: "TestGroup", formationTimestamp: 1234567890, shouldPoll: true, - groupIdentityPrivateKey: groupSecretKey, + groupIdentityPrivateKey: fixture.groupSecretKey, authData: nil, invited: false ).upsert(db) @@ -2165,10 +1887,10 @@ class MessageReceiverGroupsSpec: QuickSpec { } // MARK: ------ updates a pending member entry to an accepted member - it("updates a pending member entry to an accepted member") { - mockStorage.write { db in + itTracked("updates a pending member entry to an accepted member") { + fixture.mockStorage.write { db in try GroupMember( - groupId: groupId.hexString, + groupId: fixture.groupId.hexString, profileId: "051111111111111111111111111111111111111111111111111111111111111112", role: .standard, roleStatus: .pending, @@ -2176,19 +1898,19 @@ class MessageReceiverGroupsSpec: QuickSpec { ).upsert(db) } - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: inviteResponseMessage, + message: fixture.inviteResponseMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let members: [GroupMember]? = mockStorage.read { db in try GroupMember.fetchAll(db) } + let members: [GroupMember]? = fixture.mockStorage.read { db in try GroupMember.fetchAll(db) } expect(members?.count).to(equal(1)) expect(members?.first?.profileId).to(equal( "051111111111111111111111111111111111111111111111111111111111111112" @@ -2198,21 +1920,21 @@ class MessageReceiverGroupsSpec: QuickSpec { var cMemberId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111112".cString(using: .utf8)! var groupMember: config_group_member = config_group_member() - expect(groups_members_get(groupMembersConf, &groupMember, &cMemberId)).to(beTrue()) + expect(groups_members_get(fixture.groupMembersConfig.conf, &groupMember, &cMemberId)).to(beTrue()) expect(groupMember.invited).to(equal(0)) } // MARK: ------ updates a failed member entry to an accepted member - it("updates a failed member entry to an accepted member") { + itTracked("updates a failed member entry to an accepted member") { var cMemberId1: [CChar] = "051111111111111111111111111111111111111111111111111111111111111112".cString(using: .utf8)! var groupMember1: config_group_member = config_group_member() - _ = groups_members_get(groupMembersConf, &groupMember1, &cMemberId1) + _ = groups_members_get(fixture.groupMembersConfig.conf, &groupMember1, &cMemberId1) groupMember1.invited = 2 - groups_members_set(groupMembersConf, &groupMember1) + groups_members_set(fixture.groupMembersConfig.conf, &groupMember1) - mockStorage.write { db in + fixture.mockStorage.write { db in try GroupMember( - groupId: groupId.hexString, + groupId: fixture.groupId.hexString, profileId: "051111111111111111111111111111111111111111111111111111111111111112", role: .standard, roleStatus: .failed, @@ -2220,19 +1942,19 @@ class MessageReceiverGroupsSpec: QuickSpec { ).upsert(db) } - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: inviteResponseMessage, + message: fixture.inviteResponseMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let members: [GroupMember]? = mockStorage.read { db in try GroupMember.fetchAll(db) } + let members: [GroupMember]? = fixture.mockStorage.read { db in try GroupMember.fetchAll(db) } expect(members?.count).to(equal(1)) expect(members?.first?.profileId).to(equal( "051111111111111111111111111111111111111111111111111111111111111112" @@ -2242,55 +1964,55 @@ class MessageReceiverGroupsSpec: QuickSpec { var cMemberId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111112".cString(using: .utf8)! var groupMember: config_group_member = config_group_member() - expect(groups_members_get(groupMembersConf, &groupMember, &cMemberId)).to(beTrue()) + expect(groups_members_get(fixture.groupMembersConfig.conf, &groupMember, &cMemberId)).to(beTrue()) expect(groupMember.invited).to(equal(0)) } // MARK: ------ updates the entry in libSession directly if there is no database value - it("updates the entry in libSession directly if there is no database value") { - mockStorage.write { db in + itTracked("updates the entry in libSession directly if there is no database value") { + fixture.mockStorage.write { db in _ = try GroupMember.deleteAll(db) } - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: inviteResponseMessage, + message: fixture.inviteResponseMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } var cMemberId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111112".cString(using: .utf8)! var groupMember: config_group_member = config_group_member() - expect(groups_members_get(groupMembersConf, &groupMember, &cMemberId)).to(beTrue()) + expect(groups_members_get(fixture.groupMembersConfig.conf, &groupMember, &cMemberId)).to(beTrue()) expect(groupMember.invited).to(equal(0)) } // MARK: ---- updates the config member entry with profile information if provided - it("updates the config member entry with profile information if provided") { - mockStorage.write { db in + itTracked("updates the config member entry with profile information if provided") { + fixture.mockStorage.write { db in _ = try GroupMember.deleteAll(db) } - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: inviteResponseMessage, + message: fixture.inviteResponseMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } var cMemberId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111112".cString(using: .utf8)! var groupMember: config_group_member = config_group_member() - expect(groups_members_get(groupMembersConf, &groupMember, &cMemberId)).to(beTrue()) + expect(groups_members_get(fixture.groupMembersConfig.conf, &groupMember, &cMemberId)).to(beTrue()) expect(groupMember.get(\.name)).to(equal("TestOtherMember")) } } @@ -2299,23 +2021,23 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: -- when receiving a delete content message context("when receiving a delete content message") { beforeEach { - mockStorage.write { db in + fixture.mockStorage.write { db in try SessionThread.upsert( db, - id: groupId.hexString, + id: fixture.groupId.hexString, variant: .group, values: SessionThread.TargetValues( creationDateTimestamp: .setTo(1234567890), shouldBeVisible: .setTo(true) ), - using: dependencies + using: fixture.dependencies ) _ = try Interaction( id: 1, serverHash: "TestMessageHash1", messageUuid: nil, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, authorId: "051111111111111111111111111111111111111111111111111111111111111111", variant: .standardIncoming, body: "Test1", @@ -2340,7 +2062,7 @@ class MessageReceiverGroupsSpec: QuickSpec { id: 2, serverHash: "TestMessageHash2", messageUuid: nil, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, authorId: "051111111111111111111111111111111111111111111111111111111111111111", variant: .standardIncoming, body: "Test2", @@ -2365,7 +2087,7 @@ class MessageReceiverGroupsSpec: QuickSpec { id: 3, serverHash: "TestMessageHash3", messageUuid: nil, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, authorId: "051111111111111111111111111111111111111111111111111111111111111112", variant: .standardIncoming, body: "Test3", @@ -2390,7 +2112,7 @@ class MessageReceiverGroupsSpec: QuickSpec { id: 4, serverHash: "TestMessageHash4", messageUuid: nil, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, authorId: "051111111111111111111111111111111111111111111111111111111111111112", variant: .standardIncoming, body: "Test4", @@ -2414,64 +2136,64 @@ class MessageReceiverGroupsSpec: QuickSpec { } // MARK: ---- throws if there is no sender and no admin signature - it("throws if there is no sender and no admin signature") { - deleteContentMessage = GroupUpdateDeleteMemberContentMessage( + itTracked("throws if there is no sender and no admin signature") { + fixture.deleteContentMessage = GroupUpdateDeleteMemberContentMessage( memberSessionIds: ["051111111111111111111111111111111111111111111111111111111111111112"], messageHashes: [], adminSignature: nil ) - deleteContentMessage.sentTimestampMs = 1234567800000 + fixture.deleteContentMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: deleteContentMessage, + message: fixture.deleteContentMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) }.to(throwError(MessageReceiverError.invalidMessage)) } } // MARK: ---- throws if there is no timestamp - it("throws if there is no timestamp") { - deleteContentMessage.sentTimestampMs = nil + itTracked("throws if there is no timestamp") { + fixture.deleteContentMessage.sentTimestampMs = nil - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: deleteContentMessage, + message: fixture.deleteContentMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) }.to(throwError(MessageReceiverError.invalidMessage)) } } // MARK: ---- throws if the admin signature fails to verify - it("throws if the admin signature fails to verify") { - mockCrypto + itTracked("throws if the admin signature fails to verify") { + fixture.mockCrypto .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(false) - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: deleteContentMessage, + message: fixture.deleteContentMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) }.to(throwError(MessageReceiverError.invalidMessage)) } @@ -2480,84 +2202,84 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: ---- and there is no admin signature context("and there is no admin signature") { // MARK: ------ removes content for specific messages from the database - it("removes content for specific messages from the database") { - deleteContentMessage = GroupUpdateDeleteMemberContentMessage( + itTracked("removes content for specific messages from the database") { + fixture.deleteContentMessage = GroupUpdateDeleteMemberContentMessage( memberSessionIds: [], messageHashes: ["TestMessageHash3"], adminSignature: nil ) - deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" - deleteContentMessage.sentTimestampMs = 1234567800000 + fixture.deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" + fixture.deleteContentMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: deleteContentMessage, + message: fixture.deleteContentMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) } + let interactions: [Interaction]? = fixture.mockStorage.read { db in try Interaction.fetchAll(db) } expect(interactions?.count).to(equal(4)) // Message isn't deleted, just content expect(interactions?.map { $0.body }).toNot(contain("Test3")) } // MARK: ------ removes content for all messages from the sender from the database - it("removes content for all messages from the sender from the database") { - deleteContentMessage = GroupUpdateDeleteMemberContentMessage( + itTracked("removes content for all messages from the sender from the database") { + fixture.deleteContentMessage = GroupUpdateDeleteMemberContentMessage( memberSessionIds: [ "051111111111111111111111111111111111111111111111111111111111111112" ], messageHashes: [], adminSignature: nil ) - deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" - deleteContentMessage.sentTimestampMs = 1234567800000 + fixture.deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" + fixture.deleteContentMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: deleteContentMessage, + message: fixture.deleteContentMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) } + let interactions: [Interaction]? = fixture.mockStorage.read { db in try Interaction.fetchAll(db) } expect(interactions?.count).to(equal(4)) // Message isn't deleted, just content expect(interactions?.map { $0.body }).toNot(contain("Test3")) } // MARK: ------ ignores messages not sent by the sender - it("ignores messages not sent by the sender") { - deleteContentMessage = GroupUpdateDeleteMemberContentMessage( + itTracked("ignores messages not sent by the sender") { + fixture.deleteContentMessage = GroupUpdateDeleteMemberContentMessage( memberSessionIds: [], messageHashes: ["TestMessageHash1", "TestMessageHash3"], adminSignature: nil ) - deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" - deleteContentMessage.sentTimestampMs = 1234567800000 + fixture.deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" + fixture.deleteContentMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: deleteContentMessage, + message: fixture.deleteContentMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) } + let interactions: [Interaction]? = fixture.mockStorage.read { db in try Interaction.fetchAll(db) } expect(interactions?.count).to(equal(4)) expect(interactions?.map { $0.serverHash }).to(equal([ "TestMessageHash1", "TestMessageHash2", "TestMessageHash3", "TestMessageHash4" @@ -2583,28 +2305,28 @@ class MessageReceiverGroupsSpec: QuickSpec { } // MARK: ------ ignores messages sent after the delete content message was sent - it("ignores messages sent after the delete content message was sent") { - deleteContentMessage = GroupUpdateDeleteMemberContentMessage( + itTracked("ignores messages sent after the delete content message was sent") { + fixture.deleteContentMessage = GroupUpdateDeleteMemberContentMessage( memberSessionIds: [], messageHashes: ["TestMessageHash3", "TestMessageHash4"], adminSignature: nil ) - deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" - deleteContentMessage.sentTimestampMs = 1234567800000 + fixture.deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" + fixture.deleteContentMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: deleteContentMessage, + message: fixture.deleteContentMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) } + let interactions: [Interaction]? = fixture.mockStorage.read { db in try Interaction.fetchAll(db) } expect(interactions?.count).to(equal(4)) expect(interactions?.map { $0.serverHash }).to(equal([ "TestMessageHash1", "TestMessageHash2", "TestMessageHash3", "TestMessageHash4" @@ -2633,120 +2355,120 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: ---- and there is no admin signature context("and there is no admin signature") { // MARK: ------ removes content for specific messages from the database - it("removes content for specific messages from the database") { - deleteContentMessage = GroupUpdateDeleteMemberContentMessage( + itTracked("removes content for specific messages from the database") { + fixture.deleteContentMessage = GroupUpdateDeleteMemberContentMessage( memberSessionIds: [], messageHashes: ["TestMessageHash3"], adminSignature: .standard(signature: "TestSignature".bytes) ) - deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" - deleteContentMessage.sentTimestampMs = 1234567800000 + fixture.deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" + fixture.deleteContentMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: deleteContentMessage, + message: fixture.deleteContentMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) } + let interactions: [Interaction]? = fixture.mockStorage.read { db in try Interaction.fetchAll(db) } expect(interactions?.count).to(equal(4)) expect(interactions?.map { $0.body }).toNot(contain("Test3")) } // MARK: ------ removes content for all messages for a given id from the database - it("removes content for all messages for a given id from the database") { - deleteContentMessage = GroupUpdateDeleteMemberContentMessage( + itTracked("removes content for all messages for a given id from the database") { + fixture.deleteContentMessage = GroupUpdateDeleteMemberContentMessage( memberSessionIds: [ "051111111111111111111111111111111111111111111111111111111111111112" ], messageHashes: [], adminSignature: .standard(signature: "TestSignature".bytes) ) - deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" - deleteContentMessage.sentTimestampMs = 1234567800000 + fixture.deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" + fixture.deleteContentMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: deleteContentMessage, + message: fixture.deleteContentMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) } + let interactions: [Interaction]? = fixture.mockStorage.read { db in try Interaction.fetchAll(db) } expect(interactions?.count).to(equal(4)) expect(interactions?.map { $0.body }).toNot(contain("Test3")) } // MARK: ------ removes content for specific messages sent from a user that is not the sender from the database - it("removes content for specific messages sent from a user that is not the sender from the database") { - deleteContentMessage = GroupUpdateDeleteMemberContentMessage( + itTracked("removes content for specific messages sent from a user that is not the sender from the database") { + fixture.deleteContentMessage = GroupUpdateDeleteMemberContentMessage( memberSessionIds: [], messageHashes: ["TestMessageHash3"], adminSignature: .standard(signature: "TestSignature".bytes) ) - deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" - deleteContentMessage.sentTimestampMs = 1234567800000 + fixture.deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" + fixture.deleteContentMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: deleteContentMessage, + message: fixture.deleteContentMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) } + let interactions: [Interaction]? = fixture.mockStorage.read { db in try Interaction.fetchAll(db) } expect(interactions?.count).to(equal(4)) expect(interactions?.map { $0.body }).toNot(contain("Test3")) } // MARK: ------ removes content for all messages for a given id that is not the sender from the database - it("removes content for all messages for a given id that is not the sender from the database") { - deleteContentMessage = GroupUpdateDeleteMemberContentMessage( + itTracked("removes content for all messages for a given id that is not the sender from the database") { + fixture.deleteContentMessage = GroupUpdateDeleteMemberContentMessage( memberSessionIds: [ "051111111111111111111111111111111111111111111111111111111111111112" ], messageHashes: [], adminSignature: .standard(signature: "TestSignature".bytes) ) - deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" - deleteContentMessage.sentTimestampMs = 1234567800000 + fixture.deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" + fixture.deleteContentMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: deleteContentMessage, + message: fixture.deleteContentMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) } + let interactions: [Interaction]? = fixture.mockStorage.read { db in try Interaction.fetchAll(db) } expect(interactions?.count).to(equal(4)) expect(interactions?.map { $0.body }).toNot(contain("Test3")) } // MARK: ------ ignores messages sent after the delete content message was sent - it("ignores messages sent after the delete content message was sent") { - deleteContentMessage = GroupUpdateDeleteMemberContentMessage( + itTracked("ignores messages sent after the delete content message was sent") { + fixture.deleteContentMessage = GroupUpdateDeleteMemberContentMessage( memberSessionIds: [ "051111111111111111111111111111111111111111111111111111111111111111", "051111111111111111111111111111111111111111111111111111111111111112" @@ -2754,22 +2476,22 @@ class MessageReceiverGroupsSpec: QuickSpec { messageHashes: [], adminSignature: .standard(signature: "TestSignature".bytes) ) - deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" - deleteContentMessage.sentTimestampMs = 1234567800000 + fixture.deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111111" + fixture.deleteContentMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: deleteContentMessage, + message: fixture.deleteContentMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) } + let interactions: [Interaction]? = fixture.mockStorage.read { db in try Interaction.fetchAll(db) } expect(interactions?.count).to(equal(4)) expect(interactions?.map { $0.serverHash }).to(equal([ "TestMessageHash1", "TestMessageHash2", "TestMessageHash3", "TestMessageHash4" @@ -2798,13 +2520,13 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: ---- and the current user is an admin context("and the current user is an admin") { beforeEach { - mockStorage.write { db in + fixture.mockStorage.write { db in try ClosedGroup( - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, name: "TestGroup", formationTimestamp: 1234567890, shouldPoll: true, - groupIdentityPrivateKey: groupSecretKey, + groupIdentityPrivateKey: fixture.groupSecretKey, authData: nil, invited: false ).upsert(db) @@ -2812,45 +2534,47 @@ class MessageReceiverGroupsSpec: QuickSpec { } // MARK: ------ deletes the messages from the swarm - it("deletes the messages from the swarm") { - deleteContentMessage = GroupUpdateDeleteMemberContentMessage( + itTracked("deletes the messages from the swarm") { + fixture.deleteContentMessage = GroupUpdateDeleteMemberContentMessage( memberSessionIds: [], messageHashes: ["TestMessageHash3"], adminSignature: nil ) - deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" - deleteContentMessage.sentTimestampMs = 1234567800000 + fixture.deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" + fixture.deleteContentMessage.sentTimestampMs = 1234567800000 let preparedRequest: Network.PreparedRequest<[String: Bool]> = try! SnodeAPI .preparedDeleteMessages( serverHashes: ["TestMessageHash3"], requireSuccessfulDeletion: false, authMethod: Authentication.groupAdmin( - groupSessionId: groupId, - ed25519SecretKey: Array(groupSecretKey) + groupSessionId: fixture.groupId, + ed25519SecretKey: Array(fixture.groupSecretKey) ), - using: dependencies + using: fixture.dependencies ) - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: deleteContentMessage, + message: fixture.deleteContentMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - expect(mockNetwork) + expect(fixture.mockNetwork) .to(call(.exactly(times: 1), matchingParameters: .all) { network in network.send( - preparedRequest.body, - to: preparedRequest.destination, + endpoint: SnodeAPI.Endpoint.deleteMessages, + destination: preparedRequest.destination, + body: preparedRequest.body, + category: .standard, requestTimeout: preparedRequest.requestTimeout, - requestAndPathBuildTimeout: preparedRequest.requestAndPathBuildTimeout + overallTimeout: preparedRequest.overallTimeout ) }) } @@ -2859,31 +2583,37 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: ---- and the current user is not an admin context("and the current user is not an admin") { // MARK: ------ does not delete the messages from the swarm - it("does not delete the messages from the swarm") { - deleteContentMessage = GroupUpdateDeleteMemberContentMessage( + itTracked("does not delete the messages from the swarm") { + fixture.deleteContentMessage = GroupUpdateDeleteMemberContentMessage( memberSessionIds: [], messageHashes: ["TestMessageHash3"], adminSignature: nil ) - deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" - deleteContentMessage.sentTimestampMs = 1234567800000 + fixture.deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" + fixture.deleteContentMessage.sentTimestampMs = 1234567800000 - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: deleteContentMessage, + message: fixture.deleteContentMessage, serverExpirationTimestamp: 1234567890, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - expect(mockNetwork) - .toNot(call { network in - network.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) - }) + expect(fixture.mockNetwork).toNot(call { network in + network.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + }) } } } @@ -2891,36 +2621,36 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: -- when receiving a delete message context("when receiving a delete message") { beforeEach { - var cGroupId: [CChar] = groupId.hexString.cString(using: .utf8)! + var cGroupId: [CChar] = fixture.groupId.hexString.cString(using: .utf8)! var userGroup: ugroups_group_info = ugroups_group_info() - user_groups_get_or_construct_group(userGroupsConfig.conf, &userGroup, &cGroupId) + user_groups_get_or_construct_group(fixture.userGroupsConfig.conf, &userGroup, &cGroupId) userGroup.set(\.name, to: "TestName") - user_groups_set_group(userGroupsConfig.conf, &userGroup) + user_groups_set_group(fixture.userGroupsConfig.conf, &userGroup) // Rekey a couple of times to increase the key generation to 1 var fakeHash1: [CChar] = "fakehash1".cString(using: .utf8)! var fakeHash2: [CChar] = "fakehash2".cString(using: .utf8)! var pushResult: UnsafePointer? = nil var pushResultLen: Int = 0 - _ = groups_keys_rekey(groupKeysConf, groupInfoConf, groupMembersConf, &pushResult, &pushResultLen) - _ = groups_keys_load_message(groupKeysConf, &fakeHash1, pushResult, pushResultLen, 1234567890, groupInfoConf, groupMembersConf) - _ = groups_keys_rekey(groupKeysConf, groupInfoConf, groupMembersConf, &pushResult, &pushResultLen) - _ = groups_keys_load_message(groupKeysConf, &fakeHash2, pushResult, pushResultLen, 1234567890, groupInfoConf, groupMembersConf) + _ = groups_keys_rekey(fixture.groupKeysConfig.keysConf, fixture.groupInfoConfig.conf, fixture.groupMembersConfig.conf, &pushResult, &pushResultLen) + _ = groups_keys_load_message(fixture.groupKeysConfig.keysConf, &fakeHash1, pushResult, pushResultLen, 1234567890, fixture.groupInfoConfig.conf, fixture.groupMembersConfig.conf) + _ = groups_keys_rekey(fixture.groupKeysConfig.keysConf, fixture.groupInfoConfig.conf, fixture.groupMembersConfig.conf, &pushResult, &pushResultLen) + _ = groups_keys_load_message(fixture.groupKeysConfig.keysConf, &fakeHash2, pushResult, pushResultLen, 1234567890, fixture.groupInfoConfig.conf, fixture.groupMembersConfig.conf) - mockStorage.write { db in + fixture.mockStorage.write { db in try SessionThread.upsert( db, - id: groupId.hexString, + id: fixture.groupId.hexString, variant: .group, values: SessionThread.TargetValues( creationDateTimestamp: .setTo(1234567890), shouldBeVisible: .setTo(true) ), - using: dependencies + using: fixture.dependencies ) try ClosedGroup( - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, name: "TestGroup", formationTimestamp: 1234567890, shouldPoll: true, @@ -2930,7 +2660,7 @@ class MessageReceiverGroupsSpec: QuickSpec { ).upsert(db) try GroupMember( - groupId: groupId.hexString, + groupId: fixture.groupId.hexString, profileId: "05\(TestConstants.publicKey)", role: .standard, roleStatus: .accepted, @@ -2941,7 +2671,7 @@ class MessageReceiverGroupsSpec: QuickSpec { id: 1, serverHash: nil, messageUuid: nil, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, authorId: "051111111111111111111111111111111111111111111111111111111111111111", variant: .standardIncoming, body: "Test", @@ -2964,21 +2694,21 @@ class MessageReceiverGroupsSpec: QuickSpec { try ConfigDump( variant: .groupKeys, - sessionId: groupId.hexString, + sessionId: fixture.groupId.hexString, data: Data([1, 2, 3]), timestampMs: 1234567890 ).insert(db) try ConfigDump( variant: .groupInfo, - sessionId: groupId.hexString, + sessionId: fixture.groupId.hexString, data: Data([1, 2, 3]), timestampMs: 1234567890 ).insert(db) try ConfigDump( variant: .groupMembers, - sessionId: groupId.hexString, + sessionId: fixture.groupId.hexString, data: Data([1, 2, 3]), timestampMs: 1234567890 ).insert(db) @@ -2986,38 +2716,38 @@ class MessageReceiverGroupsSpec: QuickSpec { } // MARK: ---- deletes any interactions from the conversation - it("deletes any interactions from the conversation") { - mockStorage.write { db in + itTracked("deletes any interactions from the conversation") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } - let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) } + let interactions: [Interaction]? = fixture.mockStorage.read { db in try Interaction.fetchAll(db) } expect(interactions).to(beEmpty()) } // MARK: ---- deletes the group auth data - it("deletes the group auth data") { - mockStorage.write { db in + itTracked("deletes the group auth data") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } - let authData: [Data?]? = mockStorage.read { db in + let authData: [Data?]? = fixture.mockStorage.read { db in try ClosedGroup .select(ClosedGroup.Columns.authData) .asRequest(of: Data?.self) .fetchAll(db) } - let privateKeyData: [Data?]? = mockStorage.read { db in + let privateKeyData: [Data?]? = fixture.mockStorage.read { db in try ClosedGroup .select(ClosedGroup.Columns.groupIdentityPrivateKey) .asRequest(of: Data?.self) @@ -3028,95 +2758,109 @@ class MessageReceiverGroupsSpec: QuickSpec { } // MARK: ---- deletes the group members - it("deletes the group members") { - mockStorage.write { db in + itTracked("deletes the group members") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } - let members: [GroupMember]? = mockStorage.read { db in try GroupMember.fetchAll(db) } + let members: [GroupMember]? = fixture.mockStorage.read { db in try GroupMember.fetchAll(db) } expect(members).to(beEmpty()) } // MARK: ---- removes the group libSession state - it("removes the group libSession state") { - mockStorage.write { db in + itTracked("removes the group libSession state") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } - expect(mockLibSessionCache) + expect(fixture.mockLibSessionCache) .to(call(.exactly(times: 1), matchingParameters: .all) { - $0.removeConfigs(for: groupId) + $0.removeConfigs(for: fixture.groupId) }) } // MARK: ---- removes the cached libSession state dumps - it("removes the cached libSession state dumps") { - mockStorage.write { db in + itTracked("removes the cached libSession state dumps") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } - expect(mockLibSessionCache) + expect(fixture.mockLibSessionCache) .to(call(.exactly(times: 1), matchingParameters: .all) { - $0.removeConfigs(for: groupId) + $0.removeConfigs(for: fixture.groupId) }) - let dumps: [ConfigDump]? = mockStorage.read { db in + let dumps: [ConfigDump]? = fixture.mockStorage.read { db in try ConfigDump - .filter(ConfigDump.Columns.publicKey == groupId.hexString) + .filter(ConfigDump.Columns.publicKey == fixture.groupId.hexString) .fetchAll(db) } expect(dumps).to(beEmpty()) } // MARK: ------ unsubscribes from push notifications - it("unsubscribes from push notifications") { - mockUserDefaults + itTracked("unsubscribes from push notifications") { + fixture.mockUserDefaults .when { $0.string(forKey: UserDefaults.StringKey.deviceToken.rawValue) } .thenReturn(Data([5, 4, 3, 2, 1]).toHexString()) - mockUserDefaults + fixture.mockUserDefaults .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } .thenReturn(true) - let expectedRequest: Network.PreparedRequest = mockStorage.read { db in - try PushNotificationAPI.preparedUnsubscribe( - db, - token: Data([5, 4, 3, 2, 1]), - sessionIds: [groupId], - using: dependencies - ) - }! - - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } - expect(mockNetwork) + expect(fixture.mockNetwork) .to(call(.exactly(times: 1), matchingParameters: .all) { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, - requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + endpoint: PushNotificationAPI.Endpoint.unsubscribe, + destination: .server( + method: .post, + server: PushNotificationAPI.server, + queryParameters: [:], + headers: [:], + x25519PublicKey: PushNotificationAPI.serverPublicKey + ), + body: try! JSONEncoder(using: fixture.dependencies).encode( + PushNotificationAPI.UnsubscribeRequest( + subscriptions: [ + PushNotificationAPI.UnsubscribeRequest.Subscription( + serviceInfo: PushNotificationAPI.ServiceInfo( + token: Data([5, 4, 3, 2, 1]).toHexString() + ), + authMethod: try! Authentication.with( + swarmPublicKey: fixture.groupId.hexString, + using: fixture.dependencies + ), + timestamp: 1234567890 + ) + ] + ) + ), + category: .standard, + requestTimeout: Network.defaultTimeout, + overallTimeout: nil ) }) } @@ -3124,166 +2868,164 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: ---- and the group is an invitation context("and the group is an invitation") { beforeEach { - mockStorage.write { db in + fixture.mockStorage.write { db in try ClosedGroup.updateAll(db, ClosedGroup.Columns.invited.set(to: true)) } } // MARK: ------ deletes the thread - it("deletes the thread") { - mockStorage.write { db in + itTracked("deletes the thread") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } - let threads: [SessionThread]? = mockStorage.read { db in try SessionThread.fetchAll(db) } + let threads: [SessionThread]? = fixture.mockStorage.read { db in try SessionThread.fetchAll(db) } expect(threads).to(beEmpty()) } // MARK: ------ deletes the group - it("deletes the group") { - mockStorage.write { db in + itTracked("deletes the group") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } - let groups: [ClosedGroup]? = mockStorage.read { db in try ClosedGroup.fetchAll(db) } + let groups: [ClosedGroup]? = fixture.mockStorage.read { db in try ClosedGroup.fetchAll(db) } expect(groups).to(beEmpty()) } // MARK: ---- stops the poller - it("stops the poller") { - mockStorage.write { db in + itTracked("stops the poller") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } - expect(mockGroupPollersCache) - .to(call(.exactly(times: 1), matchingParameters: .all) { - $0.stopAndRemovePoller(for: groupId.hexString) - }) + await fixture.mockGroupPollerManager + .verify { await $0.stopAndRemovePoller(for: fixture.groupId.hexString) } + .wasCalled(exactly: 1) } // MARK: ------ removes the group from the USER_GROUPS config - it("removes the group from the USER_GROUPS config") { - mockStorage.write { db in + itTracked("removes the group from the USER_GROUPS config") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } - var cGroupId: [CChar] = groupId.hexString.cString(using: .utf8)! + var cGroupId: [CChar] = fixture.groupId.hexString.cString(using: .utf8)! var userGroup: ugroups_group_info = ugroups_group_info() - expect(user_groups_get_group(userGroupsConfig.conf, &userGroup, &cGroupId)).to(beFalse()) + expect(user_groups_get_group(fixture.userGroupsConfig.conf, &userGroup, &cGroupId)).to(beFalse()) } } // MARK: ---- and the group is not an invitation context("and the group is not an invitation") { beforeEach { - mockStorage.write { db in + fixture.mockStorage.write { db in try ClosedGroup.updateAll(db, ClosedGroup.Columns.invited.set(to: false)) } } // MARK: ------ does not delete the thread - it("does not delete the thread") { - mockStorage.write { db in + itTracked("does not delete the thread") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } - let threads: [SessionThread]? = mockStorage.read { db in try SessionThread.fetchAll(db) } + let threads: [SessionThread]? = fixture.mockStorage.read { db in try SessionThread.fetchAll(db) } expect(threads).toNot(beEmpty()) } // MARK: ------ does not remove the group from the USER_GROUPS config - it("does not remove the group from the USER_GROUPS config") { - mockStorage.write { db in + itTracked("does not remove the group from the USER_GROUPS config") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } - var cGroupId: [CChar] = groupId.hexString.cString(using: .utf8)! + var cGroupId: [CChar] = fixture.groupId.hexString.cString(using: .utf8)! var userGroup: ugroups_group_info = ugroups_group_info() - expect(user_groups_get_group(userGroupsConfig.conf, &userGroup, &cGroupId)).to(beTrue()) + expect(user_groups_get_group(fixture.userGroupsConfig.conf, &userGroup, &cGroupId)).to(beTrue()) } // MARK: ---- stops the poller and flags the group to not poll - it("stops the poller and flags the group to not poll") { - mockStorage.write { db in + itTracked("stops the poller and flags the group to not poll") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } - let shouldPoll: [Bool]? = mockStorage.read { db in + let shouldPoll: [Bool]? = fixture.mockStorage.read { db in try ClosedGroup .select(ClosedGroup.Columns.shouldPoll) .asRequest(of: Bool.self) .fetchAll(db) } - expect(mockGroupPollersCache) - .to(call(.exactly(times: 1), matchingParameters: .all) { - $0.stopAndRemovePoller(for: groupId.hexString) - }) + await fixture.mockGroupPollerManager + .verify { await $0.stopAndRemovePoller(for: fixture.groupId.hexString) } + .wasCalled(exactly: 1) expect(shouldPoll).to(equal([false])) } // MARK: ------ marks the group in USER_GROUPS as kicked - it("marks the group in USER_GROUPS as kicked") { - mockStorage.write { db in + itTracked("marks the group in USER_GROUPS as kicked") { + fixture.mockStorage.write { db in try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } - expect(mockLibSessionCache).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.markAsKicked(groupSessionIds: [groupId.hexString]) + expect(fixture.mockLibSessionCache).to(call(.exactly(times: 1), matchingParameters: .all) { + try $0.markAsKicked(groupSessionIds: [fixture.groupId.hexString]) }) } } // MARK: ---- throws if the data is invalid - it("throws if the data is invalid") { - deleteMessage = Data([1, 2, 3]) + itTracked("throws if the data is invalid") { + fixture.deleteMessage = Data([1, 2, 3]) - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } .to(throwError(MessageReceiverError.invalidMessage)) @@ -3291,19 +3033,19 @@ class MessageReceiverGroupsSpec: QuickSpec { } // MARK: ---- throws if the included member id does not match the current user - it("throws if the included member id does not match the current user") { - deleteMessage = try! LibSessionMessage.groupKicked( + itTracked("throws if the included member id does not match the current user") { + fixture.deleteMessage = try! LibSessionMessage.groupKicked( memberId: "051111111111111111111111111111111111111111111111111111111111111111", groupKeysGen: 1 ).1 - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } .to(throwError(MessageReceiverError.invalidMessage)) @@ -3311,19 +3053,19 @@ class MessageReceiverGroupsSpec: QuickSpec { } // MARK: ---- throws if the key generation is earlier than the current keys generation - it("throws if the key generation is earlier than the current keys generation") { - deleteMessage = try! LibSessionMessage.groupKicked( + itTracked("throws if the key generation is earlier than the current keys generation") { + fixture.deleteMessage = try! LibSessionMessage.groupKicked( memberId: "05\(TestConstants.publicKey)", groupKeysGen: 0 ).1 - mockStorage.write { db in + fixture.mockStorage.write { db in expect { try MessageReceiver.handleGroupDelete( db, - groupSessionId: groupId, - plaintext: deleteMessage, - using: dependencies + groupSessionId: fixture.groupId, + plaintext: fixture.deleteMessage, + using: fixture.dependencies ) } .to(throwError(MessageReceiverError.invalidMessage)) @@ -3337,29 +3079,29 @@ class MessageReceiverGroupsSpec: QuickSpec { // Only update members if they already exist in the group var cMemberId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111112".cString(using: .utf8)! var groupMember: config_group_member = config_group_member() - _ = groups_members_get_or_construct(groupMembersConf, &groupMember, &cMemberId) + _ = groups_members_get_or_construct(fixture.groupMembersConfig.conf, &groupMember, &cMemberId) groupMember.set(\.name, to: "TestOtherMember") groupMember.invited = 1 - groups_members_set(groupMembersConf, &groupMember) + groups_members_set(fixture.groupMembersConfig.conf, &groupMember) - mockStorage.write { db in + fixture.mockStorage.write { db in try SessionThread.upsert( db, - id: groupId.hexString, + id: fixture.groupId.hexString, variant: .group, values: SessionThread.TargetValues( creationDateTimestamp: .setTo(1234567890), shouldBeVisible: .setTo(true) ), - using: dependencies + using: fixture.dependencies ) try ClosedGroup( - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, name: "TestGroup", formationTimestamp: 1234567890, shouldPoll: true, - groupIdentityPrivateKey: groupSecretKey, + groupIdentityPrivateKey: fixture.groupSecretKey, authData: nil, invited: false ).upsert(db) @@ -3367,10 +3109,10 @@ class MessageReceiverGroupsSpec: QuickSpec { } // MARK: ---- updates a pending member entry to an accepted member - it("updates a pending member entry to an accepted member") { - mockStorage.write { db in + itTracked("updates a pending member entry to an accepted member") { + fixture.mockStorage.write { db in try GroupMember( - groupId: groupId.hexString, + groupId: fixture.groupId.hexString, profileId: "051111111111111111111111111111111111111111111111111111111111111112", role: .standard, roleStatus: .pending, @@ -3378,20 +3120,20 @@ class MessageReceiverGroupsSpec: QuickSpec { ).upsert(db) } - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handle( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: visibleMessage, + message: fixture.visibleMessage, serverExpirationTimestamp: nil, - associatedWithProto: visibleMessageProto, + associatedWithProto: fixture.visibleMessageProto, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let members: [GroupMember]? = mockStorage.read { db in try GroupMember.fetchAll(db) } + let members: [GroupMember]? = fixture.mockStorage.read { db in try GroupMember.fetchAll(db) } expect(members?.count).to(equal(1)) expect(members?.first?.profileId).to(equal( "051111111111111111111111111111111111111111111111111111111111111112" @@ -3401,21 +3143,21 @@ class MessageReceiverGroupsSpec: QuickSpec { var cMemberId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111112".cString(using: .utf8)! var groupMember: config_group_member = config_group_member() - expect(groups_members_get(groupMembersConf, &groupMember, &cMemberId)).to(beTrue()) + expect(groups_members_get(fixture.groupMembersConfig.conf, &groupMember, &cMemberId)).to(beTrue()) expect(groupMember.invited).to(equal(0)) } // MARK: ---- updates a failed member entry to an accepted member - it("updates a failed member entry to an accepted member") { + itTracked("updates a failed member entry to an accepted member") { var cMemberId1: [CChar] = "051111111111111111111111111111111111111111111111111111111111111112".cString(using: .utf8)! var groupMember1: config_group_member = config_group_member() - _ = groups_members_get(groupMembersConf, &groupMember1, &cMemberId1) + _ = groups_members_get(fixture.groupMembersConfig.conf, &groupMember1, &cMemberId1) groupMember1.invited = 2 - groups_members_set(groupMembersConf, &groupMember1) + groups_members_set(fixture.groupMembersConfig.conf, &groupMember1) - mockStorage.write { db in + fixture.mockStorage.write { db in try GroupMember( - groupId: groupId.hexString, + groupId: fixture.groupId.hexString, profileId: "051111111111111111111111111111111111111111111111111111111111111112", role: .standard, roleStatus: .failed, @@ -3423,20 +3165,20 @@ class MessageReceiverGroupsSpec: QuickSpec { ).upsert(db) } - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handle( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: visibleMessage, + message: fixture.visibleMessage, serverExpirationTimestamp: nil, - associatedWithProto: visibleMessageProto, + associatedWithProto: fixture.visibleMessageProto, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } - let members: [GroupMember]? = mockStorage.read { db in try GroupMember.fetchAll(db) } + let members: [GroupMember]? = fixture.mockStorage.read { db in try GroupMember.fetchAll(db) } expect(members?.count).to(equal(1)) expect(members?.first?.profileId).to(equal( "051111111111111111111111111111111111111111111111111111111111111112" @@ -3446,32 +3188,32 @@ class MessageReceiverGroupsSpec: QuickSpec { var cMemberId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111112".cString(using: .utf8)! var groupMember: config_group_member = config_group_member() - expect(groups_members_get(groupMembersConf, &groupMember, &cMemberId)).to(beTrue()) + expect(groups_members_get(fixture.groupMembersConfig.conf, &groupMember, &cMemberId)).to(beTrue()) expect(groupMember.invited).to(equal(0)) } // MARK: ---- updates the entry in libSession directly if there is no database value - it("updates the entry in libSession directly if there is no database value") { - mockStorage.write { db in + itTracked("updates the entry in libSession directly if there is no database value") { + fixture.mockStorage.write { db in _ = try GroupMember.deleteAll(db) } - mockStorage.write { db in + fixture.mockStorage.write { db in try MessageReceiver.handle( db, - threadId: groupId.hexString, + threadId: fixture.groupId.hexString, threadVariant: .group, - message: visibleMessage, + message: fixture.visibleMessage, serverExpirationTimestamp: nil, - associatedWithProto: visibleMessageProto, + associatedWithProto: fixture.visibleMessageProto, suppressNotifications: false, - using: dependencies + using: fixture.dependencies ) } var cMemberId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111112".cString(using: .utf8)! var groupMember: config_group_member = config_group_member() - expect(groups_members_get(groupMembersConf, &groupMember, &cMemberId)).to(beTrue()) + expect(groups_members_get(fixture.groupMembersConfig.conf, &groupMember, &cMemberId)).to(beTrue()) expect(groupMember.invited).to(equal(0)) } } @@ -3479,6 +3221,499 @@ class MessageReceiverGroupsSpec: QuickSpec { } } +// MARK: - Configuration + +private class MessageReceiverGroupsTestFixture: FixtureBase { + var mockStorage: Storage { + mock(for: .storage) { dependencies in + SynchronousStorage( + customWriter: try! DatabaseQueue(), + using: dependencies + ) + } + } + var mockNetwork: MockNetwork { mock(for: .network) { MockNetwork() } } + var mockJobRunner: MockJobRunner { mock(for: .jobRunner) { MockJobRunner() } } + var mockAppContext: MockAppContext { mock(for: .appContext) } + var mockUserDefaults: MockUserDefaults { mock(for: .standard) { MockUserDefaults() } } + var mockCrypto: MockCrypto { mock(for: .crypto) { MockCrypto() } } + var mockKeychain: MockKeychain { mock(for: .keychain) { MockKeychain() } } + var mockFileManager: MockFileManager { mock(for: .fileManager) { MockFileManager() } } + var mockExtensionHelper: MockExtensionHelper { mock(for: .extensionHelper) { MockExtensionHelper() } } + var mockGroupPollerManager: MockGroupPollerManager { mock(for: .groupPollerManager) } + var mockNotificationsManager: MockNotificationsManager { + mock(for: .notificationsManager) { MockNotificationsManager() } + } + var mockGeneralCache: MockGeneralCache { mock(cache: .general) { MockGeneralCache() } } + var mockLibSessionCache: MockLibSessionCache { mock(cache: .libSession) { MockLibSessionCache() } } + var mockSnodeAPICache: MockSnodeAPICache { mock(cache: .snodeAPI) { MockSnodeAPICache() } } + let mockPoller: MockPoller = .create() + + let userGroupsConfig: LibSession.Config + let convoInfoVolatileConfig: LibSession.Config + let groupInfoConfig: LibSession.Config + let groupMembersConfig: LibSession.Config + let groupKeysConfig: LibSession.Config + + let groupSeed: Data + let groupKeyPair: KeyPair + let groupId: SessionId + let groupSecretKey: Data + + let inviteMessage: GroupUpdateInviteMessage + let promoteMessage: GroupUpdatePromoteMessage + var infoChangedMessage: GroupUpdateInfoChangeMessage + var memberChangedMessage: GroupUpdateMemberChangeMessage + let memberLeftMessage: GroupUpdateMemberLeftMessage + let memberLeftNotificationMessage: GroupUpdateMemberLeftNotificationMessage + let inviteResponseMessage: GroupUpdateInviteResponseMessage + var deleteMessage: Data + var deleteContentMessage: GroupUpdateDeleteMemberContentMessage + let visibleMessageProto: SNProtoContent + let visibleMessage: VisibleMessage + + override init() { + let constants = Self.setupConstants() + groupSeed = constants.groupSeed + groupKeyPair = constants.groupKeyPair + groupId = constants.groupId + groupSecretKey = constants.groupSecretKey + + let configs = Self.setupConfigs(constants: constants) + userGroupsConfig = configs.userGroupsConfig + convoInfoVolatileConfig = configs.convoInfoVolatileConfig + groupInfoConfig = configs.groupInfoConfig + groupMembersConfig = configs.groupMembersConfig + groupKeysConfig = configs.groupKeysConfig + + let messages = Self.setupMessages(constants: constants) + inviteMessage = messages.inviteMessage + promoteMessage = messages.promoteMessage + infoChangedMessage = messages.infoChangedMessage + memberChangedMessage = messages.memberChangedMessage + memberLeftMessage = messages.memberLeftMessage + memberLeftNotificationMessage = messages.memberLeftNotificationMessage + inviteResponseMessage = messages.inviteResponseMessage + deleteMessage = messages.deleteMessage + deleteContentMessage = messages.deleteContentMessage + visibleMessageProto = messages.visibleMessageProto + visibleMessage = messages.visibleMessage + + super.init() + } + + static func create() async throws -> MessageReceiverGroupsTestFixture { + let fixture: MessageReceiverGroupsTestFixture = MessageReceiverGroupsTestFixture() + try await fixture.applyBaselineStubs() + + return fixture + } + + // MARK: - Setup + + typealias Constants = (groupSeed: Data, groupId: SessionId, groupKeyPair: KeyPair, groupSecretKey: Data) + private static func setupConstants() -> Constants { + let groupSeed: Data = Data(hex: "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210") + let groupId: SessionId = SessionId( + .group, + hex: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece" + ) + let groupKeyPair: KeyPair = try! Crypto(using: .any).tryGenerate(.ed25519KeyPair(seed: Array(groupSeed))) + let groupSecretKey: Data = Data(hex: + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210" + + "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece" + ) + + return (groupSeed, groupId, groupKeyPair, groupSecretKey) + } + + typealias Configs = ( + userGroupsConfig: LibSession.Config, + convoInfoVolatileConfig: LibSession.Config, + groupInfoConfig: LibSession.Config, + groupMembersConfig: LibSession.Config, + groupKeysConfig: LibSession.Config + ) + private static func setupConfigs(constants: Constants) -> Configs { + var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) + var groupEdPK: [UInt8] = constants.groupKeyPair.publicKey + var groupEdSK: [UInt8] = constants.groupKeyPair.secretKey + + let userGroupsConfig: LibSession.Config = { + var conf: UnsafeMutablePointer! + _ = user_groups_init(&conf, &secretKey, nil, 0, nil) + + return .userGroups(conf) + }() + let convoInfoVolatileConfig: LibSession.Config = { + var conf: UnsafeMutablePointer! + _ = convo_info_volatile_init(&conf, &secretKey, nil, 0, nil) + + return .convoInfoVolatile(conf) + }() + let groupInfoConf: UnsafeMutablePointer = { + var conf: UnsafeMutablePointer! + _ = groups_info_init(&conf, &groupEdPK, &groupEdSK, nil, 0, nil) + + return conf + }() + let groupMembersConf: UnsafeMutablePointer = { + var conf: UnsafeMutablePointer! + _ = groups_members_init(&conf, &groupEdPK, &groupEdSK, nil, 0, nil) + + return conf + }() + let groupKeysConf: UnsafeMutablePointer = { + var conf: UnsafeMutablePointer! + _ = groups_keys_init(&conf, &secretKey, &groupEdPK, &groupEdSK, groupInfoConf, groupMembersConf, nil, 0, nil) + + return conf + }() + let groupInfoConfig: LibSession.Config = .groupInfo(groupInfoConf) + let groupMembersConfig: LibSession.Config = .groupMembers(groupMembersConf) + let groupKeysConfig: LibSession.Config = .groupKeys( + groupKeysConf, + info: groupInfoConf, + members: groupMembersConf + ) + + return (userGroupsConfig, convoInfoVolatileConfig, groupInfoConfig, groupMembersConfig, groupKeysConfig) + } + + typealias Messages = ( + inviteMessage: GroupUpdateInviteMessage, + promoteMessage: GroupUpdatePromoteMessage, + infoChangedMessage: GroupUpdateInfoChangeMessage, + memberChangedMessage: GroupUpdateMemberChangeMessage, + memberLeftMessage: GroupUpdateMemberLeftMessage, + memberLeftNotificationMessage: GroupUpdateMemberLeftNotificationMessage, + inviteResponseMessage: GroupUpdateInviteResponseMessage, + deleteMessage: Data, + deleteContentMessage: GroupUpdateDeleteMemberContentMessage, + visibleMessageProto: SNProtoContent, + visibleMessage: VisibleMessage + ) + private static func setupMessages(constants: Constants) -> Messages { + let inviteMessage = { + let result: GroupUpdateInviteMessage = GroupUpdateInviteMessage( + inviteeSessionIdHexString: "TestId", + groupSessionId: constants.groupId, + groupName: "TestGroup", + memberAuthData: Data([1, 2, 3]), + profile: nil, + adminSignature: .standard(signature: "TestSignature".bytes) + ) + result.sender = "051111111111111111111111111111111111111111111111111111111111111111" + result.sentTimestampMs = 1234567890000 + + return result + }() + let promoteMessage = { + let result: GroupUpdatePromoteMessage = GroupUpdatePromoteMessage( + groupIdentitySeed: constants.groupSeed, + groupName: "TestGroup", + sentTimestampMs: 1234567890000 + ) + result.sender = "051111111111111111111111111111111111111111111111111111111111111111" + + return result + }() + let infoChangedMessage = { + let result: GroupUpdateInfoChangeMessage = GroupUpdateInfoChangeMessage( + changeType: .name, + updatedName: "TestGroup Rename", + updatedExpiration: nil, + adminSignature: .standard(signature: "TestSignature".bytes) + ) + result.sender = "051111111111111111111111111111111111111111111111111111111111111111" + result.sentTimestampMs = 1234567800000 + + return result + }() + let memberChangedMessage = { + let result: GroupUpdateMemberChangeMessage = GroupUpdateMemberChangeMessage( + changeType: .added, + memberSessionIds: ["051111111111111111111111111111111111111111111111111111111111111112"], + historyShared: false, + adminSignature: .standard(signature: "TestSignature".bytes) + ) + result.sender = "051111111111111111111111111111111111111111111111111111111111111111" + result.sentTimestampMs = 1234567800000 + + return result + }() + let memberLeftMessage = { + let result: GroupUpdateMemberLeftMessage = GroupUpdateMemberLeftMessage() + result.sender = "051111111111111111111111111111111111111111111111111111111111111112" + result.sentTimestampMs = 1234567800000 + + return result + }() + let memberLeftNotificationMessage = { + let result: GroupUpdateMemberLeftNotificationMessage = GroupUpdateMemberLeftNotificationMessage() + result.sender = "051111111111111111111111111111111111111111111111111111111111111112" + result.sentTimestampMs = 1234567800000 + + return result + }() + let inviteResponseMessage = { + let result: GroupUpdateInviteResponseMessage = GroupUpdateInviteResponseMessage( + isApproved: true, + profile: VisibleMessage.VMProfile(displayName: "TestOtherMember"), + sentTimestampMs: 1234567800000 + ) + result.sender = "051111111111111111111111111111111111111111111111111111111111111112" + + return result + }() + let deleteMessage = try! LibSessionMessage.groupKicked( + memberId: "05\(TestConstants.publicKey)", + groupKeysGen: 1 + ).1 + let deleteContentMessage = { + let result: GroupUpdateDeleteMemberContentMessage = GroupUpdateDeleteMemberContentMessage( + memberSessionIds: ["051111111111111111111111111111111111111111111111111111111111111112"], + messageHashes: [], + adminSignature: .standard(signature: "TestSignature".bytes) + ) + result.sender = "051111111111111111111111111111111111111111111111111111111111111112" + result.sentTimestampMs = 1234567800000 + + return result + }() + let visibleMessageProto = { + let proto = SNProtoContent.builder() + proto.setSigTimestamp((1234568890 - (60 * 10)) * 1000) + + let dataMessage = SNProtoDataMessage.builder() + dataMessage.setBody("Test") + proto.setDataMessage(try! dataMessage.build()) + return try! proto.build() + }() + let visibleMessage = { + let result = VisibleMessage( + sender: "051111111111111111111111111111111111111111111111111111111111111112", + sentTimestampMs: ((1234568890 - (60 * 10)) * 1000), + text: "Test" + ) + result.receivedTimestampMs = (1234568890 * 1000) + return result + }() + + return ( + inviteMessage, + promoteMessage, + infoChangedMessage, + memberChangedMessage, + memberLeftMessage, + memberLeftNotificationMessage, + inviteResponseMessage, + deleteMessage, + deleteContentMessage, + visibleMessageProto, + visibleMessage + ) + } + + // MARK: - Default State + + private func applyBaselineStubs() async throws { + try await applyBaselineStorage() + await applyBaselineNetwork() + await applyBaselineJobRunner() + await applyBaselineAppContext() + await applyBaselineUserDefaults() + await applyBaselineCrypto() + await applyBaselineKeychain() + await applyBaselineFileManager() + await applyBaselineExtensionHelper() + await applyBaselineGroupPollerManager() + await applyBaselineNotificationsManager() + await applyBaselineGeneralCache() + await applyBaselineLibSessionCache() + await applyBaselineSnodeAPICache() + await applyBaselinePoller() + } + + private func applyBaselineStorage() async throws { + try await mockStorage.perform(migrations: SNMessagingKit.migrations, onProgressUpdate: nil) + try await mockStorage.writeAsync { db in + try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) + try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) + + try Profile( + id: "05\(TestConstants.publicKey)", + name: "TestCurrentUser" + ).insert(db) + } + } + + private func applyBaselineNetwork() async { + mockNetwork + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } + .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1"))) + mockNetwork + .when { try await $0.getSwarm(for: .any) } + .thenReturn([ + LibSession.Snode( + ed25519PubkeyHex: TestConstants.edPublicKey, + ip: "1.1.1.1", + httpsPort: 1111, + quicPort: 1112, + version: "2.11.0", + swarmId: 1 + ), + LibSession.Snode( + ed25519PubkeyHex: TestConstants.edPublicKey, + ip: "1.1.1.1", + httpsPort: 1121, + quicPort: 1122, + version: "2.11.0", + swarmId: 1 + ), + LibSession.Snode( + ed25519PubkeyHex: TestConstants.edPublicKey, + ip: "1.1.1.1", + httpsPort: 1131, + quicPort: 1132, + version: "2.11.0", + swarmId: 1 + ) + ]) + } + + private func applyBaselineJobRunner() async { + mockJobRunner.when { $0.jobInfoFor(jobs: .any, state: .any, variant: .any) }.thenReturn([:]) + mockJobRunner.when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) }.thenReturn(nil) + mockJobRunner.when { $0.upsert(.any, job: .any, canStartJob: .any) }.thenReturn(nil) + mockJobRunner.when { $0.manuallyTriggerResult(.any, result: .any) }.thenReturn(()) + } + + private func applyBaselineAppContext() async { + await mockAppContext.when { $0.isMainApp }.thenReturn(false) + } + + private func applyBaselineUserDefaults() async { + mockUserDefaults.when { $0.string(forKey: .any) }.thenReturn(nil) + } + + private func applyBaselineCrypto() async { + mockCrypto + .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } + .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) + mockCrypto + .when { $0.generate(.signatureSubaccount(config: .any, verificationBytes: .any, memberAuthData: .any)) } + .thenReturn(Authentication.Signature.subaccount( + subaccount: "TestSubAccount".bytes, + subaccountSig: "TestSubAccountSignature".bytes, + signature: "TestSignature".bytes + )) + mockCrypto + .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } + .thenReturn(true) + mockCrypto.when { $0.generate(.ed25519KeyPair(seed: .any)) }.thenReturn(groupKeyPair) + mockCrypto + .when { $0.verify(.memberAuthData(groupSessionId: .any, ed25519SecretKey: .any, memberAuthData: .any)) } + .thenReturn(true) + mockCrypto + .when { $0.generate(.hash(message: .any, key: .any, length: .any)) } + .thenReturn("TestHash".bytes) + } + + private func applyBaselineKeychain() async { + mockKeychain + .when { + try $0.migrateLegacyKeyIfNeeded( + legacyKey: .any, + legacyService: .any, + toKey: .pushNotificationEncryptionKey + ) + } + .thenReturn(()) + mockKeychain + .when { + try $0.getOrGenerateEncryptionKey( + forKey: .any, + length: .any, + cat: .any, + legacyKey: .any, + legacyService: .any + ) + } + .thenReturn(Data([1, 2, 3])) + mockKeychain + .when { try $0.data(forKey: .pushNotificationEncryptionKey) } + .thenReturn(Data((0..? { + switch self { + case .groupKeys(let conf, _, _): return conf + default: return nil + } + } } private extension Result { diff --git a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift new file mode 100644 index 0000000000..2b575b5c84 --- /dev/null +++ b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift @@ -0,0 +1,235 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import Combine +import GRDB +import SessionNetworkingKit +import SessionUtilitiesKit + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class CommunityPollerManagerSpec: AsyncSpec { + override class func spec() { + @TestState var fixture: CommunityPollerManagerTestFixture! + + beforeEach { + fixture = try await CommunityPollerManagerTestFixture.create() + } + + // MARK: - a CommunityPollerManager + describe("a CommunityPollerManager") { + // MARK: -- when starting polling + context("when starting polling") { + // MARK: ---- creates pollers for all of the communities + it("creates pollers for all of the communities") { + await fixture.setupForActivePolling() + await fixture.manager.startAllPollers() + + await expect { await fixture.manager.serversBeingPolled } + .to(equal(["testserver", "testserver1"])) + } + + // MARK: ---- creates a poll task + it("creates a poll task") { + await fixture.setupForActivePolling() + await fixture.manager.startAllPollers() + + await expect { await fixture.manager.allPollers.count } .to(equal(2)) + try await require { await fixture.manager.allPollers.count }.to(equal(2)) + await expect { await fixture.manager.allPollers[0].pollTask }.toNot(beNil()) + await expect { await fixture.manager.allPollers[1].pollTask }.toNot(beNil()) + } + + // MARK: ---- does not create additional pollers if it's already polling + it("does not create additional pollers if it's already polling") { + await fixture.setupForActivePolling() + await fixture.manager + .getOrCreatePoller(for: CommunityPoller.Info(server: "testserver", pollFailureCount: 0)) + .startIfNeeded() + await fixture.manager + .getOrCreatePoller(for: CommunityPoller.Info(server: "testserver1", pollFailureCount: 0)) + .startIfNeeded() + + await fixture.manager.startAllPollers() + + await expect { await fixture.manager.allPollers.count }.to(equal(2)) + } + } + + // MARK: -- when stopping polling + context("when stopping polling") { + // MARK: ---- removes all pollers + it("removes all pollers") { + await fixture.manager.startAllPollers() + await fixture.manager.stopAndRemoveAllPollers() + + await expect { await fixture.manager.allPollers.count }.to(equal(0)) + } + + // MARK: ---- updates the isPolling flag + it("updates the isPolling flag") { + await fixture.manager.startAllPollers() + + let poller1 = await fixture.manager.getOrCreatePoller(for: CommunityPoller.Info(server: "testserver", pollFailureCount: 0)) + let poller2 = await fixture.manager.getOrCreatePoller(for: CommunityPoller.Info(server: "testserver1", pollFailureCount: 0)) + await fixture.manager.stopAndRemoveAllPollers() + + await expect { await poller1.pollTask }.to(beNil()) + await expect { await poller2.pollTask }.to(beNil()) + } + } + } + } +} + +// MARK: - Configuration + +private class CommunityPollerManagerTestFixture: FixtureBase { + var mockStorage: Storage { + mock(for: .storage) { dependencies in + Storage( + customWriter: try! DatabaseQueue(), + using: dependencies + ) + } + } + var mockNetwork: MockNetwork { mock(for: .network) { MockNetwork() } } + var mockAppContext: MockAppContext { mock(for: .appContext) } + var mockUserDefaults: MockUserDefaults { mock(for: .standard) { MockUserDefaults() } } + var mockGeneralCache: MockGeneralCache { mock(cache: .general) { MockGeneralCache() } } + var mockOGMCache: MockOGMCache { mock(cache: .openGroupManager) { MockOGMCache() } } + var mockCrypto: MockCrypto { mock(for: .crypto) { MockCrypto() } } + lazy var manager: CommunityPollerManager = CommunityPollerManager(using: dependencies) + + static func create() async throws -> CommunityPollerManagerTestFixture { + let fixture: CommunityPollerManagerTestFixture = CommunityPollerManagerTestFixture() + try await fixture.applyBaselineStubs() + + return fixture + } + + // MARK: - Default State + + private func applyBaselineStubs() async throws { + try await applyBaselineStorage() + await applyBaselineNetwork() + await applyBaselineAppContext() + await applyBaselineUserDefaults() + await applyBaselineGeneralCache() + await applyBaselineOGMCache() + await applyBaselineCrypto() + } + + private func applyBaselineStorage() async throws { + try await mockStorage.perform(migrations: SNMessagingKit.migrations, onProgressUpdate: nil) + try await mockStorage.writeAsync { db in + try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) + try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) + + try OpenGroup( + server: "testServer", + roomToken: "testRoom", + publicKey: TestConstants.publicKey, + isActive: true, + name: "Test", + roomDescription: nil, + imageId: nil, + userCount: 0, + infoUpdates: 0 + ).insert(db) + try OpenGroup( + server: "testServer1", + roomToken: "testRoom1", + publicKey: TestConstants.publicKey, + isActive: true, + name: "Test1", + roomDescription: nil, + imageId: nil, + userCount: 0, + infoUpdates: 0 + ).insert(db) + try Capability(openGroupServer: "testServer", variant: .sogs, isMissing: false).insert(db) + } + } + + private func applyBaselineNetwork() async { + mockNetwork.when { await $0.isSuspended }.thenReturn(false) + mockNetwork.when { $0.networkStatus }.thenReturn(.singleValue(value: .connected)) + + /// Delay for 10 seconds because we don't want the Poller to get stuck in a recursive loop + mockNetwork + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } + .thenReturn( + MockNetwork.response(with: FileUploadResponse(id: "1")) + .delay(for: .seconds(10), scheduler: DispatchQueue.main) + .eraseToAnyPublisher() + ) + } + + private func applyBaselineAppContext() async { + await mockAppContext.when { await $0.isMainAppAndActive }.thenReturn(false) + } + + private func applyBaselineUserDefaults() async {} + + private func applyBaselineGeneralCache() async { + mockGeneralCache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) + mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) + mockGeneralCache + .when { $0.ed25519Seed } + .thenReturn(Array(Array(Data(hex: TestConstants.edSecretKey)).prefix(upTo: 32))) + } + + private func applyBaselineOGMCache() async { + mockOGMCache.when { $0.pendingChanges }.thenReturn([]) + mockOGMCache.when { $0.getLastSuccessfulCommunityPollTimestamp() }.thenReturn(0) + } + + private func applyBaselineCrypto() async { + mockCrypto + .when { $0.generate(.hash(message: .any, key: .any, length: .any)) } + .thenReturn([]) + mockCrypto + .when { $0.generate(.blinded15KeyPair(serverPublicKey: .any, ed25519SecretKey: .any)) } + .thenReturn( + KeyPair( + publicKey: Data(hex: TestConstants.publicKey).bytes, + secretKey: Data(hex: TestConstants.edSecretKey).bytes + ) + ) + mockCrypto + .when { $0.generate(.signatureBlind15(message: .any, serverPublicKey: .any, ed25519SecretKey: .any)) } + .thenReturn("TestSogsSignature".bytes) + mockCrypto + .when { $0.generate(.randomBytes(16)) } + .thenReturn(Array(Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!)) + mockCrypto + .when { $0.generate(.ed25519KeyPair(seed: .any)) } + .thenReturn( + KeyPair( + publicKey: Array(Data(hex: TestConstants.edPublicKey)), + secretKey: Array(Data(hex: TestConstants.edSecretKey)) + ) + ) + } + + // MARK: - Test Specific Configurations + + @MainActor func setupForActivePolling() async { + await mockAppContext.when { $0.isMainAppAndActive }.thenReturn(true) + } +} diff --git a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift deleted file mode 100644 index 79f98b853e..0000000000 --- a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. - -import UIKit -import Combine -import GRDB -import SessionNetworkingKit -import SessionUtilitiesKit - -import Quick -import Nimble - -@testable import SessionMessagingKit - -class CommunityPollerSpec: AsyncSpec { - override class func spec() { - // MARK: Configuration - - @TestState var dependencies: TestDependencies! = TestDependencies { dependencies in - dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) - dependencies.forceSynchronous = true - } - @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( - customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, - using: dependencies, - initialData: { db in - try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) - try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - - try OpenGroup( - server: "testServer", - roomToken: "testRoom", - publicKey: TestConstants.publicKey, - isActive: true, - name: "Test", - roomDescription: nil, - imageId: nil, - userCount: 0, - infoUpdates: 0 - ).insert(db) - try OpenGroup( - server: "testServer1", - roomToken: "testRoom1", - publicKey: TestConstants.publicKey, - isActive: true, - name: "Test1", - roomDescription: nil, - imageId: nil, - userCount: 0, - infoUpdates: 0 - ).insert(db) - try Capability(openGroupServer: "testServer", variant: .sogs, isMissing: false).insert(db) - } - ) - @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( - initialSetup: { network in - // Delay for 10 seconds because we don't want the Poller to get stuck in a recursive loop - network - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } - .thenReturn( - MockNetwork.response(with: FileUploadResponse(id: "1")) - .delay(for: .seconds(10), scheduler: DispatchQueue.main) - .eraseToAnyPublisher() - ) - } - ) - @TestState(singleton: .appContext, in: dependencies) var mockAppContext: MockAppContext! = MockAppContext( - initialSetup: { context in - context.when { @MainActor in $0.isMainAppAndActive }.thenReturn(false) - } - ) - @TestState(defaults: .standard, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults() - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - cache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) - cache - .when { $0.ed25519Seed } - .thenReturn(Array(Array(Data(hex: TestConstants.edSecretKey)).prefix(upTo: 32))) - } - ) - @TestState(cache: .openGroupManager, in: dependencies) var mockOGMCache: MockOGMCache! = MockOGMCache( - initialSetup: { cache in - cache.when { $0.pendingChanges }.thenReturn([]) - cache.when { $0.getLastSuccessfulCommunityPollTimestamp() }.thenReturn(0) - } - ) - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( - initialSetup: { crypto in - crypto - .when { $0.generate(.hash(message: .any, key: .any, length: .any)) } - .thenReturn([]) - crypto - .when { $0.generate(.blinded15KeyPair(serverPublicKey: .any, ed25519SecretKey: .any)) } - .thenReturn( - KeyPair( - publicKey: Data(hex: TestConstants.publicKey).bytes, - secretKey: Data(hex: TestConstants.edSecretKey).bytes - ) - ) - crypto - .when { $0.generate(.signatureBlind15(message: .any, serverPublicKey: .any, ed25519SecretKey: .any)) } - .thenReturn("TestSogsSignature".bytes) - crypto - .when { $0.generate(.randomBytes(16)) } - .thenReturn(Array(Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!)) - crypto - .when { $0.generate(.ed25519KeyPair(seed: .any)) } - .thenReturn( - KeyPair( - publicKey: Array(Data(hex: TestConstants.edPublicKey)), - secretKey: Array(Data(hex: TestConstants.edSecretKey)) - ) - ) - } - ) - @TestState var cache: CommunityPoller.Cache! = CommunityPoller.Cache(using: dependencies) - - // MARK: - a CommunityPollerCache - describe("a CommunityPollerCache") { - // MARK: -- when starting polling - context("when starting polling") { - beforeEach { - mockAppContext.when { @MainActor in $0.isMainAppAndActive }.thenReturn(true) - } - - // MARK: ---- creates pollers for all of the communities - it("creates pollers for all of the communities") { - cache.startAllPollers() - - await expect(cache.serversBeingPolled).toEventually(equal(["testserver", "testserver1"])) - } - - // MARK: ---- updates the isPolling flag - it("updates the isPolling flag") { - cache.startAllPollers() - - await expect(cache.allPollers.count).toEventually(equal(2)) - try require(cache.allPollers.count).to(equal(2)) - expect(cache.allPollers[0].isPolling).to(beTrue()) - expect(cache.allPollers[1].isPolling).to(beTrue()) - } - - // MARK: ---- does not create additional pollers if it's already polling - it("does not create additional pollers if it's already polling") { - cache - .getOrCreatePoller(for: CommunityPoller.Info(server: "testserver", pollFailureCount: 0)) - .startIfNeeded() - cache - .getOrCreatePoller(for: CommunityPoller.Info(server: "testserver1", pollFailureCount: 0)) - .startIfNeeded() - - cache.startAllPollers() - - expect(cache.allPollers.count).to(equal(2)) - } - } - - // MARK: -- when stopping polling - context("when stopping polling") { - beforeEach { - cache.startAllPollers() - } - - // MARK: ---- removes all pollers - it("removes all pollers") { - cache.stopAndRemoveAllPollers() - - expect(cache.allPollers.count).to(equal(0)) - } - - // MARK: ---- updates the isPolling flag - it("updates the isPolling flag") { - let poller1 = cache.getOrCreatePoller(for: CommunityPoller.Info(server: "testserver", pollFailureCount: 0)) - let poller2 = cache.getOrCreatePoller(for: CommunityPoller.Info(server: "testserver1", pollFailureCount: 0)) - cache.stopAndRemoveAllPollers() - - expect(poller1.isPolling).to(beFalse()) - expect(poller2.isPolling).to(beFalse()) - } - } - } - } -} diff --git a/SessionMessagingKitTests/_TestUtilities/CustomArgSummaryDescribable+SMK.swift b/SessionMessagingKitTests/_TestUtilities/ArgumentDescribing+SMK.swift similarity index 94% rename from SessionMessagingKitTests/_TestUtilities/CustomArgSummaryDescribable+SMK.swift rename to SessionMessagingKitTests/_TestUtilities/ArgumentDescribing+SMK.swift index fa8fe05502..13e8a44e55 100644 --- a/SessionMessagingKitTests/_TestUtilities/CustomArgSummaryDescribable+SMK.swift +++ b/SessionMessagingKitTests/_TestUtilities/ArgumentDescribing+SMK.swift @@ -3,9 +3,10 @@ import Foundation import SessionMessagingKit import SessionUtilitiesKit +import TestUtilities -extension Job: CustomArgSummaryDescribable { - var customArgSummaryDescribable: String? { +extension Job: @retroactive ArgumentDescribing { + public var summary: String? { switch variant { case .attachmentUpload: guard diff --git a/SessionMessagingKitTests/_TestUtilities/CommonSMKMockExtensions.swift b/SessionMessagingKitTests/_TestUtilities/CommonSMKMockExtensions.swift deleted file mode 100644 index edda4ebd98..0000000000 --- a/SessionMessagingKitTests/_TestUtilities/CommonSMKMockExtensions.swift +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import SessionUtil -import SessionUIKit -import SessionUtilitiesKit - -@testable import SessionMessagingKit - -extension Message.Destination: Mocked { - static var mock: Message.Destination = .contact(publicKey: "") -} - -extension LibSession.Config: Mocked { - static var mock: LibSession.Config = { - var conf = config_object() - return withUnsafeMutablePointer(to: &conf) { .contacts($0) } - }() -} - -extension ConfigDump.Variant: Mocked { - static var mock: ConfigDump.Variant = .userProfile -} - -extension LibSession.CacheBehaviour: Mocked { - static var mock: LibSession.CacheBehaviour = .skipAutomaticConfigSync -} - -extension LibSession.OpenGroupUrlInfo: Mocked { - static var mock: LibSession.OpenGroupUrlInfo = LibSession.OpenGroupUrlInfo( - threadId: .mock, - server: .mock, - roomToken: .mock, - publicKey: .mock - ) -} - -extension ObservableKey: Mocked { - static var mock: ObservableKey = "mockObservableKey" -} - -extension SessionThread: Mocked { - static var mock: SessionThread = SessionThread( - id: .mock, - variant: .contact, - creationDateTimestamp: 0, - shouldBeVisible: false, - isPinned: false, - messageDraft: nil, - notificationSound: nil, - mutedUntilTimestamp: nil, - onlyNotifyForMentions: false, - markedAsUnread: nil, - pinnedPriority: nil - ) -} - -extension SessionThread.Variant: Mocked { - static var mock: SessionThread.Variant = .contact -} - -extension Interaction.Variant: Mocked { - static var mock: Interaction.Variant = .standardIncoming -} - -extension Interaction: Mocked { - static var mock: Interaction = Interaction( - id: 123456, - serverHash: .mock, - messageUuid: nil, - threadId: .mock, - authorId: .mock, - variant: .mock, - body: .mock, - timestampMs: 1234567890, - receivedAtTimestampMs: 1234567890, - wasRead: false, - hasMention: false, - expiresInSeconds: nil, - expiresStartedAtMs: nil, - linkPreviewUrl: nil, - openGroupServerMessageId: nil, - openGroupWhisper: false, - openGroupWhisperMods: false, - openGroupWhisperTo: nil, - state: .sent, - recipientReadTimestampMs: nil, - mostRecentFailureText: nil, - isProMessage: false - ) -} - -extension VisibleMessage: Mocked { - static var mock: VisibleMessage = VisibleMessage(text: "mock") -} - -extension KeychainStorage.DataKey: Mocked { - static var mock: KeychainStorage.DataKey = .dbCipherKeySpec -} - -extension NotificationCategory: Mocked { - static var mock: NotificationCategory = .incomingMessage -} - -extension NotificationContent: Mocked { - static var mock: NotificationContent = NotificationContent( - threadId: .mock, - threadVariant: .mock, - identifier: .mock, - category: .mock, - applicationState: .any - ) -} - -extension Preferences.NotificationSettings: Mocked { - static var mock: Preferences.NotificationSettings = Preferences.NotificationSettings( - previewType: .mock, - sound: .mock, - mentionsOnly: false, - mutedUntil: nil - ) -} - -extension ImageDataManager.DataSource: Mocked { - static var mock: ImageDataManager.DataSource = ImageDataManager.DataSource.data("mock", Data([1, 2, 3])) -} - -enum MockLibSessionConvertible: Int, Codable, LibSessionConvertibleEnum, Mocked { - typealias LibSessionType = Int - - static var mock: MockLibSessionConvertible = .mockValue - - case mockValue = 0 - - public static var defaultLibSessionValue: LibSessionType { 0 } - public var libSessionValue: LibSessionType { 0 } - - public init(_ libSessionValue: LibSessionType) { - self = .mockValue - } -} - -extension Preferences.Sound: Mocked { - static var mock: Preferences.Sound = .defaultNotificationSound -} - -extension Preferences.NotificationPreviewType: Mocked { - static var mock: Preferences.NotificationPreviewType = .defaultPreviewType -} - -extension Theme: Mocked { - static var mock: Theme = .defaultTheme -} - -extension Theme.PrimaryColor: Mocked { - static var mock: Theme.PrimaryColor = .defaultPrimaryColor -} - -extension ConfigDump: Mocked { - static var mock: ConfigDump = ConfigDump( - variant: .invalid, - sessionId: "", - data: Data(), - timestampMs: 1234567890 - ) -} diff --git a/SessionMessagingKitTests/_TestUtilities/MockCommunityPoller.swift b/SessionMessagingKitTests/_TestUtilities/MockCommunityPoller.swift deleted file mode 100644 index f13ea85853..0000000000 --- a/SessionMessagingKitTests/_TestUtilities/MockCommunityPoller.swift +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import Combine - -@testable import SessionMessagingKit - -class MockCommunityPoller: Mock, CommunityPollerType { - var isPolling: Bool { mock() } - var receivedPollResponse: AnyPublisher { mock() } - - func startIfNeeded() { mockNoReturn() } - func stop() { mockNoReturn() } -} diff --git a/SessionMessagingKitTests/_TestUtilities/MockCommunityPollerCache.swift b/SessionMessagingKitTests/_TestUtilities/MockCommunityPollerCache.swift index 415417abe1..7d2f2b05a2 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockCommunityPollerCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockCommunityPollerCache.swift @@ -1,15 +1,29 @@ // Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. import Foundation +import TestUtilities @testable import SessionMessagingKit -class MockCommunityPollerCache: Mock, CommunityPollerCacheType { - var serversBeingPolled: Set { mock() } - var allPollers: [CommunityPollerType] { mock() } - - func startAllPollers() { mockNoReturn() } - @discardableResult func getOrCreatePoller(for info: CommunityPoller.Info) -> CommunityPollerType { mock(args: [info]) } - func stopAndRemovePoller(for server: String) { mockNoReturn(args: [server]) } - func stopAndRemoveAllPollers() { mockNoReturn() } +class MockCommunityPollerManager: CommunityPollerManagerType, Mockable { + nonisolated let handler: MockHandler + + required init(handler: MockHandler) { + self.handler = handler + } + + required init(handlerForBuilder: any MockFunctionHandler) { + self.handler = MockHandler(forwardingHandler: handlerForBuilder) + } + + nonisolated var syncState: CommunityPollerManagerSyncState { handler.mock() } + var serversBeingPolled: Set { get async { handler.mock() } } + var allPollers: [any PollerType] { get async { handler.mock() } } + + func startAllPollers() async { handler.mockNoReturn() } + @discardableResult func getOrCreatePoller(for info: CommunityPoller.Info) async -> any PollerType { + handler.mock(args: [info]) + } + func stopAndRemovePoller(for server: String) async { handler.mockNoReturn(args: [server]) } + func stopAndRemoveAllPollers() async { handler.mockNoReturn() } } diff --git a/SessionMessagingKitTests/_TestUtilities/MockGroupPollerCache.swift b/SessionMessagingKitTests/_TestUtilities/MockGroupPollerCache.swift index a9238310e5..e2cef74a42 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockGroupPollerCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockGroupPollerCache.swift @@ -1,12 +1,25 @@ // Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. import Foundation +import TestUtilities @testable import SessionMessagingKit -class MockGroupPollerCache: Mock, GroupPollerCacheType { - func startAllPollers() { mockNoReturn() } - @discardableResult func getOrCreatePoller(for swarmPublicKey: String) -> SwarmPollerType { mock(args: [swarmPublicKey]) } - func stopAndRemovePoller(for swarmPublicKey: String) { mockNoReturn(args: [swarmPublicKey]) } - func stopAndRemoveAllPollers() { mockNoReturn() } +final class MockGroupPollerManager: GroupPollerManagerType, Mockable { + let handler: MockHandler + + required init(handler: MockHandler) { + self.handler = handler + } + + required init(handlerForBuilder: any MockFunctionHandler) { + self.handler = MockHandler(forwardingHandler: handlerForBuilder) + } + + func startAllPollers() { handler.mockNoReturn() } + @discardableResult func getOrCreatePoller(for swarmPublicKey: String) -> any PollerType { + return handler.mock(args: [swarmPublicKey]) + } + func stopAndRemovePoller(for swarmPublicKey: String) { handler.mockNoReturn(args: [swarmPublicKey]) } + func stopAndRemoveAllPollers() { handler.mockNoReturn() } } diff --git a/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift b/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift index d6d04cce23..e7da3d370f 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift @@ -15,8 +15,8 @@ class MockLibSessionCache: Mock, LibSessionCacheType { // MARK: - State Management - func loadState(_ db: ObservingDatabase, requestId: String?) { - mockNoReturn(args: [requestId], untrackedArgs: [db]) + func loadState(_ db: ObservingDatabase) { + mockNoReturn(untrackedArgs: [db]) } func loadDefaultStateFor( @@ -321,6 +321,10 @@ class MockLibSessionCache: Mock, LibSessionCacheType { func groupDeleteAttachmentsBefore(groupSessionId: SessionId) -> TimeInterval? { return mock(args: [groupSessionId]) } + + func authData(groupSessionId: SessionId) -> GroupAuthData { + return mock(args: [groupSessionId]) + } } // MARK: - Convenience @@ -459,5 +463,11 @@ extension Mock where T == LibSessionCacheType { self .when { $0.displayPictureUrl(threadId: .any, threadVariant: .any) } .thenReturn(nil) + self + .when { $0.authData(groupSessionId: .any) } + .thenReturn(GroupAuthData( + groupIdentityPrivateKey: Data([1, 2, 3]), + authData: nil + )) } } diff --git a/SessionMessagingKitTests/_TestUtilities/MockNotificationsManager.swift b/SessionMessagingKitTests/_TestUtilities/MockNotificationsManager.swift index d73ce6685d..427bf01912 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockNotificationsManager.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockNotificationsManager.swift @@ -13,7 +13,7 @@ public class MockNotificationsManager: Mock, Notificat mockNoReturn(untrackedArgs: [dependencies]) } - internal required init(functionHandler: MockFunctionHandler? = nil, initialSetup: ((Mock) -> ())? = nil) { + internal required init(functionHandler: MockFunctionHandler_Old? = nil, initialSetup: ((Mock) -> ())? = nil) { super.init(functionHandler: functionHandler, initialSetup: initialSetup) } @@ -41,7 +41,7 @@ public class MockNotificationsManager: Mock, Notificat public func notificationUserInfo( threadId: String, threadVariant: SessionThread.Variant - ) -> [String: Any] { + ) -> [String: AnyHashable] { return mock(args: [threadId, threadVariant]) } diff --git a/SessionMessagingKitTests/_TestUtilities/MockPoller.swift b/SessionMessagingKitTests/_TestUtilities/MockPoller.swift index 794fa81829..3d25aa2c6c 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockPoller.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockPoller.swift @@ -4,45 +4,41 @@ import Foundation import Combine import SessionNetworkingKit import SessionUtilitiesKit +import TestUtilities @testable import SessionMessagingKit -extension PollerDestination: Mocked { static var mock: PollerDestination { .swarm(TestConstants.publicKey) } } - -class MockPoller: Mock, PollerType { +actor MockPoller: PollerType, Mockable { typealias PollResponse = Void - var pollerQueue: DispatchQueue { DispatchQueue.main } - var pollerName: String { mock() } - var pollerDestination: PollerDestination { mock() } - var logStartAndStopCalls: Bool { mock() } - var receivedPollResponse: AnyPublisher { mock() } - var isPolling: Bool { - get { mock() } - set { mockNoReturn(args: [newValue]) } - } + nonisolated let handler: MockHandler + + var dependencies: Dependencies { handler.mock() } + var pollerName: String { handler.mock() } + var destination: PollerDestination { handler.mock() } + var logStartAndStopCalls: Bool { handler.mock() } + nonisolated var receivedPollResponse: AsyncStream { handler.mock() } var pollCount: Int { - get { mock() } - set { mockNoReturn(args: [newValue]) } + get { handler.mock() } + set { handler.mockNoReturn(args: [newValue]) } } var failureCount: Int { - get { mock() } - set { mockNoReturn(args: [newValue]) } + get { handler.mock() } + set { handler.mockNoReturn(args: [newValue]) } } var lastPollStart: TimeInterval { - get { mock() } - set { mockNoReturn(args: [newValue]) } + get { handler.mock() } + set { handler.mockNoReturn(args: [newValue]) } } - var cancellable: AnyCancellable? { - get { mock() } - set { mockNoReturn(args: [newValue]) } + var pollTask: Task? { + get { handler.mock() } + set { handler.mockNoReturn(args: [newValue]) } } - required init( + init( pollerName: String, - pollerQueue: DispatchQueue, - pollerDestination: PollerDestination, - pollerDrainBehaviour: ThreadSafeObject, + destination: PollerDestination, + swarmDrainStrategy: SwarmDrainer.Strategy, namespaces: [SnodeAPI.Namespace], failureCount: Int, shouldStoreMessages: Bool, @@ -50,33 +46,41 @@ class MockPoller: Mock, PollerType { customAuthMethod: (any AuthenticationMethod)?, using dependencies: Dependencies ) { - super.init() - - mockNoReturn( + handler = MockHandler(dummyProvider: { _ in MockPoller(handler: .invalid()) }) + handler.mockNoReturn( args: [ pollerName, - pollerQueue, - pollerDestination, - pollerDrainBehaviour, + destination, + swarmDrainStrategy, namespaces, failureCount, shouldStoreMessages, logStartAndStopCalls, customAuthMethod - ], - untrackedArgs: [dependencies] + ] ) } - internal required init(functionHandler: MockFunctionHandler? = nil, initialSetup: ((Mock) -> ())? = nil) { - super.init(functionHandler: functionHandler, initialSetup: initialSetup) + internal init(handler: MockHandler) { + self.handler = handler } - func startIfNeeded() { mockNoReturn() } - func stop() { mockNoReturn() } + internal init(handlerForBuilder: any MockFunctionHandler) { + self.handler = MockHandler(forwardingHandler: handlerForBuilder) + } - func pollerDidStart() { mockNoReturn() } - func poll(forceSynchronousProcessing: Bool) -> AnyPublisher { mock(args: [forceSynchronousProcessing]) } - func nextPollDelay() -> AnyPublisher { mock() } - func handlePollError(_ error: Error, _ lastError: Error?) -> PollerErrorResponse { mock(args: [error, lastError]) } + func startIfNeeded(forceStartInBackground: Bool) async { handler.mockNoReturn(args: [forceStartInBackground]) } + func stop() { handler.mockNoReturn() } + + func pollerDidStart() { handler.mockNoReturn() } + func pollerReceivedResponse(_ response: PollResponse) async { handler.mockNoReturn(args: [response]) } + func pollerDidStop() { handler.mockNoReturn() } + func poll(forceSynchronousProcessing: Bool) async throws -> PollResult { + return try handler.mockThrowing(args: [forceSynchronousProcessing]) + } + func pollFromBackground() async throws -> PollResult { + return try handler.mockThrowing() + } + func nextPollDelay() async -> TimeInterval { return handler.mock() } + func handlePollError(_ error: Error) async { handler.mockNoReturn(args: [error]) } } diff --git a/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift b/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift deleted file mode 100644 index d20f1da875..0000000000 --- a/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import Combine -import SessionNetworkingKit -import SessionUtilitiesKit - -@testable import SessionMessagingKit - -class MockSwarmPoller: Mock, SwarmPollerType & PollerType { - var pollerQueue: DispatchQueue { DispatchQueue.main } - var pollerName: String { mock() } - var pollerDestination: PollerDestination { mock() } - var logStartAndStopCalls: Bool { mock() } - var receivedPollResponse: AnyPublisher { mock() } - var isPolling: Bool { - get { mock() } - set { mockNoReturn(args: [newValue]) } - } - var pollCount: Int { - get { mock() } - set { mockNoReturn(args: [newValue]) } - } - var failureCount: Int { - get { mock() } - set { mockNoReturn(args: [newValue]) } - } - var lastPollStart: TimeInterval { - get { mock() } - set { mockNoReturn(args: [newValue]) } - } - var cancellable: AnyCancellable? { - get { mock() } - set { mockNoReturn(args: [newValue]) } - } - - required init( - pollerName: String, - pollerQueue: DispatchQueue, - pollerDestination: PollerDestination, - pollerDrainBehaviour: ThreadSafeObject, - namespaces: [SnodeAPI.Namespace], - failureCount: Int, - shouldStoreMessages: Bool, - logStartAndStopCalls: Bool, - customAuthMethod: (any AuthenticationMethod)?, - using dependencies: Dependencies - ) { - super.init() - - mockNoReturn( - args: [ - pollerName, - pollerQueue, - pollerDestination, - pollerDrainBehaviour, - namespaces, - failureCount, - shouldStoreMessages, - logStartAndStopCalls, - customAuthMethod - ], - untrackedArgs: [dependencies] - ) - } - - internal required init(functionHandler: MockFunctionHandler? = nil, initialSetup: ((Mock) -> ())? = nil) { - super.init(functionHandler: functionHandler, initialSetup: initialSetup) - } - - func startIfNeeded() { mockNoReturn() } - func stop() { mockNoReturn() } - - func pollerDidStart() { mockNoReturn() } - func poll(forceSynchronousProcessing: Bool) -> AnyPublisher { mock(args: [forceSynchronousProcessing]) } - func nextPollDelay() -> AnyPublisher { mock() } - func handlePollError(_ error: Error, _ lastError: Error?) -> PollerErrorResponse { mock(args: [error, lastError]) } -} diff --git a/SessionMessagingKitTests/_TestUtilities/Mocked+SMK.swift b/SessionMessagingKitTests/_TestUtilities/Mocked+SMK.swift new file mode 100644 index 0000000000..3df3b53ee9 --- /dev/null +++ b/SessionMessagingKitTests/_TestUtilities/Mocked+SMK.swift @@ -0,0 +1,247 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtil +import SessionUIKit +import SessionUtilitiesKit +import TestUtilities + +@testable import SessionMessagingKit + +extension Message.Destination: @retroactive Mocked { + public static var any: Message.Destination = .contact(publicKey: String.any) + public static var mock: Message.Destination = .contact(publicKey: "") +} + +extension LibSession.Config: @retroactive Mocked { + public static var any: LibSession.Config = .mock + public static var mock: LibSession.Config = { + var conf = config_object() + return withUnsafeMutablePointer(to: &conf) { .contacts($0) } + }() +} + +extension ConfigDump.Variant: @retroactive Mocked { + public static var any: ConfigDump.Variant = .local + public static var mock: ConfigDump.Variant = .userProfile +} + +extension LibSession.CacheBehaviour: @retroactive Mocked { + public static var any: LibSession.CacheBehaviour = .skipAutomaticConfigSync + public static var mock: LibSession.CacheBehaviour = .skipAutomaticConfigSync +} + +extension LibSession.OpenGroupUrlInfo: @retroactive Mocked { + public static var any: LibSession.OpenGroupUrlInfo = LibSession.OpenGroupUrlInfo( + threadId: .any, + server: .any, + roomToken: .any, + publicKey: .any + ) + public static var mock: LibSession.OpenGroupUrlInfo = LibSession.OpenGroupUrlInfo( + threadId: .mock, + server: .mock, + roomToken: .mock, + publicKey: .mock + ) +} + +extension SessionThread: @retroactive Mocked { + public static var any: SessionThread = SessionThread( + id: .any, + variant: .any, + creationDateTimestamp: .any, + shouldBeVisible: .any, + isPinned: .any, + messageDraft: .any, + notificationSound: .any, + mutedUntilTimestamp: .any, + onlyNotifyForMentions: .any, + markedAsUnread: .any, + pinnedPriority: .any + ) + + public static var mock: SessionThread = SessionThread( + id: .mock, + variant: .mock, + creationDateTimestamp: .mock, + shouldBeVisible: .mock, + isPinned: .mock, + messageDraft: .mock, + notificationSound: .mock, + mutedUntilTimestamp: .mock, + onlyNotifyForMentions: .mock, + markedAsUnread: .mock, + pinnedPriority: .mock + ) +} + +extension SessionThread.Variant: @retroactive Mocked { + public static var any: SessionThread.Variant = .contact + public static var mock: SessionThread.Variant = .contact +} + +extension Interaction.Variant: @retroactive Mocked { + public static var any: Interaction.Variant = ._legacyStandardIncomingDeleted + public static var mock: Interaction.Variant = .standardIncoming +} + +extension Interaction: @retroactive Mocked { + public static var any: Interaction = Interaction( + id: .any, + serverHash: .any, + messageUuid: .any, + threadId: .any, + authorId: .any, + variant: .any, + body: .any, + timestampMs: .any, + receivedAtTimestampMs: .any, + wasRead: .any, + hasMention: .any, + expiresInSeconds: .any, + expiresStartedAtMs: .any, + linkPreviewUrl: .any, + openGroupServerMessageId: .any, + openGroupWhisper: .any, + openGroupWhisperMods: .any, + openGroupWhisperTo: .any, + state: .sent, + recipientReadTimestampMs: .any, + mostRecentFailureText: .any, + isProMessage: .any + ) + public static var mock: Interaction = Interaction( + id: 123456, + serverHash: .mock, + messageUuid: nil, + threadId: .mock, + authorId: .mock, + variant: .mock, + body: .mock, + timestampMs: 1234567890, + receivedAtTimestampMs: 1234567890, + wasRead: false, + hasMention: false, + expiresInSeconds: nil, + expiresStartedAtMs: nil, + linkPreviewUrl: nil, + openGroupServerMessageId: nil, + openGroupWhisper: false, + openGroupWhisperMods: false, + openGroupWhisperTo: nil, + state: .sent, + recipientReadTimestampMs: nil, + mostRecentFailureText: nil, + isProMessage: false + ) +} + +extension VisibleMessage: @retroactive Mocked { + public static var any: VisibleMessage = VisibleMessage(text: .any) + public static var mock: VisibleMessage = VisibleMessage(text: "mock") +} + +extension KeychainStorage.DataKey: @retroactive Mocked { + public static var any: KeychainStorage.DataKey = "__MOCKED_KEYCHAIN_DATE_KEY_VALUE__" + public static var mock: KeychainStorage.DataKey = .dbCipherKeySpec +} + +extension NotificationCategory: @retroactive Mocked { + public static var any: NotificationCategory = .threadlessErrorMessage + public static var mock: NotificationCategory = .incomingMessage +} + +extension NotificationContent: @retroactive Mocked { + public static var any: NotificationContent = NotificationContent( + threadId: .any, + threadVariant: .any, + identifier: .any, + category: .any, + applicationState: .any + ) + public static var mock: NotificationContent = NotificationContent( + threadId: .mock, + threadVariant: .mock, + identifier: .mock, + category: .mock, + applicationState: .mock + ) +} + +extension Preferences.NotificationSettings: @retroactive Mocked { + public static var any: Preferences.NotificationSettings = Preferences.NotificationSettings( + previewType: .any, + sound: .any, + mentionsOnly: .any, + mutedUntil: .any + ) + public static var mock: Preferences.NotificationSettings = Preferences.NotificationSettings( + previewType: .mock, + sound: .mock, + mentionsOnly: .mock, + mutedUntil: .mock + ) +} + +extension ImageDataManager.DataSource: @retroactive Mocked { + public static var any: ImageDataManager.DataSource = ImageDataManager.DataSource.data(.any, .any) + public static var mock: ImageDataManager.DataSource = ImageDataManager.DataSource.data(.mock, .mock) +} + +enum MockLibSessionConvertible: Int, Codable, LibSessionConvertibleEnum, Mocked { + typealias LibSessionType = Int + + public static var any: MockLibSessionConvertible = .anyValue + public static var mock: MockLibSessionConvertible = .mockValue + + case anyValue = 12345554321 + case mockValue = 0 + + public static var defaultLibSessionValue: LibSessionType { 0 } + public var libSessionValue: LibSessionType { 0 } + + public init(_ libSessionValue: LibSessionType) { + self = .mockValue + } +} + +extension Preferences.Sound: @retroactive Mocked { + public static var any: Preferences.Sound = .callFailure + public static var mock: Preferences.Sound = .defaultNotificationSound +} + +extension Preferences.NotificationPreviewType: @retroactive Mocked { + public static var any: Preferences.NotificationPreviewType = .noNameNoPreview + public static var mock: Preferences.NotificationPreviewType = .defaultPreviewType +} + +extension Theme: @retroactive Mocked { + public static var any: Theme = .classicLight + public static var mock: Theme = .defaultTheme +} + +extension Theme.PrimaryColor: @retroactive Mocked { + public static var any: Theme.PrimaryColor = .yellow + public static var mock: Theme.PrimaryColor = .defaultPrimaryColor +} + +extension ConfigDump: @retroactive Mocked { + public static var any: ConfigDump = ConfigDump( + variant: .invalid, + sessionId: .any, + data: .any, + timestampMs: .any + ) + public static var mock: ConfigDump = ConfigDump( + variant: .invalid, + sessionId: .mock, + data: .mock, + timestampMs: .mock + ) +} + +extension PollerDestination: @retroactive Mocked { + public static var any: PollerDestination { .swarm(.any) } + public static var mock: PollerDestination { .swarm(TestConstants.publicKey) } +} diff --git a/SessionNetworkingKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift index 2c04405a38..f6dc6c230a 100644 --- a/SessionNetworkingKit/LibSession/LibSession+Networking.swift +++ b/SessionNetworkingKit/LibSession/LibSession+Networking.swift @@ -195,8 +195,8 @@ actor LibSessionNetwork: NetworkType { return nodes } - nonisolated func send( - endpoint: (any EndpointType), + nonisolated func send( + endpoint: E, destination: Network.Destination, body: Data?, category: Network.RequestCategory, @@ -332,8 +332,8 @@ actor LibSessionNetwork: NetworkType { .eraseToAnyPublisher() } - func send( - endpoint: (any EndpointType), + func send( + endpoint: E, destination: Network.Destination, body: Data?, category: Network.RequestCategory, @@ -1097,8 +1097,8 @@ public extension LibSession { public func getSwarm(for swarmPublicKey: String) async throws -> Set { return [] } public func getRandomNodes(count: Int) async throws -> Set { return [] } - nonisolated public func send( - endpoint: (any EndpointType), + nonisolated public func send( + endpoint: E, destination: Network.Destination, body: Data?, category: Network.RequestCategory, @@ -1108,8 +1108,8 @@ public extension LibSession { return Fail(error: NetworkError.invalidState).eraseToAnyPublisher() } - public func send( - endpoint: (any EndpointType), + public func send( + endpoint: E, destination: Network.Destination, body: Data?, category: Network.RequestCategory, diff --git a/SessionNetworkingKit/Models/DeleteAllBeforeRequest.swift b/SessionNetworkingKit/Models/DeleteAllBeforeRequest.swift deleted file mode 100644 index 6e6e1e64af..0000000000 --- a/SessionNetworkingKit/Models/DeleteAllBeforeRequest.swift +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import Foundation -import SessionUtilitiesKit - -extension SnodeAPI { - public final class DeleteAllBeforeRequest: SnodeAuthenticatedRequestBody, UpdatableTimestamp { - enum CodingKeys: String, CodingKey { - case beforeMs = "before" - case namespace - } - - let beforeMs: UInt64 - let namespace: SnodeAPI.Namespace? - - override var verificationBytes: [UInt8] { - /// Ed25519 signature of `("delete_before" || namespace || before)`, signed by - /// `pubkey`. Must be base64 encoded (json) or bytes (OMQ). `namespace` is the stringified - /// version of the given non-default namespace parameter (i.e. "-42" or "all"), or the empty - /// string for the default namespace (whether explicitly given or not). - SnodeAPI.Endpoint.deleteAllBefore.path.bytes - .appending( - contentsOf: (namespace == nil ? - "all" : - namespace?.verificationString - )?.bytes - ) - .appending(contentsOf: "\(beforeMs)".data(using: .ascii)?.bytes) - } - - // MARK: - Init - - public init( - beforeMs: UInt64, - namespace: SnodeAPI.Namespace?, - authMethod: AuthenticationMethod, - timestampMs: UInt64 - ) { - self.beforeMs = beforeMs - self.namespace = namespace - - super.init( - authMethod: authMethod, - timestampMs: timestampMs - ) - } - - // MARK: - Coding - - override public func encode(to encoder: Encoder) throws { - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(beforeMs, forKey: .beforeMs) - - // If no namespace is specified it defaults to the default namespace only (namespace - // 0), so instead in this case we want to explicitly delete from `all` namespaces - switch namespace { - case .some(let namespace): try container.encode(namespace, forKey: .namespace) - case .none: try container.encode("all", forKey: .namespace) - } - - try super.encode(to: encoder) - } - - // MARK: - UpdatableTimestamp - - public func with(timestampMs: UInt64) -> DeleteAllBeforeRequest { - return DeleteAllBeforeRequest( - beforeMs: self.beforeMs, - namespace: self.namespace, - authMethod: self.authMethod, - timestampMs: timestampMs - ) - } - } -} diff --git a/SessionNetworkingKit/Models/DeleteAllBeforeResponse.swift b/SessionNetworkingKit/Models/DeleteAllBeforeResponse.swift deleted file mode 100644 index e55052a26d..0000000000 --- a/SessionNetworkingKit/Models/DeleteAllBeforeResponse.swift +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import SessionUtilitiesKit - -public class DeleteAllBeforeResponse: SnodeRecursiveResponse {} - -// MARK: - ValidatableResponse - -extension DeleteAllBeforeResponse: ValidatableResponse { - typealias ValidationData = UInt64 - typealias ValidationResponse = Bool - - /// Just one response in the swarm must be valid - internal static var requiredSuccessfulResponses: Int { 1 } - - internal func validResultMap( - swarmPublicKey: String, - validationData: UInt64, - using dependencies: Dependencies - ) throws -> [String: Bool] { - let validationMap: [String: Bool] = swarm.reduce(into: [:]) { result, next in - guard - !next.value.failed, - let signatureBase64: String = next.value.signatureBase64, - let encodedSignature: Data = Data(base64Encoded: signatureBase64) - else { - result[next.key] = false - - if let reason: String = next.value.reason, let statusCode: Int = next.value.code { - Log.warn(.validator(self), "Couldn't delete data from: \(next.key) due to error: \(reason) (\(statusCode)).") - } - else { - Log.warn(.validator(self), "Couldn't delete data from: \(next.key).") - } - return - } - - /// Signature of `( PUBKEY_HEX || BEFORE || DELETEDHASH[0] || ... || DELETEDHASH[N] )` - /// signed by the node's ed25519 pubkey. When doing a multi-namespace delete the `DELETEDHASH` - /// values are totally ordered (i.e. among all the hashes deleted regardless of namespace) - let verificationBytes: [UInt8] = swarmPublicKey.bytes - .appending(contentsOf: "\(validationData)".data(using: .ascii)?.bytes) - .appending(contentsOf: next.value.deleted.joined().bytes) - - result[next.key] = dependencies[singleton: .crypto].verify( - .signature( - message: verificationBytes, - publicKey: Data(hex: next.key).bytes, - signature: encodedSignature.bytes - ) - ) - } - - return try Self.validated(map: validationMap) - } -} diff --git a/SessionNetworkingKit/Models/DeleteAllMessagesRequest.swift b/SessionNetworkingKit/Models/DeleteAllMessagesRequest.swift index 57f0c28f2b..9ea6935e48 100644 --- a/SessionNetworkingKit/Models/DeleteAllMessagesRequest.swift +++ b/SessionNetworkingKit/Models/DeleteAllMessagesRequest.swift @@ -4,7 +4,7 @@ import Foundation import SessionUtilitiesKit extension SnodeAPI { - public final class DeleteAllMessagesRequest: SnodeAuthenticatedRequestBody, UpdatableTimestamp { + public final class DeleteAllMessagesRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case namespace } @@ -55,15 +55,5 @@ extension SnodeAPI { try super.encode(to: encoder) } - - // MARK: - UpdatableTimestamp - - public func with(timestampMs: UInt64) -> DeleteAllMessagesRequest { - return DeleteAllMessagesRequest( - namespace: self.namespace, - authMethod: self.authMethod, - timestampMs: timestampMs - ) - } } } diff --git a/SessionNetworkingKit/Models/SnodeBatchRequest.swift b/SessionNetworkingKit/Models/SnodeBatchRequest.swift deleted file mode 100644 index e6a05c0c19..0000000000 --- a/SessionNetworkingKit/Models/SnodeBatchRequest.swift +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import SessionUtilitiesKit - -internal extension SnodeAPI { - struct BatchRequest: Encodable { - let requests: [Child] - - init(requests: [Info]) { - self.requests = requests.map { $0.child } - } - - // MARK: - BatchRequest.Info - - struct Info { - public let responseType: Decodable.Type - fileprivate let child: Child - - public init(request: SnodeRequest, responseType: R.Type) { - self.child = Child(request: request) - self.responseType = Network.BatchSubResponse.self - } - - public init(request: SnodeRequest) { - self.init( - request: request, - responseType: NoResponse.self - ) - } - } - - // MARK: - BatchRequest.Child - - struct Child: Encodable { - enum CodingKeys: String, CodingKey { - case method - case params - } - - let endpoint: SnodeAPI.Endpoint - - /// The `jsonBodyEncoder` is used to avoid having to make `BatchSubRequest` a generic type (haven't found - /// a good way to keep `BatchSubRequest` encodable using protocols unfortunately so need this work around) - private let jsonBodyEncoder: ((inout KeyedEncodingContainer, CodingKeys) throws -> ())? - - init(request: SnodeRequest) { - self.endpoint = request.endpoint - - self.jsonBodyEncoder = { [body = request.body] container, key in - try container.encode(body, forKey: key) - } - } - - public func encode(to encoder: Encoder) throws { - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(endpoint.path, forKey: .method) - try jsonBodyEncoder?(&container, .params) - } - } - } -} diff --git a/SessionNetworkingKit/Models/SnodeRequest.swift b/SessionNetworkingKit/Models/SnodeRequest.swift deleted file mode 100644 index ab23b427b3..0000000000 --- a/SessionNetworkingKit/Models/SnodeRequest.swift +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import SessionUtilitiesKit - -public struct SnodeRequest: Encodable { - private enum CodingKeys: String, CodingKey { - case method - case body = "params" - } - - internal let endpoint: SnodeAPI.Endpoint - internal let body: T - - // MARK: - Initialization - - public init( - endpoint: SnodeAPI.Endpoint, - body: T - ) { - self.endpoint = endpoint - self.body = body - } - - // MARK: - Codable - - public func encode(to encoder: Encoder) throws { - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(endpoint.path, forKey: .method) - try container.encode(body, forKey: .body) - } -} - -// MARK: - BatchRequestChildRetrievable - -extension SnodeRequest: BatchRequestChildRetrievable where T: BatchRequestChildRetrievable { - public var requests: [Network.BatchRequest.Child] { body.requests } -} - -// MARK: - UpdatableTimestamp - -extension SnodeRequest: UpdatableTimestamp where T: UpdatableTimestamp { - public func with(timestampMs: UInt64) -> SnodeRequest { - return SnodeRequest( - endpoint: self.endpoint, - body: self.body.with(timestampMs: timestampMs) - ) - } -} diff --git a/SessionNetworkingKit/Types/Destination.swift b/SessionNetworkingKit/Types/Destination.swift index 3037fe7c9c..8595463d2a 100644 --- a/SessionNetworkingKit/Types/Destination.swift +++ b/SessionNetworkingKit/Types/Destination.swift @@ -153,7 +153,7 @@ public extension Network { queryParameters: [HTTPQueryParam: String] = [:], headers: [HTTPHeader: String] = [:], x25519PublicKey: String - ) throws -> Destination { + ) -> Destination { return .server(info: ServerInfo( method: method, server: server, @@ -169,7 +169,7 @@ public extension Network { headers: [HTTPHeader: String] = [:], x25519PublicKey: String, fileName: String? - ) throws -> Destination { + ) -> Destination { return .serverUpload( info: ServerInfo( method: .post, @@ -188,7 +188,7 @@ public extension Network { headers: [HTTPHeader: String] = [:], x25519PublicKey: String, fileName: String? - ) throws -> Destination { + ) -> Destination { return .serverDownload(info: ServerInfo( method: .get, url: url, diff --git a/SessionNetworkingKit/Types/Network.swift b/SessionNetworkingKit/Types/Network.swift index d4bf6c654f..2f01ab586f 100644 --- a/SessionNetworkingKit/Types/Network.swift +++ b/SessionNetworkingKit/Types/Network.swift @@ -28,8 +28,8 @@ public protocol NetworkType { func getRandomNodes(count: Int) async throws -> Set @available(*, deprecated, message: "We want to shift from Combine to Async/Await when possible") - nonisolated func send( - endpoint: (any EndpointType), + nonisolated func send( + endpoint: E, destination: Network.Destination, body: Data?, category: Network.RequestCategory, @@ -37,8 +37,8 @@ public protocol NetworkType { overallTimeout: TimeInterval? ) -> AnyPublisher<(ResponseInfoType, Data?), Error> - func send( - endpoint: (any EndpointType), + func send( + endpoint: E, destination: Network.Destination, body: Data?, category: Network.RequestCategory, @@ -122,10 +122,10 @@ public extension Network { } enum FileServer { - fileprivate static let fileServer = "http://filev2.getsession.org" - fileprivate static let fileServerPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59" - fileprivate static let legacyFileServer = "http://88.99.175.227" - fileprivate static let legacyFileServerPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" + internal static let fileServer = "http://filev2.getsession.org" + internal static let fileServerPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59" + internal static let legacyFileServer = "http://88.99.175.227" + internal static let legacyFileServerPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" public enum Endpoint: EndpointType { case file diff --git a/SessionNetworkingKit/Types/UpdatableTimestamp.swift b/SessionNetworkingKit/Types/UpdatableTimestamp.swift deleted file mode 100644 index 4847a9445f..0000000000 --- a/SessionNetworkingKit/Types/UpdatableTimestamp.swift +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -public protocol UpdatableTimestamp { - func with(timestampMs: UInt64) -> Self -} diff --git a/SessionNetworkingKitTests/Models/SnodeRequestSpec.swift b/SessionNetworkingKitTests/Models/SnodeRequestSpec.swift deleted file mode 100644 index 0405d71e7c..0000000000 --- a/SessionNetworkingKitTests/Models/SnodeRequestSpec.swift +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import Combine -import SessionUtilitiesKit - -import Quick -import Nimble - -@testable import SessionNetworkingKit - -class SnodeRequestSpec: QuickSpec { - override class func spec() { - // MARK: Configuration - - @TestState var dependencies: TestDependencies! = TestDependencies() - @TestState var batchRequest: Network.BatchRequest! - - // MARK: - a SnodeRequest - describe("a SnodeRequest") { - // MARK: -- when encoding a Network.BatchRequest storage server type endpoint - context("when encoding a Network.BatchRequest storage server type endpoint") { - // MARK: ---- successfully encodes a SnodeRequest body - it("successfully encodes a SnodeRequest body") { - batchRequest = Network.BatchRequest( - requestsKey: .requests, - requests: [ - try! Network.PreparedRequest( - request: Request, TestEndpoint>( - endpoint: .endpoint, - destination: try! .server( - method: .post, - server: "testServer", - x25519PublicKey: "05\(TestConstants.publicKey)" - ), - body: SnodeRequest( - endpoint: .sendMessage, - body: TestType(stringValue: "testValue") - ) - ), - responseType: NoResponse.self, - requestTimeout: 0, - using: dependencies - ) - ] - ) - - let requestData: Data? = try? JSONEncoder().encode(batchRequest) - let requestJson: [String: [[String: Any]]]? = requestData - .map { try? JSONSerialization.jsonObject(with: $0) as? [String: [[String: Any]]] } - let request: [String: Any]? = requestJson?["requests"]?.first - expect(request?["method"] as? String).to(equal("store")) - expect(request?["params"] as? [String: String]).to(equal(["stringValue": "testValue"])) - } - } - } - } -} - -// MARK: - Test Types - -fileprivate enum TestEndpoint: EndpointType { - case endpoint - - static var name: String { "TestEndpoint" } - static var batchRequestVariant: Network.BatchRequest.Child.Variant { .storageServer } - static var excludedSubRequestHeaders: [HTTPHeader] { [] } - - var path: String { return "endpoint" } -} - -fileprivate struct TestType: Codable, Equatable { - let stringValue: String -} diff --git a/SessionNetworkingKitTests/Types/BatchRequestSpec.swift b/SessionNetworkingKitTests/Types/BatchRequestSpec.swift index 05554a2be1..ccdb6d5e42 100644 --- a/SessionNetworkingKitTests/Types/BatchRequestSpec.swift +++ b/SessionNetworkingKitTests/Types/BatchRequestSpec.swift @@ -24,7 +24,7 @@ class BatchRequestSpec: QuickSpec { it("correctly strips specified headers from sub requests") { let httpRequest: Request = try! Request( endpoint: .endpoint1, - destination: try! .server( + destination: .server( server: "testServer", queryParameters: [:], headers: [ @@ -33,7 +33,8 @@ class BatchRequestSpec: QuickSpec { ], x25519PublicKey: "05\(TestConstants.publicKey)" ), - body: nil + body: nil, + requestTimeout: 0 ) request = Network.BatchRequest( @@ -41,7 +42,6 @@ class BatchRequestSpec: QuickSpec { try! Network.PreparedRequest( request: httpRequest, responseType: NoResponse.self, - requestTimeout: 0, using: dependencies ) ] @@ -60,7 +60,7 @@ class BatchRequestSpec: QuickSpec { it("does not strip unspecified headers from sub requests") { let httpRequest: Request = try! Request( endpoint: .endpoint1, - destination: try! .server( + destination: .server( server: "testServer", queryParameters: [:], headers: [ @@ -69,14 +69,14 @@ class BatchRequestSpec: QuickSpec { ], x25519PublicKey: "05\(TestConstants.publicKey)" ), - body: nil + body: nil, + requestTimeout: 0 ) request = Network.BatchRequest( requests: [ try! Network.PreparedRequest( request: httpRequest, responseType: NoResponse.self, - requestTimeout: 0, using: dependencies ) ] @@ -99,16 +99,16 @@ class BatchRequestSpec: QuickSpec { try! Network.PreparedRequest( request: Request( endpoint: .endpoint1, - destination: try! .server( + destination: .server( server: "testServer", queryParameters: [:], headers: [:], x25519PublicKey: "05\(TestConstants.publicKey)" ), - body: "testBody" + body: "testBody", + requestTimeout: 0 ), responseType: NoResponse.self, - requestTimeout: 0, using: dependencies ) ] @@ -129,16 +129,16 @@ class BatchRequestSpec: QuickSpec { try! Network.PreparedRequest( request: Request<[UInt8], TestEndpoint1>( endpoint: .endpoint1, - destination: try! .server( + destination: .server( server: "testServer", queryParameters: [:], headers: [:], x25519PublicKey: "05\(TestConstants.publicKey)" ), - body: [1, 2, 3] + body: [1, 2, 3], + requestTimeout: 0 ), responseType: NoResponse.self, - requestTimeout: 0, using: dependencies ) ] @@ -159,16 +159,16 @@ class BatchRequestSpec: QuickSpec { try! Network.PreparedRequest( request: Request( endpoint: .endpoint1, - destination: try! .server( + destination: .server( server: "testServer", queryParameters: [:], headers: [:], x25519PublicKey: "05\(TestConstants.publicKey)" ), - body: TestType(stringValue: "testValue") + body: TestType(stringValue: "testValue"), + requestTimeout: 0 ), responseType: NoResponse.self, - requestTimeout: 0, using: dependencies ) ] @@ -193,16 +193,16 @@ class BatchRequestSpec: QuickSpec { try! Network.PreparedRequest( request: Request( endpoint: .endpoint2, - destination: try! .server( + destination: .server( server: "testServer", queryParameters: [:], headers: [:], x25519PublicKey: "05\(TestConstants.publicKey)" ), - body: "TestMessage".data(using: .utf8)!.base64EncodedString() + body: "TestMessage".data(using: .utf8)!.base64EncodedString(), + requestTimeout: 0 ), responseType: NoResponse.self, - requestTimeout: 0, using: dependencies ) ] @@ -213,7 +213,7 @@ class BatchRequestSpec: QuickSpec { .map { try? JSONSerialization.jsonObject(with: $0) as? [String: [[String: Any]]] } let requests: [[String: Any]]? = requestJson?["requests"] expect(requests?.count).to(equal(1)) - expect(requests?.first?.count).to(equal(0)) + expect(requests?.first?["method"] as? String).to(equal("endpoint2")) } // MARK: ---- ignores a byte body @@ -224,16 +224,16 @@ class BatchRequestSpec: QuickSpec { try! Network.PreparedRequest( request: Request<[UInt8], TestEndpoint2>( endpoint: .endpoint2, - destination: try! .server( + destination: .server( server: "testServer", queryParameters: [:], headers: [:], x25519PublicKey: "05\(TestConstants.publicKey)" ), - body: [1, 2, 3] + body: [1, 2, 3], + requestTimeout: 0 ), responseType: NoResponse.self, - requestTimeout: 0, using: dependencies ) ] @@ -244,7 +244,7 @@ class BatchRequestSpec: QuickSpec { .map { try? JSONSerialization.jsonObject(with: $0) as? [String: [[String: Any]]] } let requests: [[String: Any]]? = requestJson?["requests"] expect(requests?.count).to(equal(1)) - expect(requests?.first?.count).to(equal(0)) + expect(requests?.first?["method"] as? String).to(equal("endpoint2")) } // MARK: ---- successfully encodes a JSON body @@ -255,16 +255,16 @@ class BatchRequestSpec: QuickSpec { try! Network.PreparedRequest( request: Request( endpoint: .endpoint2, - destination: try! .server( + destination: .server( server: "testServer", queryParameters: [:], headers: [:], x25519PublicKey: "05\(TestConstants.publicKey)" ), - body: TestType(stringValue: "testValue") + body: TestType(stringValue: "testValue"), + requestTimeout: 0 ), responseType: NoResponse.self, - requestTimeout: 0, using: dependencies ) ] @@ -275,7 +275,8 @@ class BatchRequestSpec: QuickSpec { .map { try? JSONSerialization.jsonObject(with: $0) as? [String: [[String: Any]]] } let requests: [[String: Any]]? = requestJson?["requests"] expect(requests?.count).to(equal(1)) - expect(requests?.first as? [String: String]) + expect(requests?.first?["method"] as? String).to(equal("endpoint2")) + expect(requests?.first?["params"] as? [String: String]) .to(equal(["stringValue": "testValue"])) } } diff --git a/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift index b62d147f7a..94f7f59ac6 100644 --- a/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift +++ b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift @@ -3,6 +3,7 @@ import Foundation import Combine import SessionUtilitiesKit +import TestUtilities import Quick import Nimble @@ -20,7 +21,7 @@ class PreparedRequestSendingSpec: QuickSpec { @TestState var preparedRequest: Network.PreparedRequest! = { let request = try! Request( endpoint: .endpoint1, - destination: try! .server( + destination: .server( method: .post, server: "testServer", x25519PublicKey: "" @@ -109,25 +110,11 @@ class PreparedRequestSendingSpec: QuickSpec { // MARK: ---- and handling events context("and handling events") { - @TestState var didReceiveSubscription: Bool! = false - @TestState var didReceiveCancel: Bool! = false @TestState var receivedOutput: (ResponseInfoType, Int)? = nil @TestState var receivedCompletion: Subscribers.Completion? = nil - @TestState var multiDidReceiveSubscription: [Bool]! = [] + @TestState var multiReceivedOutput: [(ResponseInfoType, Int)]! = [] @TestState var multiReceivedCompletion: [Subscribers.Completion]! = [] - // MARK: ------ calls receiveSubscription correctly - it("calls receiveSubscription correctly") { - preparedRequest - .handleEvents( - receiveSubscription: { didReceiveSubscription = true } - ) - .send(using: dependencies) - .sinkAndStore(in: &disposables) - - expect(didReceiveSubscription).to(beTrue()) - } - // MARK: ------ calls receiveOutput correctly it("calls receiveOutput correctly") { preparedRequest @@ -152,32 +139,17 @@ class PreparedRequestSendingSpec: QuickSpec { expect(receivedCompletion).toNot(beNil()) } - // MARK: ------ calls receiveCancel correctly - it("calls receiveCancel correctly") { - preparedRequest - .handleEvents( - receiveCancel: { didReceiveCancel = true } - ) - .send(using: dependencies) - .handleEvents( - receiveSubscription: { $0.cancel() } - ) - .sinkAndStore(in: &disposables) - - expect(didReceiveCancel).to(beTrue()) - } - // MARK: ------ calls multiple callbacks without issue it("calls multiple callbacks without issue") { preparedRequest .handleEvents( - receiveSubscription: { didReceiveSubscription = true }, + receiveOutput: { info, output in receivedOutput = (info, output) }, receiveCompletion: { result in receivedCompletion = result } ) .send(using: dependencies) .sinkAndStore(in: &disposables) - expect(didReceiveSubscription).to(beTrue()) + expect(receivedOutput).toNot(beNil()) expect(receivedCompletion).toNot(beNil()) } @@ -185,21 +157,21 @@ class PreparedRequestSendingSpec: QuickSpec { it("supports multiple handleEvents calls") { preparedRequest .handleEvents( - receiveSubscription: { multiDidReceiveSubscription.append(true) }, + receiveOutput: { info, output in multiReceivedOutput.append((info, output)) }, receiveCompletion: { result in multiReceivedCompletion.append(result) } ) .handleEvents( - receiveSubscription: { multiDidReceiveSubscription.append(true) }, + receiveOutput: { info, output in multiReceivedOutput.append((info, output)) }, receiveCompletion: { result in multiReceivedCompletion.append(result) } ) .handleEvents( - receiveSubscription: { multiDidReceiveSubscription.append(true) }, + receiveOutput: { info, output in multiReceivedOutput.append((info, output)) }, receiveCompletion: { result in multiReceivedCompletion.append(result) } ) .send(using: dependencies) .sinkAndStore(in: &disposables) - expect(multiDidReceiveSubscription).to(equal([true, true, true])) + expect(multiReceivedOutput.count).to(equal(3)) expect(multiReceivedCompletion.count).to(equal(3)) } } @@ -285,13 +257,11 @@ class PreparedRequestSendingSpec: QuickSpec { preparedRequest .map { _, output -> String in "\(output)" } .handleEvents( - receiveSubscription: { didReceiveSubscription = true }, receiveCompletion: { result in receivedCompletion = result } ) .send(using: dependencies) .sinkAndStore(in: &disposables) - expect(didReceiveSubscription).to(beTrue()) expect(receivedCompletion).toNot(beNil()) } } @@ -302,7 +272,7 @@ class PreparedRequestSendingSpec: QuickSpec { context("with a BatchResponseMap") { @TestState var subRequest1: Request! = try! Request( endpoint: TestEndpoint.endpoint1, - destination: try! .server( + destination: .server( method: .post, server: "testServer", x25519PublicKey: "" @@ -310,7 +280,7 @@ class PreparedRequestSendingSpec: QuickSpec { ) @TestState var subRequest2: Request! = try! Request( endpoint: TestEndpoint.endpoint2, - destination: try! .server( + destination: .server( method: .post, server: "testServer", x25519PublicKey: "" @@ -319,7 +289,7 @@ class PreparedRequestSendingSpec: QuickSpec { @TestState var preparedBatchRequest: Network.PreparedRequest>! = { let request = try! Request( endpoint: TestEndpoint.batch, - destination: try! .server( + destination: .server( method: .post, server: "testServer", x25519PublicKey: "" @@ -403,7 +373,7 @@ class PreparedRequestSendingSpec: QuickSpec { preparedBatchRequest = { let request = try! Request( endpoint: TestEndpoint.batch, - destination: try! .server( + destination: .server( method: .post, server: "testServer", x25519PublicKey: "" @@ -451,13 +421,11 @@ class PreparedRequestSendingSpec: QuickSpec { it("works with the event handling") { preparedBatchRequest .handleEvents( - receiveSubscription: { didReceiveSubscription = true }, receiveCompletion: { result in receivedCompletion = result } ) .send(using: dependencies) .sinkAndStore(in: &disposables) - expect(didReceiveSubscription).to(beTrue()) expect(receivedCompletion).toNot(beNil()) } @@ -466,7 +434,7 @@ class PreparedRequestSendingSpec: QuickSpec { preparedBatchRequest = { let request = try! Request( endpoint: TestEndpoint.batch, - destination: try! .server( + destination: .server( method: .post, server: "testServer", x25519PublicKey: "" @@ -531,7 +499,8 @@ fileprivate enum TestEndpoint: EndpointType { } fileprivate struct TestType: Codable, Equatable, Mocked { - static var mock: TestType { TestType(intValue: 100, stringValue: "Test", optionalStringValue: nil) } + public static var any: TestType { TestType(intValue: .any, stringValue: .any, optionalStringValue: .any) } + public static var mock: TestType { TestType(intValue: 100, stringValue: "Test", optionalStringValue: nil) } let intValue: Int let stringValue: String diff --git a/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift b/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift index 94bb2b4d73..b472404a17 100644 --- a/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift +++ b/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift @@ -26,7 +26,7 @@ class PreparedRequestSpec: QuickSpec { it("generates the request correctly") { request = try! Request( endpoint: .endpoint, - destination: try! .server( + destination: .server( method: .post, server: "testServer", queryParameters: [:], @@ -64,7 +64,7 @@ class PreparedRequestSpec: QuickSpec { it("does not strip excluded subrequest headers") { request = try! Request( endpoint: .endpoint, - destination: try! .server( + destination: .server( method: .post, server: "testServer", queryParameters: [:], diff --git a/SessionNetworkingKitTests/Types/RequestSpec.swift b/SessionNetworkingKitTests/Types/RequestSpec.swift index 0b951cf1d3..fd0255ec88 100644 --- a/SessionNetworkingKitTests/Types/RequestSpec.swift +++ b/SessionNetworkingKitTests/Types/RequestSpec.swift @@ -23,7 +23,7 @@ class RequestSpec: QuickSpec { it("is initialized with the correct default values") { let request: Request = try! Request( endpoint: .test1, - destination: try! .server( + destination: .server( server: "testServer", x25519PublicKey: "" ) @@ -38,7 +38,7 @@ class RequestSpec: QuickSpec { it("sets all the values correctly") { let request: Request = try! Request( endpoint: .test1, - destination: try! .server( + destination: .server( method: .delete, server: "testServer", headers: [ @@ -61,7 +61,7 @@ class RequestSpec: QuickSpec { it("successfully encodes the body") { let request: Request = try! Request( endpoint: .test1, - destination: try! .server( + destination: .server( server: "testServer", x25519PublicKey: "" ), @@ -78,7 +78,7 @@ class RequestSpec: QuickSpec { it("throws an error if the body is not base64 encoded") { let request: Request = try! Request( endpoint: .test1, - destination: try! .server( + destination: .server( server: "testServer", x25519PublicKey: "" ), @@ -98,7 +98,7 @@ class RequestSpec: QuickSpec { it("successfully encodes the body") { let request: Request<[UInt8], TestEndpoint> = try! Request( endpoint: .test1, - destination: try! .server( + destination: .server( server: "testServer", x25519PublicKey: "" ), @@ -117,7 +117,7 @@ class RequestSpec: QuickSpec { it("successfully encodes the body") { let request: Request = try! Request( endpoint: .test1, - destination: try! .server( + destination: .server( server: "testServer", x25519PublicKey: "" ), @@ -137,7 +137,7 @@ class RequestSpec: QuickSpec { it("successfully encodes no body") { let request: Request = try! Request( endpoint: .test1, - destination: try! .server( + destination: .server( server: "testServer", x25519PublicKey: "" ), diff --git a/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift b/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift deleted file mode 100644 index 2aac55bcf0..0000000000 --- a/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -@testable import SessionNetworkingKit - -extension NoResponse: Mocked { - static var mock: NoResponse = NoResponse() -} - -extension Network.BatchSubResponse: MockedGeneric where T: Mocked { - typealias Generic = T - - static func mock(type: T.Type) -> Network.BatchSubResponse { - return Network.BatchSubResponse( - code: 200, - headers: [:], - body: Generic.mock, - failedToParseBody: false - ) - } -} - -extension Network.BatchSubResponse { - static func mockArrayValue(type: M.Type) -> Network.BatchSubResponse> { - return Network.BatchSubResponse( - code: 200, - headers: [:], - body: [M.mock], - failedToParseBody: false - ) - } -} - -extension Network.Destination: Mocked { - static var mock: Network.Destination = try! Network.Destination.server( - server: "testServer", - headers: [:], - x25519PublicKey: "" - ).withGeneratedUrl(for: MockEndpoint.mock) -} - -extension Network.RequestCategory: Mocked { - static var mock: Network.RequestCategory = .standard -} diff --git a/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift index 1412423914..1ed9d66ff9 100644 --- a/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift +++ b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift @@ -3,6 +3,7 @@ import Foundation import Combine import SessionUtilitiesKit +import TestUtilities @testable import SessionNetworkingKit @@ -11,44 +12,90 @@ import SessionUtilitiesKit class MockNetwork: Mock, NetworkType { var requestData: RequestData? - func getSwarm(for swarmPublicKey: String) -> AnyPublisher, Error> { - return mock(args: [swarmPublicKey]) + var isSuspended: Bool { mock() } + var networkStatus: AsyncStream { mock() } + var syncState: NetworkSyncState { mock() } + + func getActivePaths() async throws -> [LibSession.Path] { + return try mockThrowing() + } + + func getSwarm(for swarmPublicKey: String) async throws -> Set { + return try mockThrowing(args: [swarmPublicKey]) } - func getRandomNodes(count: Int) -> AnyPublisher, Error> { - return mock(args: [count]) + func getRandomNodes(count: Int) async throws -> Set { + return try mockThrowing(args: [count]) } - func send( - _ body: Data?, - to destination: Network.Destination, + func send( + endpoint: E, + destination: Network.Destination, + body: Data?, + category: Network.RequestCategory, requestTimeout: TimeInterval, - requestAndPathBuildTimeout: TimeInterval? + overallTimeout: TimeInterval? ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { requestData = RequestData( + method: destination.method, + headers: destination.headers, + urlPathAndParamsString: destination.urlPathAndParamsString, body: body, + category: category, + requestTimeout: requestTimeout, + overallTimeout: overallTimeout + ) + + return mock(args: [endpoint, destination, body, category, requestTimeout, overallTimeout]) + } + + func send( + endpoint: E, + destination: Network.Destination, + body: Data?, + category: Network.RequestCategory, + requestTimeout: TimeInterval, + overallTimeout: TimeInterval? + ) async throws -> (info: ResponseInfoType, value: Data?) { + requestData = RequestData( method: destination.method, - pathAndParamsString: destination.urlPathAndParamsString, headers: destination.headers, - x25519PublicKey: { - switch destination { - case .server(let info), .serverUpload(let info, _), .serverDownload(let info): return info.x25519PublicKey - case .snode(_, let swarmPublicKey): return swarmPublicKey - case .randomSnode(let swarmPublicKey, _), .randomSnodeLatestNetworkTimeTarget(let swarmPublicKey, _, _): - return swarmPublicKey - - case .cached: return nil - } - }(), + urlPathAndParamsString: destination.urlPathAndParamsString, + body: body, + category: category, requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout + overallTimeout: overallTimeout ) - return mock(args: [body, destination, requestTimeout, requestAndPathBuildTimeout]) + return try mockThrowing(args: [endpoint, destination, body, category, requestTimeout, overallTimeout]) + } + + func checkClientVersion(ed25519SecretKey: [UInt8]) async throws -> (info: ResponseInfoType, value: AppVersionResponse) { + return try mockThrowing(args: [ed25519SecretKey]) + } + + func resetNetworkStatus() async { + mockNoReturn() + } + + func setNetworkStatus(status: NetworkStatus) async { + mockNoReturn(args: [status]) + } + + func suspendNetworkAccess() async { + mockNoReturn() + } + + func resumeNetworkAccess(autoReconnect: Bool) async { + mockNoReturn(args: [autoReconnect]) + } + + func finishCurrentObservations() async { + mockNoReturn() } - func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error> { - return mock(args: [ed25519SecretKey]) + func clearCache() async { + mockNoReturn() } } @@ -101,6 +148,7 @@ extension MockNetwork { // MARK: - MockResponseInfo struct MockResponseInfo: ResponseInfoType, Mocked { + static let any: MockResponseInfo = MockResponseInfo(requestData: .any, code: .any, headers: .any) static let mock: MockResponseInfo = MockResponseInfo(requestData: .mock, code: 200, headers: [:]) let requestData: RequestData @@ -115,23 +163,32 @@ struct MockResponseInfo: ResponseInfoType, Mocked { } struct RequestData: Codable, Mocked { + static let any: RequestData = RequestData( + method: .get, + headers: .any, + urlPathAndParamsString: .any, + body: .any, + category: .standard, + requestTimeout: .any, + overallTimeout: .any + ) static let mock: RequestData = RequestData( - body: nil, method: .get, - pathAndParamsString: "", headers: [:], - x25519PublicKey: nil, + urlPathAndParamsString: "", + body: nil, + category: .standard, requestTimeout: 0, - requestAndPathBuildTimeout: nil + overallTimeout: nil ) - let body: Data? let method: HTTPMethod - let pathAndParamsString: String let headers: [HTTPHeader: String] - let x25519PublicKey: String? + let urlPathAndParamsString: String + let body: Data? + let category: Network.RequestCategory let requestTimeout: TimeInterval - let requestAndPathBuildTimeout: TimeInterval? + let overallTimeout: TimeInterval? } // MARK: - Network.BatchSubResponse Encoding Convenience @@ -149,24 +206,31 @@ extension Encodable where Self: Codable { } } -extension Mocked where Self: Codable { +public extension Mocked where Self: Codable { static func mockBatchSubResponse() -> Data { return mock.batchSubResponse() } } -extension Array where Element: Mocked, Element: Codable { +public extension Array where Element: Mocked, Element: Codable { static func mockBatchSubResponse() -> Data { return [Element.mock].batchSubResponse() } } // MARK: - Endpoint enum MockEndpoint: EndpointType, Mocked { + static var any: MockEndpoint = .anyValue static var mockValue: MockEndpoint = .mock + case anyValue case mock static var name: String { "MockEndpoint" } static var batchRequestVariant: Network.BatchRequest.Child.Variant { .storageServer } static var excludedSubRequestHeaders: [HTTPHeader] { [] } - var path: String { return "mock" } + var path: String { + switch self { + case .anyValue: return "__MOCKED_ANY_ENDPOINT_VALUE__" + case .mock: return "mock" + } + } } diff --git a/SessionNetworkingKitTests/_TestUtilities/Mocked+SNK.swift b/SessionNetworkingKitTests/_TestUtilities/Mocked+SNK.swift new file mode 100644 index 0000000000..1246509917 --- /dev/null +++ b/SessionNetworkingKitTests/_TestUtilities/Mocked+SNK.swift @@ -0,0 +1,53 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import TestUtilities + +@testable import SessionNetworkingKit + +extension NoResponse: @retroactive Mocked { + public static var any: NoResponse = NoResponse() + public static var mock: NoResponse = NoResponse() +} + +extension Network.BatchSubResponse: @retroactive MockedGeneric where T: Mocked { + public typealias Generic = T + + public static func mock(type: T.Type) -> Network.BatchSubResponse { + return Network.BatchSubResponse( + code: 200, + headers: [:], + body: Generic.mock, + failedToParseBody: false + ) + } +} + +extension Network.BatchSubResponse { + static func mockArrayValue(type: M.Type) -> Network.BatchSubResponse> { + return Network.BatchSubResponse( + code: 200, + headers: [:], + body: [M.mock], + failedToParseBody: false + ) + } +} + +extension Network.Destination: @retroactive Mocked { + public static var any: Network.Destination = Network.Destination.server( + server: .any, + headers: .any, + x25519PublicKey: .any + ) + public static var mock: Network.Destination = try! Network.Destination.server( + server: "testServer", + headers: [:], + x25519PublicKey: "" + ).withGeneratedUrl(for: MockEndpoint.mock) +} + +extension Network.RequestCategory: @retroactive Mocked { + public static var any: Network.RequestCategory = .upload + public static var mock: Network.RequestCategory = .standard +} diff --git a/SessionNotificationServiceExtension/NSENotificationPresenter.swift b/SessionNotificationServiceExtension/NSENotificationPresenter.swift index cf343cc500..3e0980d716 100644 --- a/SessionNotificationServiceExtension/NSENotificationPresenter.swift +++ b/SessionNotificationServiceExtension/NSENotificationPresenter.swift @@ -61,7 +61,7 @@ public class NSENotificationPresenter: NotificationsManagerType { mutedUntil: TimeInterval? ) {} - public func notificationUserInfo(threadId: String, threadVariant: SessionThread.Variant) -> [String: Any] { + public func notificationUserInfo(threadId: String, threadVariant: SessionThread.Variant) -> [String: AnyHashable] { return [ NotificationUserInfoKey.isFromRemote: true, NotificationUserInfoKey.threadId: threadId, diff --git a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift index beaaa7ed26..4f45a47356 100644 --- a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift @@ -21,15 +21,7 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, - using: dependencies, - initialData: { db in - try SessionThread( - id: "TestId", - variant: .contact, - creationDateTimestamp: 0 - ).insert(db) - } + using: dependencies ) @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( initialSetup: { jobRunner in @@ -59,6 +51,19 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: AsyncSpec { ) ] + beforeEach { + try await mockStorage.perform( + migrations: SNMessagingKit.migrations + ) + try await mockStorage.writeAsync { db in + try SessionThread( + id: "TestId", + variant: .contact, + creationDateTimestamp: 0 + ).insert(db) + } + } + // MARK: - a ThreadDisappearingMessagesSettingsViewModel describe("a ThreadDisappearingMessagesSettingsViewModel") { // MARK: -- has the correct title diff --git a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift index 8d2532b0b0..417bf4376a 100644 --- a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift @@ -21,15 +21,7 @@ class ThreadNotificationSettingsViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, - using: dependencies, - initialData: { db in - try SessionThread( - id: "TestId", - variant: .contact, - creationDateTimestamp: 0 - ).insert(db) - } + using: dependencies ) @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( initialSetup: { jobRunner in @@ -44,25 +36,37 @@ class ThreadNotificationSettingsViewModelSpec: AsyncSpec { @TestState(singleton: .notificationsManager, in: dependencies) var mockNotificationsManager: MockNotificationsManager! = MockNotificationsManager( initialSetup: { $0.defaultInitialSetup() } ) - @TestState var viewModel: ThreadNotificationSettingsViewModel! = TestState.create { - await ThreadNotificationSettingsViewModel( + @TestState var viewModel: ThreadNotificationSettingsViewModel! + @TestState var cancellables: [AnyCancellable]! + + beforeEach { + try await mockStorage.perform( + migrations: SNMessagingKit.migrations + ) + try await mockStorage.writeAsync { db in + try SessionThread( + id: "TestId", + variant: .contact, + creationDateTimestamp: 0 + ).insert(db) + } + viewModel = await ThreadNotificationSettingsViewModel( threadId: "TestId", threadVariant: .contact, threadOnlyNotifyForMentions: nil, threadMutedUntilTimestamp: nil, using: dependencies ) + cancellables = [ + viewModel.tableDataPublisher + .receive(on: ImmediateScheduler.shared) + .sink( + receiveCompletion: { _ in }, + receiveValue: { viewModel.updateTableData($0) } + ) + ] } - @TestState var cancellables: [AnyCancellable]! = [ - viewModel.tableDataPublisher - .receive(on: ImmediateScheduler.shared) - .sink( - receiveCompletion: { _ in }, - receiveValue: { viewModel.updateTableData($0) } - ) - ] - // MARK: - a ThreadNotificationSettingsViewModel describe("a ThreadNotificationSettingsViewModel") { beforeEach { diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index fffc71c648..0d3fbc63f2 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -255,7 +255,7 @@ open class Storage { public func perform( migrations: [Migration.Type], - onProgressUpdate: ((CGFloat, TimeInterval) -> ())? + onProgressUpdate: ((CGFloat, TimeInterval) -> ())? = nil ) async throws { guard isValid, let dbWriter: DatabaseWriter = dbWriter else { let error: Error = (startupError ?? StorageError.startupFailed) diff --git a/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift b/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift index 34a9bb0582..985906999e 100644 --- a/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift +++ b/SessionUtilitiesKit/LibSession/Types/ObservingDatabase.swift @@ -5,8 +5,9 @@ import GRDB // MARK: - ObservingDatabase -public class ObservingDatabase { +public class ObservingDatabase: Equatable { public let dependencies: Dependencies + internal let id: UUID internal let originalDb: Database internal var events: [ObservedEvent] = [] internal var postCommitActions: [String: () -> Void] = [:] @@ -15,15 +16,20 @@ public class ObservingDatabase { /// The observation mechanism works via the `Storage` wrapper so if we create a new `ObservingDatabase` outside of that /// mechanism the observed events won't be emitted - public static func create(_ db: Database, using dependencies: Dependencies) -> ObservingDatabase { - return ObservingDatabase(db, using: dependencies) + public static func create(_ db: Database, id: UUID = UUID(), using dependencies: Dependencies) -> ObservingDatabase { + return ObservingDatabase(db, id: id, using: dependencies) } - private init(_ db: Database, using dependencies: Dependencies) { + private init(_ db: Database, id: UUID, using dependencies: Dependencies) { self.dependencies = dependencies + self.id = id self.originalDb = db } + public static func == (lhs: ObservingDatabase, rhs: ObservingDatabase) -> Bool { + return lhs.id == rhs.id + } + // MARK: - Functions public func addEvent(_ event: ObservedEvent) { diff --git a/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift b/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift index bfd9d50009..31b599bdc8 100644 --- a/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift +++ b/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift @@ -3,13 +3,14 @@ import Foundation import Combine import GRDB +import TestUtilities import Quick import Nimble @testable import SessionUtilitiesKit -class JobRunnerSpec: QuickSpec { +class JobRunnerSpec: AsyncSpec { override class func spec() { // MARK: Configuration @@ -47,11 +48,6 @@ class JobRunnerSpec: QuickSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: [ - _001_SUK_InitialSetupMigration.self, - _012_AddJobPriority.self, - _020_AddJobUniqueHash.self - ], using: dependencies ) @TestState(singleton: .jobRunner, in: dependencies) var jobRunner: JobRunnerType! = JobRunner( @@ -64,6 +60,16 @@ class JobRunnerSpec: QuickSpec { using: dependencies ) + beforeEach { + try await mockStorage.perform( + migrations: [ + _001_SUK_InitialSetupMigration.self, + _012_AddJobPriority.self, + _020_AddJobUniqueHash.self + ] + ) + } + // MARK: - a JobRunner describe("a JobRunner") { afterEach { @@ -1778,7 +1784,7 @@ fileprivate struct TestDetails: Codable { } fileprivate struct InvalidDetails: Codable { - func encode(to encoder: Encoder) throws { throw MockError.mockedData } + func encode(to encoder: Encoder) throws { throw MockError.mock } } fileprivate enum TestJob: JobExecutor { @@ -1816,8 +1822,8 @@ fileprivate enum TestJob: JobExecutor { switch details.result { case .success: success(job, true) - case .failure: failure(job, MockError.mockedData, false) - case .permanentFailure: failure(job, MockError.mockedData, true) + case .failure: failure(job, MockError.mock, false) + case .permanentFailure: failure(job, MockError.mock, true) case .deferred: deferred(updatedJob) } } diff --git a/SessionUtilitiesKitTests/_TestUtilities/Mocked+SUK.swift b/SessionUtilitiesKitTests/_TestUtilities/Mocked+SUK.swift new file mode 100644 index 0000000000..0afc50cecf --- /dev/null +++ b/SessionUtilitiesKitTests/_TestUtilities/Mocked+SUK.swift @@ -0,0 +1,99 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Combine +import GRDB +import TestUtilities + +@testable import SessionUtilitiesKit + +extension SessionId { static var any: SessionId { SessionId.invalid } } +extension Dependencies { + static var any: Dependencies { + TestDependencies { dependencies in + dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) + dependencies.forceSynchronous = true + } + } +} + +extension ObservingDatabase: @retroactive Mocked { + public static var any: Self { + var result: Database! + try! DatabaseQueue().read { result = $0 } + return ObservingDatabase.create(result!, id: .any, using: .any) as! Self + } + public static var mock: Self { + var result: Database! + try! DatabaseQueue().read { result = $0 } + return ObservingDatabase.create(result!, id: .mock, using: .any) as! Self + } +} + +extension ObservableKey: @retroactive Mocked { + public static var any: ObservableKey = "__MOCKED_ANY_KEY_VALUE__" + public static var mock: ObservableKey = "mockObservableKey" +} + +extension ObservedEvent: @retroactive Mocked { + public static var any: ObservedEvent { return ObservedEvent(key: "__MOCKED_ANY_KEY_VALUE__", value: nil) } + public static var mock: ObservedEvent = ObservedEvent(key: "mock", value: nil) +} + +extension KeyPair: @retroactive Mocked { + public static var any: KeyPair = KeyPair(publicKey: Array(Data.any), secretKey: Array(Data.any)) + public static var mock: KeyPair = KeyPair( + publicKey: Data(hex: TestConstants.publicKey).bytes, + secretKey: Data(hex: TestConstants.edSecretKey).bytes + ) +} + +extension Job: @retroactive Mocked { + public static var any: Job = Job(variant: .any) + public static var mock: Job = Job(variant: .mock) +} + +extension Job.Variant: @retroactive Mocked { + public static var any: Job.Variant = ._legacy_notifyPushServer + public static var mock: Job.Variant = .messageSend +} + +extension JobRunner.JobResult: @retroactive Mocked { + public static var any: JobRunner.JobResult = .failed(MockError.any, false) + public static var mock: JobRunner.JobResult = .succeeded +} + +extension Log.Category: @retroactive Mocked { + public static var any: Log.Category = .create(.any, defaultLevel: .debug) + public static var mock: Log.Category = .create("mock", defaultLevel: .debug) +} + +extension Setting.BoolKey: @retroactive Mocked { + public static var any: Setting.BoolKey = "__MOCKED_ANY_BOOL_KEY_VALUE__" + public static var mock: Setting.BoolKey = "mockBool" +} + +extension Setting.EnumKey: @retroactive Mocked { + public static var any: Setting.EnumKey = "__MOCKED_ANY_ENUM_KEY_VALUE__" + public static var mock: Setting.EnumKey = "mockEnum" +} + +// MARK: - Encodable Convenience + +extension Mocked where Self: Encodable { + func encoded(using dependencies: Dependencies) -> Data { + try! JSONEncoder(using: dependencies).with(outputFormatting: .sortedKeys).encode(self) + } +} + +extension MockedGeneric where Self: Encodable { + func encoded(using dependencies: Dependencies) -> Data { + try! JSONEncoder(using: dependencies).with(outputFormatting: .sortedKeys).encode(self) + } +} + +extension Array where Element: Encodable { + func encoded(using dependencies: Dependencies) -> Data { + try! JSONEncoder(using: dependencies).with(outputFormatting: .sortedKeys).encode(self) + } +} diff --git a/TestUtilities/ArgumentDescribing.swift b/TestUtilities/ArgumentDescribing.swift new file mode 100644 index 0000000000..a3d1421ca8 --- /dev/null +++ b/TestUtilities/ArgumentDescribing.swift @@ -0,0 +1,142 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation + +public protocol ArgumentDescribing { + var summary: String? { get } +} + +internal func summary(for argument: Any?) -> String { + guard let argument: Any = argument else { return "nil" } + + if isAnyValue(argument) { + return "" + } + + /// Then handle any `ArgumentDescribing` values + if let customSummary: String = (argument as? ArgumentDescribing)?.summary { + return customSummary + } + + /// Finally try to process standard types + switch argument { + case let string as String: return string.debugDescription + case let array as [Any]: return "[\(array.map { summary(for: $0) }.joined(separator: ", "))]" + + case let dict as [String: Any]: + if dict.isEmpty { return "[:]" } + + let sortedValues: [String] = dict + .map { key, value in "\(summary(for: key)):\(summary(for: value))" } + .sorted() + return "[\(sortedValues.joined(separator: ", "))]" + + case let data as Data: return "Data(base64Encoded: \(data.base64EncodedString()))" + + default: + // Default to the `debugDescription` if available but sort any dictionary content by keys + return sortDictionariesInReflectedString(String(reflecting: argument)) + } +} + +private func isAnyValue(_ value: Any) -> Bool { + func open(value: T) -> Bool { + return value == T.any + } + + if let mockedEquatableValue = value as? any (Mocked & Equatable) { + return open(value: mockedEquatableValue) + } + + return false +} + +private func sortDictionariesInReflectedString(_ input: String) -> String { + // Regular expression to match the headers dictionary + let pattern = "\\[(.+?)\\]" + let regex = try! NSRegularExpression(pattern: pattern, options: []) + + var result = "" + var lastRange = input.startIndex.. String { + var pairs: [(String, String)] = [] + var currentKey = "" + var currentValue = "" + var inQuotes = false + var parsingKey = true + var nestedLevel = 0 + + for char in dictionaryString { + switch char { + case "\"": + inQuotes.toggle() + if nestedLevel > 0 { + currentKey.append(char) + continue + } + + case ":": + if !inQuotes && nestedLevel == 0 { + parsingKey = false + continue + } + + case ",": + if !inQuotes && nestedLevel == 0 { + pairs.append((currentKey.trimmingCharacters(in: .whitespaces), currentValue.trimmingCharacters(in: .whitespaces))) + currentKey = "" + currentValue = "" + parsingKey = true + continue + } + + case "[", "{": nestedLevel += (parsingKey ? 0 : 1) + case "]", "}": nestedLevel -= (parsingKey ? 0 : 1) + default: break + } + + switch parsingKey { + case true: currentKey.append(char) + case false: currentValue.append(char) + } + } + + // Add the last pair if exists + if !currentKey.isEmpty || !currentValue.isEmpty { + pairs.append((currentKey.trimmingCharacters(in: .whitespaces), currentValue.trimmingCharacters(in: .whitespaces))) + } + + // Sort pairs by key + let sortedPairs = pairs.sorted { $0.0 < $1.0 } + + // Join sorted pairs back into a string + return sortedPairs.map { "\($0): \($1)" }.joined(separator: ", ") +} diff --git a/TestUtilities/MockError.swift b/TestUtilities/MockError.swift new file mode 100644 index 0000000000..0bc0ab6112 --- /dev/null +++ b/TestUtilities/MockError.swift @@ -0,0 +1,36 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation + +public enum MockError: Error, CustomStringConvertible { + case any + case mock + case noStubFound(function: String, args: [Any?]) + case stubbedValueIsWrongType(expected: Any.Type, actual: Any.Type?) + case cannotCreateDummyValue(type: Any.Type, function: String) + case invalidWhenBlock(message: String) + + public var description: String { + switch self { + case .any: return "AnyError" + case .mock: return "MockError: This error should never be thrown." + case .noStubFound(let function, let args): + let argsDescription: String = args.map { summary(for: $0) }.joined(separator: ", ") + + return "MockError: No stub found for '\(function)(\(argsDescription))'. Use .when { ... } to provide a return value or action." + + case .stubbedValueIsWrongType(let expected, let actual): + return "MockError: A stub for this function was found, but its return value is the wrong type. Expected '\(expected)', but found '\(actual.map { "\($0)" } ?? "nil")'." + + case .cannotCreateDummyValue(let type, let function): + return "MockError: The function '\(function)' being stubbed returns a non-optional value of type '\(type)', which does not conform to the 'Mocked' protocol. Please add conformance to provide a default value." + + case .invalidWhenBlock(let message): + return "MockError: Invalid `when` block. \(message)" + + } + } +} + diff --git a/TestUtilities/MockFunction.swift b/TestUtilities/MockFunction.swift new file mode 100644 index 0000000000..303fb1ce04 --- /dev/null +++ b/TestUtilities/MockFunction.swift @@ -0,0 +1,80 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +internal final class MockFunction { + let name: String + let generics: [Any.Type] + let arguments: [Any?] + let returnValue: Any? + let returnError: (any Error)? + let actions: [([Any?]) -> Void] + + init( + name: String, + generics: [Any.Type], + arguments: [Any?], + returnValue: Any?, + returnError: Error?, + actions: [([Any?]) -> Void] + ) { + self.name = name + self.generics = generics + self.arguments = arguments + self.returnValue = returnValue + self.returnError = returnError + self.actions = actions + } + + func matches(args: [Any?]) -> Bool { + guard args.count == arguments.count else { return false } + + for (stubArg, callArg) in zip(arguments, args) { + if !argumentMatches(stubArg: stubArg, callArg: callArg) { + return false + } + } + + return true + } + + private func argumentMatches(stubArg: Any?, callArg: Any?) -> Bool { + func isStubArgAnAnyMatcher(value: T) -> Bool { + return areEqual(value, T.any) + } + + switch (stubArg, callArg) { + case (.none, .none): return true /// Too hard to compare `nil == nil` after type erasure + case (.none, .some): return false /// Expected `nil`, given a value + case (.some(let stub as any Mocked), .none): + return isStubArgAnAnyMatcher(value: stub) + + case (.some, .none): return false /// Expected non-`nil` value for non-`Mocked` type, given `nil` + case (.some(let stub), .some(let call)): + /// If the `stubArg` is `Mocked.any` then we want to match anything + if let mockedStub = stub as? any Mocked, isStubArgAnAnyMatcher(value: mockedStub) { + return type(of: stub) == type(of: call) + } + + /// Otherwise we need to do some form of equality check + return areEqual(stub, call) + } + } + + private func areEqual(_ lhs: Any, _ rhs: Any) -> Bool { + func open(lhs: T, rhs: Any) -> Bool { + if let rhs = rhs as? T { + return lhs == rhs + } + + return false + } + + if let equatableLhs = lhs as? any Equatable { + return open(lhs: equatableLhs, rhs: rhs) + } + + /// Compare using the `summary` as a fallback + return summary(for: lhs) == summary(for: rhs) + } +} diff --git a/TestUtilities/MockFunctionBuilder.swift b/TestUtilities/MockFunctionBuilder.swift new file mode 100644 index 0000000000..0e849408f6 --- /dev/null +++ b/TestUtilities/MockFunctionBuilder.swift @@ -0,0 +1,110 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +internal protocol Buildable { + func build() async throws -> MockFunction +} + +public class MockFunctionBuilder { + private let handler: MockHandler + private let callBlock: (T) async throws -> R + private let dummyProvider: (any MockFunctionHandler) -> T + + private var capturedFunctionName: String? + private var capturedGenerics: [Any.Type] = [] + private var capturedArguments: [Any?] = [] + + private var returnValue: Any? + private var returnError: Error? + private var actions: [([Any?]) -> Void] = [] + + public init( + handler: MockHandler, + callBlock: @escaping (T) async throws -> R, + dummyProvider: @escaping (any MockFunctionHandler) -> T + ) { + self.handler = handler + self.callBlock = callBlock + self.dummyProvider = dummyProvider + } + + // MARK: - Mock Configuration API + + @discardableResult public func then(_ action: @escaping ([Any?]) -> Void) -> Self { + self.actions.append({ args in action(args) }) + return self + } + + public func thenReturn(_ value: R) async { + self.returnValue = value + await finalize() + } + + public func thenThrow(_ error: Error) async { + self.returnError = error + await finalize() + } + + // MARK: - Internal Functions + + private func finalize() async { + do { + let function = try await self.build() + handler.register(stub: function) + } catch { + /// If the build fails (e.g., invalid `when` block), we should fail the test + handler.reportFailureFromBuilder(error) + } + } + + private func captureDetails() async { + /// Only run capture once + guard capturedFunctionName == nil else { return } + + let dummy: T = dummyProvider(self) + _ = try? await callBlock(dummy) + } +} + +extension MockFunctionBuilder: Buildable { + func build() async throws -> MockFunction { + await captureDetails() + + guard let name: String = capturedFunctionName else { + throw MockError.invalidWhenBlock(message: "The when{...} block did not call a mockable function.") + } + + return MockFunction( + name: name, + generics: capturedGenerics, + arguments: capturedArguments, + returnValue: returnValue, + returnError: returnError, + actions: actions + ) + } +} + +// MARK: - MockFunctionHandler + +extension MockFunctionBuilder: MockFunctionHandler { + public func mock(funcName: String, generics: [Any.Type], args: [Any?]) -> Output { + self.capturedFunctionName = funcName + self.capturedGenerics = generics + self.capturedArguments = args + + /// This is a hack to force Swift to think it has received a value, if we try to use the value it will crash but becasue this is only + /// ever used for constructing stub calls (as opposed to calling mocked functions) the result will never be used so won't crash + func createPlaceholder() -> V { + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + defer { pointer.deallocate() } + return pointer.move() + } + + return createPlaceholder() + } +} + +private protocol _OptionalProtocol {} +extension Optional: _OptionalProtocol {} diff --git a/TestUtilities/MockFunctionHandler.swift b/TestUtilities/MockFunctionHandler.swift new file mode 100644 index 0000000000..daf1cc0a6c --- /dev/null +++ b/TestUtilities/MockFunctionHandler.swift @@ -0,0 +1,8 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public protocol MockFunctionHandler { + @discardableResult + func mock(funcName: String, generics: [Any.Type], args: [Any?]) -> Output +} diff --git a/TestUtilities/MockHandler.swift b/TestUtilities/MockHandler.swift new file mode 100644 index 0000000000..0303dbd019 --- /dev/null +++ b/TestUtilities/MockHandler.swift @@ -0,0 +1,284 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public final class MockHandler { + private let lock = NSLock() + private let dummyProvider: (any MockFunctionHandler) -> T + private let failureReporter: TestFailureReporter + private let forwardingHandler: (any MockFunctionHandler)? + private var stubs: [Key: [MockFunction]] = [:] + private var calls: [Key: [RecordedCall]] = [:] + + // MARK: - Initialization + + public init( + dummyProvider: @escaping (any MockFunctionHandler) -> T, + failureReporter: TestFailureReporter = NimbleFailureReporter() + ) { + self.dummyProvider = dummyProvider + self.failureReporter = failureReporter + self.forwardingHandler = nil + } + + public init(forwardingHandler: any MockFunctionHandler) { + self.dummyProvider = { _ in fatalError("A dummy instance cannot create other dummies.") } + self.failureReporter = NimbleFailureReporter() + self.forwardingHandler = forwardingHandler + } + + + public static func invalid() -> MockHandler { + return MockHandler(dummyProvider: { _ in fatalError("Should not call mock on a mock") } ) + } + + // MARK: - Setup + + func createBuilder(for callBlock: @escaping (T) async throws -> R) -> MockFunctionBuilder { + return MockFunctionBuilder( + handler: self, + callBlock: callBlock, + dummyProvider: dummyProvider + ) + } + + internal func register(stub: MockFunction) { + lock.lock() + defer { lock.unlock() } + let key: Key = Key(name: stub.name, generics: stub.generics, paramCount: stub.arguments.count) + stubs[key, default: []].append(stub) + } + + internal func reportFailureFromBuilder( + _ error: Error, + fileID: String = #fileID, + file: String = #file, + line: UInt = #line + ) { + failureReporter.reportFailure("Mocking Framework Error during stubbing: \(error)", fileID: fileID, file: file, line: line) + } + + // MARK: - Verification + + func recordedCalls(for functionBlock: @escaping (T) async throws -> R) async -> [RecordedCall]? { + let builder: MockFunctionBuilder = createBuilder(for: functionBlock) + + guard let builtFunction = try? await builder.build() else { + return nil + } + + let key: Key = Key( + name: builtFunction.name, + generics: builtFunction.generics, + paramCount: builtFunction.arguments.count + ) + + guard let callsForKey: [RecordedCall] = calls[key] else { return [] } + + return callsForKey.filter { builtFunction.matches(args: $0.args) } + } + + func allRecordedCalls(for functionBlock: @escaping (T) async throws -> R) async -> [RecordedCall]? { + let builder: MockFunctionBuilder = createBuilder(for: functionBlock) + + guard let builtFunction = try? await builder.build() else { + return nil + } + + let key: Key = Key( + name: builtFunction.name, + generics: builtFunction.generics, + paramCount: builtFunction.arguments.count + ) + + return calls[key] + } + + // MARK: - Test Lifecycle + + public func reset() { + stubs.removeAll() + calls.removeAll() + } + + // MARK: - Internal Logic + + private func findAndExecute( + funcName: String, + generics: [Any.Type], + args: [Any?], + fileID: String, + file: String, + line: UInt + ) -> Result { + lock.lock() + let key: Key = Key(name: funcName, generics: generics, paramCount: args.count) + let recordedCall: RecordedCall = RecordedCall(name: funcName, args: args) + calls[key, default: []].append(recordedCall) + + /// Get the `last` value as it was the one called most recently + let maybeMatchingCall: MockFunction? = stubs[key]?.last(where: { $0.matches(args: args) }) + lock.unlock() + + guard let matchingCall: MockFunction = maybeMatchingCall else { + return .failure(MockError.noStubFound(function: funcName, args: args)) + } + + /// Perform any actions + for action in matchingCall.actions { + action(args) + } + + return execute(stub: matchingCall) + } + + private func execute(stub: MockFunction) -> Result { + if let error: Error = stub.returnError { + return .failure(error) + } + + /// Handle `Void` returns first + guard Output.self != Void.self else { + return .success(() as! Output) + } + + /// Then handle the proper typed return value + if let returnValue: Any = stub.returnValue, let typedValue: Output = returnValue as? Output { + return .success(typedValue) + } + + return .failure(MockError.stubbedValueIsWrongType( + expected: Output.self, + actual: type(of: stub.returnValue) + )) + } +} + +// MARK: - MockHandler.Key + +private extension MockHandler { + struct Key: Equatable, Hashable { + let name: String + let generics: [String] + let paramCount: Int + + init(name: String, generics: [Any.Type], paramCount: Int) { + self.name = name + self.generics = generics.map { String(describing: $0) } + self.paramCount = paramCount + } + } +} + +// MARK: - Mock Functions + +public extension MockHandler { + func mock( + funcName: String = #function, + generics: [Any.Type] = [], + args: [Any?] = [], + fileID: String = #fileID, + file: String = #file, + line: UInt = #line + ) -> Output { + if let forwardedHandler: (any MockFunctionHandler) = forwardingHandler { + return forwardedHandler.mock(funcName: funcName, generics: generics, args: args) + } + + return handlingNonThrowingResult( + result: findAndExecute( + funcName: funcName, + generics: generics, + args: args, + fileID: fileID, + file: file, + line: line + ), + funcName: funcName, + fileID: fileID, + file: file, + line: line + ) + } + + func mockNoReturn( + funcName: String = #function, + generics: [Any.Type] = [], + args: [Any?] = [], + fileID: String = #fileID, + file: String = #file, + line: UInt = #line + ) { + let _: Void = mock(funcName: funcName, generics: generics, args: args, fileID: fileID, file: file, line: line) + } + + func mockThrowing( + funcName: String = #function, + generics: [Any.Type] = [], + args: [Any?] = [], + fileID: String = #fileID, + file: String = #file, + line: UInt = #line + ) throws -> Output { + if let forwardedHandler: (any MockFunctionHandler) = forwardingHandler { + return forwardedHandler.mock(funcName: funcName, generics: generics, args: args) + } + + return try findAndExecute( + funcName: funcName, + generics: generics, + args: args, + fileID: fileID, + file: file, + line: line + ).get() + } + + func mockThrowingNoReturn( + funcName: String = #function, + generics: [Any.Type] = [], + args: [Any?] = [], + fileID: String = #fileID, + file: String = #file, + line: UInt = #line + ) throws { + let _: Void = try mockThrowing(funcName: funcName, generics: generics, args: args, fileID: fileID, file: file, line: line) + } + + private func handlingNonThrowingResult( + result: Result, + funcName: String, + fileID: String, + file: String, + line: UInt + ) -> Output { + switch result { + case .success(let value): return value + case .failure(let error): + /// Log if the failure was due to a missing mock + if case MockError.noStubFound(_, _) = error { + let targetFileID: String = (TestContext.current?.fileID ?? fileID) + let targetFile: String = (TestContext.current?.file ?? file) + let targetLine: UInt = (TestContext.current?.line ?? line) + + failureReporter.reportFailure( + "Mocking Error: An unstubbed function was called: `\(funcName)`", + fileID: targetFileID, + file: targetFile, + line: targetLine + ) + } + + /// Custom handle a `Void` return type before checking for Mocked conformance + guard Output.self != Void.self else { + return () as! Output + } + + guard let mockedType = Output.self as? Mocked.Type else { + fatalError("FATAL: The return type '\(Output.self)' of the non-throwing function '\(funcName)' does not conform to 'Mocked'. This conformance is required to provide a fallback value when a test fails due to a missing stub.") + } + + return mockedType.mock as! Output + } + } +} diff --git a/TestUtilities/Mockable.swift b/TestUtilities/Mockable.swift new file mode 100644 index 0000000000..823b8dc32d --- /dev/null +++ b/TestUtilities/Mockable.swift @@ -0,0 +1,28 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public protocol Mockable { + associatedtype MockedType + + var handler: MockHandler { get } + + init(handler: MockHandler) + init(handlerForBuilder: any MockFunctionHandler) +} + +public extension Mockable { + static func create() -> M { + let handler: MockHandler = MockHandler( + dummyProvider: { builderHandler in + return M(handlerForBuilder: builderHandler) as! M.MockedType + } + ) + + return M(handler: handler) + } + + func when(_ callBlock: @escaping (MockedType) async throws -> R) -> MockFunctionBuilder { + return handler.createBuilder(for: callBlock) + } +} diff --git a/TestUtilities/Mocked.swift b/TestUtilities/Mocked.swift new file mode 100644 index 0000000000..d29fa1487b --- /dev/null +++ b/TestUtilities/Mocked.swift @@ -0,0 +1,170 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import UIKit.UIApplication + +// MARK: - Mocked + +public protocol Mocked { + static var any: Self { get } + static var mock: Self { get } +} + +public protocol MockedGeneric { + associatedtype Generic + + static func mock(type: Generic.Type) -> Self +} + +// MARK: - DSL + +extension Int: Mocked { + public static let any: Int = (Int.max - 123) + public static let mock: Int = 0 +} +extension UInt: Mocked { + public static let any: UInt = (UInt.max - 123) + public static let mock: UInt = 0 +} +extension Int8: Mocked { + public static let any: Int8 = (Int8.max - 123) + public static let mock: Int8 = 0 +} +extension UInt8: Mocked { + public static let any: UInt8 = (UInt8.max - 123) + public static let mock: UInt8 = 0 +} +extension Int16: Mocked { + public static let any: Int16 = (Int16.max - 123) + public static let mock: Int16 = 0 +} +extension UInt16: Mocked { + public static let any: UInt16 = (UInt16.max - 123) + public static let mock: UInt16 = 0 +} +extension Int32: Mocked { + public static let any: Int32 = (Int32.max - 123) + public static let mock: Int32 = 0 +} +extension UInt32: Mocked { + public static let any: UInt32 = (UInt32.max - 123) + public static let mock: UInt32 = 0 +} +extension Int64: Mocked { + public static let any: Int64 = (Int64.max - 123) + public static let mock: Int64 = 0 +} +extension UInt64: Mocked { + public static let any: UInt64 = (UInt64.max - 123) + public static let mock: UInt64 = 0 +} +extension Float: Mocked { + public static let any: Float = (Float.greatestFiniteMagnitude - 123) + public static let mock: Float = 0 +} +extension Double: Mocked { + public static let any: Double = (Double.greatestFiniteMagnitude - 123) + public static let mock: Double = 0 +} +extension String: Mocked { + public static let any: String = "__MOCKED_ANY_VALUE__" + public static let mock: String = "" +} +extension Data: Mocked { + public static let any: Data = Data([1, 1, 1, 200, 200, 200, 1, 1, 1]) + public static let mock: Data = Data() +} +extension Bool: Mocked { + public static let any: Bool = false + public static let mock: Bool = false +} + +extension Dictionary: Mocked { + public static var any: Self { + if let mockedKeyType = Key.self as? any (Mocked & Hashable).Type, let mockedValueType = Value.self as? any Mocked.Type { + let anyKey = mockedKeyType.any + let anyValue = mockedValueType.any + + guard let hashableKey = anyKey as? AnyHashable else { + return [:] + } + + return [hashableKey: anyValue] as! Self + } + + return [:] + } + public static var mock: Self { [:] } +} +extension Array: Mocked { + public static var any: Self { + if let mockedType = Element.self as? any Mocked.Type { + return [mockedType.any] as! Self + } + + return [] + } + public static var mock: Self { [] } +} +extension Set: Mocked { + public static var any: Self { + if let mockedType = Element.self as? any Mocked.Type { + return [mockedType.any] as! Self + } + + return [] + } + public static var mock: Self { [] } +} + +extension UIApplication.State: Mocked { + public static let any: UIApplication.State = .active + public static let mock: UIApplication.State = .active +} +extension UnsafeMutablePointer?: Mocked { + public static var any: UnsafeMutablePointer? { nil } + public static var mock: UnsafeMutablePointer? { nil } +} + +extension UUID: Mocked { + public static let any: UUID = UUID(uuidString: "12300099-0099-0000-0000-990099000321")! + public static let mock: UUID = UUID(uuidString: "00000000-0000-0000-0000-000000000001")! +} + +extension URL: Mocked { + public static let any: URL = URL(fileURLWithPath: "__MOCKED_ANY_VALUE__") + public static let mock: URL = URL(fileURLWithPath: "mock") +} + +extension URLRequest: Mocked { + public static let any: URLRequest = URLRequest(url: URL(fileURLWithPath: "__MOCKED_ANY_VALUE__")) + public static let mock: URLRequest = URLRequest(url: URL(fileURLWithPath: "mock")) +} + +extension AnyPublisher: MockedGeneric where Failure == Error { + public typealias Generic = Output + + public static func any(type: Output.Type) -> AnyPublisher { mock(type: type) } + + public static func mock(type: Output.Type) -> AnyPublisher { + return Fail(error: MockError.mock).eraseToAnyPublisher() + } +} + +extension AsyncStream: Mocked { + public static var any: AsyncStream { AsyncStream { $0.finish() } } + public static var mock: AsyncStream { AsyncStream { $0.finish() } } +} + +extension FileManager.ItemReplacementOptions: Mocked { + public static let any: FileManager.ItemReplacementOptions = FileManager.ItemReplacementOptions() + public static let mock: FileManager.ItemReplacementOptions = FileManager.ItemReplacementOptions() +} + +extension FileProtectionType: Mocked { + public static let any: FileProtectionType = .complete + public static let mock: FileProtectionType = .complete +} diff --git a/TestUtilities/Nimble/NimbleFailureReporter.swift b/TestUtilities/Nimble/NimbleFailureReporter.swift new file mode 100644 index 0000000000..72a15ca150 --- /dev/null +++ b/TestUtilities/Nimble/NimbleFailureReporter.swift @@ -0,0 +1,12 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +internal import Nimble + +public struct NimbleFailureReporter: TestFailureReporter { + public init() {} + + public func reportFailure(_ message: String, fileID: String, file: String, line: UInt) { + fail(message, fileID: fileID, file: file, line: line) + } +} diff --git a/TestUtilities/Nimble/NimbleVerification.swift b/TestUtilities/Nimble/NimbleVerification.swift new file mode 100644 index 0000000000..7080c061cc --- /dev/null +++ b/TestUtilities/Nimble/NimbleVerification.swift @@ -0,0 +1,78 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +internal import Nimble + +public struct NimbleVerification { + public let matchingCalls: [RecordedCall]? + public let allCallsForFunction: [RecordedCall]? + + public func wasCalled(exactly times: Int, fileID: String = #fileID, file: String = #filePath, line: UInt = #line) { + expect(fileID: fileID, file: file, line: line, self).to(beCalled(exactly: times)) + } + + public func wasCalled(atLeast times: Int = 1, fileID: String = #fileID, file: String = #filePath, line: UInt = #line) { + let actualCount: Int = (matchingCalls?.count ?? 0) + let description: String = "Expected call to happen at least \(times) time(s), but was called \(actualCount) times(s)." + + expect(fileID: fileID, file: file, line: line, actualCount) + .to(beGreaterThanOrEqualTo(times), description: description) + } + + public func wasNotCalled(fileID: String = #fileID, file: String = #filePath, line: UInt = #line) { + expect(fileID: fileID, file: file, line: line, self).to(beCalled(exactly: 0)) + } +} + +public extension Mockable { + func verify(_ callBlock: @escaping (MockedType) async throws -> R) async -> NimbleVerification { + let matching: [RecordedCall] = (await handler.recordedCalls(for: callBlock) ?? []) + let all: [RecordedCall] = (await handler.allRecordedCalls(for: callBlock) ?? []) + + return NimbleVerification( + matchingCalls: matching, + allCallsForFunction: all + ) + } +} + +internal func beCalled(exactly times: Int) -> Matcher { + return Matcher { actualExpression in + let message: ExpectationMessage = ExpectationMessage.expectedTo("be called exactly \(times) time(s)") + + guard let verification = try actualExpression.evaluate() else { + return MatcherResult(status: .fail, message: message.appendedBeNilHint()) + } + + let actualCount: Int = (verification.matchingCalls?.count ?? 0) + + if actualCount == times { + return MatcherResult(status: .matches, message: message) + } + + var details: String = "" + + if let allCalls: [RecordedCall] = verification.allCallsForFunction, !allCalls.isEmpty { + let callDescriptions: String = allCalls + .map { call in + let args: String = call.args.map { summary(for: $0) }.joined(separator: ", ") + + return "- \(call.name) [\(args)]" + } + .joined(separator: "\n") + + details += "\n\nAll calls to this function with different arguments:\n\(callDescriptions)" + } else { + details += "\n\nNo other calls were made to this function." + } + + return MatcherResult( + status: .fail, + message: message + .appended(message: ", got \(actualCount) matching call(s).") + .appended(details: details) + ) + } +} diff --git a/TestUtilities/RecordedCall.swift b/TestUtilities/RecordedCall.swift new file mode 100644 index 0000000000..912bcfd562 --- /dev/null +++ b/TestUtilities/RecordedCall.swift @@ -0,0 +1,8 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public struct RecordedCall { + let name: String + let args: [Any?] +} diff --git a/TestUtilities/TestFailureReporter.swift b/TestUtilities/TestFailureReporter.swift new file mode 100644 index 0000000000..52ee43b3a9 --- /dev/null +++ b/TestUtilities/TestFailureReporter.swift @@ -0,0 +1,14 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public protocol TestFailureReporter { + /// Reports a fatal error for the current test case, stopping its execution + func reportFailure(_ message: String, fileID: String, file: String, line: UInt) +} + +public extension TestFailureReporter { + func reportFailure(_ message: String, fileID: String = #fileID, file: String = #filePath, line: UInt = #line) { + reportFailure(message, fileID: fileID, file: file, line: line) + } +} diff --git a/TestUtilities/TextContext.swift b/TestUtilities/TextContext.swift new file mode 100644 index 0000000000..a33914fa01 --- /dev/null +++ b/TestUtilities/TextContext.swift @@ -0,0 +1,28 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public final class TestContext { + public var fileID: String = #fileID + public var file: String = #filePath + public var line: UInt = #line + + @TaskLocal public static var current: TestContext? +} + +public func withTestContext( + fileID: String = #fileID, + file: String = #file, + line: UInt = #line, + _ body: () async throws -> T +) async rethrows -> T { + let context: TestContext = TestContext() + context.fileID = fileID + context.file = file + context.line = line + + return try await TestContext.$current.withValue(context) { + try await body() + } +} + diff --git a/TestUtilities/Utilities/Async+Utilities.swift b/TestUtilities/Utilities/Async+Utilities.swift new file mode 100644 index 0000000000..d686ec3898 --- /dev/null +++ b/TestUtilities/Utilities/Async+Utilities.swift @@ -0,0 +1,16 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension AsyncStream { + static func singleValue(value: Element) -> AsyncStream { + var hasEmittedValue: Bool = false + + return AsyncStream(unfolding: { + guard !hasEmittedValue else { return nil } + + hasEmittedValue = true + return value + }) + } +} diff --git a/_SharedTestUtilities/FixtureBase.swift b/_SharedTestUtilities/FixtureBase.swift new file mode 100644 index 0000000000..0f113680bc --- /dev/null +++ b/_SharedTestUtilities/FixtureBase.swift @@ -0,0 +1,104 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit +import TestUtilities + +open class FixtureBase { + let dependencies: TestDependencies = TestDependencies() + + public init() { + dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) + dependencies.forceSynchronous = true + } + + public func mock( + for singleton: SingletonConfig, + _ creation: (TestDependencies) -> R + ) -> R { + if let existingMock: R = dependencies.get(singleton: singleton) as? R { + return existingMock + } + + let value: R = creation(dependencies) + (value as? DependenciesSettable)?.setDependencies(dependencies) + + guard let conformingValue: SingletonType = value as? SingletonType else { + fatalError("Type Mismatch: The mock of type '\(type(of: value))' does not conform to the required protocol '\(SingletonType.self)' for the provided singleton key.") + } + + dependencies.set(singleton: singleton, to: conformingValue) + return value + } + + public func mock( + cache: CacheConfig, + _ creation: (TestDependencies) -> R + ) -> R { + if let existingMock: R = dependencies.get(cache: cache) as? R { + return existingMock + } + + let value: R = creation(dependencies) + (value as? DependenciesSettable)?.setDependencies(dependencies) + + guard let conformingValue: MutableCache = value as? MutableCache else { + fatalError("Type Mismatch: The mock of type '\(type(of: value))' does not conform to the required protocol '\(MutableCache.self)' for the provided singleton key.") + } + + dependencies.set(cache: cache, to: conformingValue) + return value + } + + public func mock( + for defaults: UserDefaultsConfig, + _ creation: (TestDependencies) -> T + ) -> T { + if let existingMock: T = dependencies.get(defaults: defaults) { + return existingMock + } + + let value: T = creation(dependencies) + (value as? DependenciesSettable)?.setDependencies(dependencies) + dependencies.set(defaults: defaults, to: value) + + return value + } + + // MARK: - No Dependencies Convenience + + public func mock( + for singleton: SingletonConfig, + _ creation: () -> R + ) -> R { + return mock(for: singleton) { _ in creation() } + } + + public func mock( + cache: CacheConfig, + _ creation: () -> R + ) -> R { + return mock(cache: cache) { _ in creation() } + } + + public func mock( + for defaults: UserDefaultsConfig, + _ creation: () -> T + ) -> T { + return mock(for: defaults) { _ in creation() } + } + + // MARK: - Mockable Convenience + + public func mock(for singleton: SingletonConfig) -> R { + return mock(for: singleton) { _ in R.create() } + } + + public func mock(cache: CacheConfig) -> R { + return mock(cache: cache) { _ in R.create() } + } + + public func mock(defaults: UserDefaultsConfig) -> T { + return mock(for: defaults) { _ in T.create() } + } +} diff --git a/_SharedTestUtilities/Mock.swift b/_SharedTestUtilities/Mock.swift index 7b493a3d7b..eb40040f7f 100644 --- a/_SharedTestUtilities/Mock.swift +++ b/_SharedTestUtilities/Mock.swift @@ -3,19 +3,14 @@ import Foundation import Combine import SessionUtilitiesKit - -// MARK: - MockError - -public enum MockError: Error { - case mockedData -} +import TestUtilities // MARK: - Mock public class Mock: DependenciesSettable, InitialSetupable { private var _dependencies: Dependencies! - private let functionHandler: MockFunctionHandler - internal let functionConsumer: FunctionConsumer + private let functionHandler: MockFunctionHandler_Old + internal let functionConsumer: FunctionConsumer_Old public var dependencies: Dependencies { _dependencies } private var initialSetup: ((Mock) -> ())? @@ -23,10 +18,10 @@ public class Mock: DependenciesSettable, InitialSetupable { // MARK: - Initialization internal required init( - functionHandler: MockFunctionHandler? = nil, + functionHandler: MockFunctionHandler_Old? = nil, initialSetup: ((Mock) -> ())? = nil ) { - self.functionConsumer = FunctionConsumer() + self.functionConsumer = FunctionConsumer_Old() self.functionHandler = (functionHandler ?? self.functionConsumer) self.initialSetup = initialSetup } @@ -39,12 +34,12 @@ public class Mock: DependenciesSettable, InitialSetupable { // MARK: - InitialSetupable - func performInitialSetup() { + public func performInitialSetup() { self.initialSetup?(self) self.initialSetup = nil } - // MARK: - MockFunctionHandler + // MARK: - MockFunctionHandler_Old @discardableResult internal func mock(funcName: String = #function, generics: [Any.Type] = [], args: [Any?] = [], untrackedArgs: [Any?] = []) -> Output { return functionHandler.mock( @@ -113,27 +108,27 @@ public class Mock: DependenciesSettable, InitialSetupable { } internal func removeMocksFor(_ callBlock: @escaping (inout T) throws -> R) { - let builder: MockFunctionBuilder = MockFunctionBuilder(callBlock, mockInit: type(of: self).init) + let builder: MockFunctionBuilder_Old = MockFunctionBuilder_Old(callBlock, mockInit: type(of: self).init) functionConsumer.removeBuilder(builder.build) } - internal func when(_ callBlock: @escaping (inout T) throws -> R) -> MockFunctionBuilder { - let builder: MockFunctionBuilder = MockFunctionBuilder(callBlock, mockInit: type(of: self).init) + internal func when(_ callBlock: @escaping (inout T) throws -> R) -> MockFunctionBuilder_Old { + let builder: MockFunctionBuilder_Old = MockFunctionBuilder_Old(callBlock, mockInit: type(of: self).init) functionConsumer.addBuilder(builder.build) return builder } - internal func when(_ callBlock: @escaping (inout T) async throws -> R) -> MockFunctionBuilder { - let builder: MockFunctionBuilder = MockFunctionBuilder(callBlock, mockInit: type(of: self).init) + internal func when(_ callBlock: @escaping (inout T) async throws -> R) -> MockFunctionBuilder_Old { + let builder: MockFunctionBuilder_Old = MockFunctionBuilder_Old(callBlock, mockInit: type(of: self).init) functionConsumer.addBuilder(builder.build) return builder } internal func allCalls(_ functionBlock: @escaping (inout T) async throws -> R) -> [CallDetails]? { - let maybeTargetFunction: MockFunction? = try? MockFunctionBuilder.mockFunctionWith(self, functionBlock) - let key: FunctionConsumer.Key = FunctionConsumer.Key( + let maybeTargetFunction: MockFunction? = try? MockFunctionBuilder_Old.mockFunctionWith(self, functionBlock) + let key: FunctionConsumer_Old.Key = FunctionConsumer_Old.Key( name: (maybeTargetFunction?.name ?? ""), generics: (maybeTargetFunction?.generics ?? []), paramCount: (maybeTargetFunction?.parameterCount ?? 0) @@ -155,10 +150,9 @@ public class Mock: DependenciesSettable, InitialSetupable { } private func summary(for argument: Any) -> String { - if - let customDescribable: CustomArgSummaryDescribable = argument as? CustomArgSummaryDescribable, - let customArgSummaryDescribable: String = customDescribable.customArgSummaryDescribable - { return customArgSummaryDescribable } + if let customSummary: String = (argument as? ArgumentDescribing)?.summary { + return customSummary + } switch argument { case let string as String: return string @@ -269,9 +263,9 @@ public class Mock: DependenciesSettable, InitialSetupable { } } -// MARK: - MockFunctionHandler +// MARK: - MockFunctionHandler_Old -protocol MockFunctionHandler { +protocol MockFunctionHandler_Old { func mock( _ functionName: String, parameterCount: Int, @@ -380,11 +374,11 @@ internal class MockFunction { } } -// MARK: - MockFunctionBuilder +// MARK: - MockFunctionBuilder_Old -internal class MockFunctionBuilder: MockFunctionHandler { +internal class MockFunctionBuilder_Old: MockFunctionHandler_Old { private let callBlock: (inout T) async throws -> R - private let mockInit: (MockFunctionHandler?, ((Mock) -> ())?) -> Mock + private let mockInit: (MockFunctionHandler_Old?, ((Mock) -> ())?) -> Mock private var functionName: String? private var parameterCount: Int? private var parameterSummary: String? @@ -405,7 +399,7 @@ internal class MockFunctionBuilder: MockFunctionHandler { // MARK: - Initialization - init(_ callBlock: @escaping (inout T) async throws -> R, mockInit: @escaping (MockFunctionHandler?, ((Mock) -> ())?) -> Mock) { + init(_ callBlock: @escaping (inout T) async throws -> R, mockInit: @escaping (MockFunctionHandler_Old?, ((Mock) -> ())?) -> Mock) { self.callBlock = callBlock self.mockInit = mockInit } @@ -414,11 +408,11 @@ internal class MockFunctionBuilder: MockFunctionHandler { _ validInstance: M, _ functionBlock: @escaping (inout T) async throws -> R ) throws -> MockFunction? where M: Mock { - let builder: MockFunctionBuilder = MockFunctionBuilder(functionBlock, mockInit: type(of: validInstance).init) + let builder: MockFunctionBuilder_Old = MockFunctionBuilder_Old(functionBlock, mockInit: type(of: validInstance).init) builder.returnValueGenerator = { name, generics, parameterCount, parameterSummary, allParameterSummaryCombinations in validInstance.functionConsumer .firstFunction( - for: FunctionConsumer.Key(name: name, generics: generics, paramCount: parameterCount), + for: FunctionConsumer_Old.Key(name: name, generics: generics, paramCount: parameterCount), matchingParameterSummaryIfPossible: parameterSummary, allParameterSummaryCombinations: allParameterSummaryCombinations )? @@ -431,25 +425,25 @@ internal class MockFunctionBuilder: MockFunctionHandler { // MARK: - Behaviours /// Closure parameter is an array of arguments called by the function - @discardableResult func then(_ block: @escaping ([Any?]) -> Void) -> MockFunctionBuilder { + @discardableResult func then(_ block: @escaping ([Any?]) -> Void) -> MockFunctionBuilder_Old { actions.append({ args, _ in block(args) }) return self } /// Closure parameter is an array of arguments called by the function - @discardableResult func then(_ block: @escaping ([Any?]) async -> Void) -> MockFunctionBuilder { + @discardableResult func then(_ block: @escaping ([Any?]) async -> Void) -> MockFunctionBuilder_Old { asyncActions.append({ args, _ in await block(args) }) return self } /// Closure parameters are an array of arguments, followed by an array of "untracked" arguments called by the function - @discardableResult func then(_ block: @escaping ([Any?], [Any?]) -> Void) -> MockFunctionBuilder { + @discardableResult func then(_ block: @escaping ([Any?], [Any?]) -> Void) -> MockFunctionBuilder_Old { actions.append(block) return self } /// Closure parameters are an array of arguments, followed by an array of "untracked" arguments called by the function - @discardableResult func then(_ block: @escaping ([Any?], [Any?]) async -> Void) -> MockFunctionBuilder { + @discardableResult func then(_ block: @escaping ([Any?], [Any?]) async -> Void) -> MockFunctionBuilder_Old { asyncActions.append(block) return self } @@ -475,7 +469,7 @@ internal class MockFunctionBuilder: MockFunctionHandler { returnError = error } - // MARK: - MockFunctionHandler + // MARK: - MockFunctionHandler_Old func mock( _ functionName: String, @@ -563,11 +557,11 @@ internal class MockFunctionBuilder: MockFunctionHandler { guard let numericType: (any Numeric.Type) = Output.self as? any Numeric.Type, let convertedValue: Output = convertNumeric(value, to: numericType) as? Output - else { throw MockError.mockedData } + else { throw MockError.mock } return convertedValue - default: return try Optional.none as? Output ?? { throw MockError.mockedData }() + default: return try Optional.none as? Output ?? { throw MockError.mock }() } } @@ -646,7 +640,7 @@ internal class MockFunctionBuilder: MockFunctionHandler { // MARK: - Combine Convenience -extension MockFunctionBuilder { +extension MockFunctionBuilder_Old { func thenReturn(_ value: [Element]) where R == AnyPublisher<[Element], Never> { returnValue = Just(value) .setFailureType(to: Never.self) @@ -672,23 +666,15 @@ extension MockFunctionBuilder { } } -// MARK: - DependenciesSettable - -protocol DependenciesSettable { - var dependencies: Dependencies { get } - - func setDependencies(_ dependencies: Dependencies?) -} - // MARK: - InitialSetupable protocol InitialSetupable { func performInitialSetup() } -// MARK: - FunctionConsumer +// MARK: - FunctionConsumer_Old -internal class FunctionConsumer: MockFunctionHandler { +internal class FunctionConsumer_Old: MockFunctionHandler_Old { internal struct Key: Equatable, Hashable { let name: String let paramCount: Int @@ -893,12 +879,12 @@ internal class FunctionConsumer: MockFunctionHandler { guard let numericType: (any Numeric.Type) = Output.self as? any Numeric.Type, let convertedValue: Output = convertNumeric(value, to: numericType) as? Output - else { throw MockError.mockedData } + else { throw MockError.mock } return convertedValue case (_, _, .some(let closure)): return closure(args, untrackedArgs) as! Output - default: return try Optional.none as? Output ?? { throw MockError.mockedData }() + default: return try Optional.none as? Output ?? { throw MockError.mock }() } } @@ -1000,7 +986,7 @@ internal class FunctionConsumer: MockFunctionHandler { // MARK: - Conversion Convenience -private extension MockFunctionHandler { +private extension MockFunctionHandler_Old { func convertNumeric(_ value: Any, to type: any Numeric.Type) -> (any Numeric)? { switch (value, type) { case (let x as any BinaryInteger, is Int64.Type): return Int64(x) @@ -1035,9 +1021,3 @@ private extension MockFunctionHandler { } } } - -// MARK: - CustomArgSummaryDescribable - -protocol CustomArgSummaryDescribable { - var customArgSummaryDescribable: String? { get } -} diff --git a/_SharedTestUtilities/MockAppContext.swift b/_SharedTestUtilities/MockAppContext.swift index 325498e134..cfb68fb9a7 100644 --- a/_SharedTestUtilities/MockAppContext.swift +++ b/_SharedTestUtilities/MockAppContext.swift @@ -2,36 +2,47 @@ import UIKit import SessionUtilitiesKit +import TestUtilities -class MockAppContext: Mock, AppContext { - var mainWindow: UIWindow? { mock() } - var frontMostViewController: UIViewController? { mock() } - - var isValid: Bool { mock() } - var appLaunchTime: Date { mock() } - var isMainApp: Bool { mock() } - var isMainAppAndActive: Bool { mock() } - var isShareExtension: Bool { mock() } - var reportedApplicationState: UIApplication.State { mock() } - var backgroundTimeRemaining: TimeInterval { mock() } +class MockAppContext: AppContext, Mockable { + public var handler: MockHandler + + required init(handler: MockHandler) { + self.handler = handler + } + + required init(handlerForBuilder: any MockFunctionHandler) { + self.handler = MockHandler(forwardingHandler: handlerForBuilder) + } + + var mainWindow: UIWindow? { handler.mock() } + var frontMostViewController: UIViewController? { handler.mock() } + + var isValid: Bool { handler.mock() } + var appLaunchTime: Date { handler.mock() } + var isMainApp: Bool { handler.mock() } + var isMainAppAndActive: Bool { handler.mock() } + var isShareExtension: Bool { handler.mock() } + var reportedApplicationState: UIApplication.State { handler.mock() } + var backgroundTimeRemaining: TimeInterval { handler.mock() } // Override the extension functions - var isInBackground: Bool { mock() } - var isAppForegroundAndActive: Bool { mock() } + var isInBackground: Bool { handler.mock() } + var isAppForegroundAndActive: Bool { handler.mock() } func setMainWindow(_ mainWindow: UIWindow) { - mockNoReturn(args: [mainWindow]) + handler.mockNoReturn(args: [mainWindow]) } func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjects: [Any]) { - mockNoReturn(args: [shouldBeBlocking, blockingObjects]) + handler.mockNoReturn(args: [shouldBeBlocking, blockingObjects]) } func beginBackgroundTask(expirationHandler: @escaping () -> ()) -> UIBackgroundTaskIdentifier { - return mock(args: [expirationHandler]) + return handler.mock(args: [expirationHandler]) } func endBackgroundTask(_ backgroundTaskIdentifier: UIBackgroundTaskIdentifier) { - mockNoReturn(args: [backgroundTaskIdentifier]) + handler.mockNoReturn(args: [backgroundTaskIdentifier]) } } diff --git a/_SharedTestUtilities/Mocked.swift b/_SharedTestUtilities/Mocked.swift deleted file mode 100644 index 4d4aa57415..0000000000 --- a/_SharedTestUtilities/Mocked.swift +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import UIKit.UIApplication -import Combine -import GRDB -import SessionUtilitiesKit - -// MARK: - Mocked - -protocol Mocked { static var mock: Self { get } } -protocol MockedGeneric { - associatedtype Generic - - static func mock(type: Generic.Type) -> Self -} -protocol MockedDoubleGeneric { - associatedtype GenericA - associatedtype GenericB - - static func mock(typeA: GenericA.Type, typeB: GenericB.Type) -> Self -} - -// MARK: - DSL - -/// Needs to be a function as you can't extend 'Any' -func anyAny() -> Any { 0 } - -extension Mocked { static var any: Self { mock } } - -extension Int: Mocked { static var mock: Int { 0 } } -extension Int64: Mocked { static var mock: Int64 { 0 } } -extension Dictionary: Mocked { static var mock: Self { [:] } } -extension Array: Mocked { static var mock: Self { [] } } -extension Set: Mocked { static var mock: Self { [] } } -extension Float: Mocked { static var mock: Float { 0 } } -extension Double: Mocked { static var mock: Double { 0 } } -extension String: Mocked { static var mock: String { "" } } -extension Data: Mocked { static var mock: Data { Data() } } -extension Bool: Mocked { static var mock: Bool { false } } -extension UnsafeMutablePointer?: Mocked { static var mock: UnsafeMutablePointer? { nil } } - -// The below types either can't be mocked or use the 'MockedGeneric' or 'MockedDoubleGeneric' types -// so need their own direct 'any' values - -extension Error { static var any: Error { TestError.mock } } - -extension UIApplication.State { static var any: UIApplication.State { .active } } -extension TimeInterval { static var any: TimeInterval { 0 } } -extension SessionId { static var any: SessionId { SessionId.invalid } } -extension Dependencies { - static var any: Dependencies { - TestDependencies { dependencies in - dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) - dependencies.forceSynchronous = true - } - } -} - -// MARK: - Conformance - -extension ObservingDatabase: Mocked { - static var mock: Self { - var result: Database! - try! DatabaseQueue().read { result = $0 } - return ObservingDatabase.create(result!, using: .any) as! Self - } -} - -extension ObservedEvent: Mocked { - static var mock: ObservedEvent = ObservedEvent(key: "mock", value: nil) -} - -extension UUID: Mocked { - static var mock: UUID = UUID(uuidString: "00000000-0000-0000-0000-000000000001")! -} - -extension URL: Mocked { - static var mock: URL = URL(fileURLWithPath: "mock") -} - -extension URLRequest: Mocked { - static var mock: URLRequest = URLRequest(url: URL(fileURLWithPath: "mock")) -} - -extension AnyPublisher: MockedGeneric where Failure == Error { - typealias Generic = Output - - static func any(type: Output.Type) -> AnyPublisher { mock(type: type) } - - static func mock(type: Output.Type) -> AnyPublisher { - return Fail(error: MockError.mockedData).eraseToAnyPublisher() - } -} - -extension KeyPair: Mocked { - static var mock: KeyPair = KeyPair( - publicKey: Data(hex: TestConstants.publicKey).bytes, - secretKey: Data(hex: TestConstants.edSecretKey).bytes - ) -} - -extension Job: Mocked { - static var mock: Job = Job(variant: .mock) -} - -extension Job.Variant: Mocked { - static var mock: Job.Variant = .messageSend -} - -extension JobRunner.JobResult: Mocked { - static var mock: JobRunner.JobResult = .succeeded -} - -extension FileProtectionType: Mocked { - static var mock: FileProtectionType = .complete -} - -extension Log.Category: Mocked { - static var mock: Log.Category = .create("mock", defaultLevel: .debug) -} - -extension Setting.BoolKey: Mocked { - static var mock: Setting.BoolKey = "mockBool" -} - -extension Setting.EnumKey: Mocked { - static var mock: Setting.EnumKey = "mockEnum" -} - -extension FileManager.ItemReplacementOptions: Mocked { - static var mock: FileManager.ItemReplacementOptions = FileManager.ItemReplacementOptions() -} - -// MARK: - Encodable Convenience - -extension Mocked where Self: Encodable { - func encoded(using dependencies: Dependencies) -> Data { - try! JSONEncoder(using: dependencies).with(outputFormatting: .sortedKeys).encode(self) - } -} - -extension MockedGeneric where Self: Encodable { - func encoded(using dependencies: Dependencies) -> Data { - try! JSONEncoder(using: dependencies).with(outputFormatting: .sortedKeys).encode(self) - } -} - -extension Array where Element: Encodable { - func encoded(using dependencies: Dependencies) -> Data { - try! JSONEncoder(using: dependencies).with(outputFormatting: .sortedKeys).encode(self) - } -} diff --git a/_SharedTestUtilities/NimbleExtensions.swift b/_SharedTestUtilities/NimbleExtensions.swift index 12e0786b16..435d7b42b7 100644 --- a/_SharedTestUtilities/NimbleExtensions.swift +++ b/_SharedTestUtilities/NimbleExtensions.swift @@ -250,7 +250,7 @@ fileprivate struct CallInfo { let didError: Bool let caughtError: Error? let targetFunction: MockFunction? - let allFunctionsCalled: [FunctionConsumer.Key] + let allFunctionsCalled: [FunctionConsumer_Old.Key] let allCallDetails: [CallDetails] var functionName: String { "\((targetFunction?.name).map { "\($0)" } ?? "a function")" } @@ -271,7 +271,7 @@ fileprivate struct CallInfo { didError: Bool = false, caughtError: Error?, targetFunction: MockFunction?, - allFunctionsCalled: [FunctionConsumer.Key], + allFunctionsCalled: [FunctionConsumer_Old.Key], allCallDetails: [CallDetails] ) { self.didError = didError @@ -287,7 +287,7 @@ fileprivate func generateCallInfo( _ functionBlock: @escaping (inout T) async throws -> R ) -> CallInfo where M: Mock { var maybeTargetFunction: MockFunction? - var allFunctionsCalled: [FunctionConsumer.Key] = [] + var allFunctionsCalled: [FunctionConsumer_Old.Key] = [] var allCallDetails: [CallDetails] = [] var caughtError: Error? = nil @@ -304,9 +304,9 @@ fileprivate func generateCallInfo( // to build) if !allFunctionsCalled.isEmpty { validInstance.functionConsumer.trackCalls = false - maybeTargetFunction = try MockFunctionBuilder.mockFunctionWith(validInstance, functionBlock) + maybeTargetFunction = try MockFunctionBuilder_Old.mockFunctionWith(validInstance, functionBlock) - let key: FunctionConsumer.Key = FunctionConsumer.Key( + let key: FunctionConsumer_Old.Key = FunctionConsumer_Old.Key( name: (maybeTargetFunction?.name ?? ""), generics: (maybeTargetFunction?.generics ?? []), paramCount: (maybeTargetFunction?.parameterCount ?? 0) diff --git a/_SharedTestUtilities/Quick+TestContext.swift b/_SharedTestUtilities/Quick+TestContext.swift new file mode 100644 index 0000000000..deefc3726e --- /dev/null +++ b/_SharedTestUtilities/Quick+TestContext.swift @@ -0,0 +1,35 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Quick +import TestUtilities + +public extension AsyncSpec { + static func itTracked( + _ description: String, + fileID: String = #fileID, + file: String = #file, + line: UInt = #line, + closure: @escaping () async throws -> Void + ) { + it(description, file: file, line: line) { + try await withTestContext(fileID: fileID, file: file, line: line) { + try await closure() + } + } + } + + static func fitTracked( + _ description: String, + fileID: String = #fileID, + file: String = #file, + line: UInt = #line, + closure: @escaping () async throws -> Void + ) { + fit(description, file: file, line: line) { + try await withTestContext(fileID: fileID, file: file, line: line) { + try await closure() + } + } + } +} diff --git a/_SharedTestUtilities/SynchronousStorage.swift b/_SharedTestUtilities/SynchronousStorage.swift index 59510cf436..63c6b0a5ac 100644 --- a/_SharedTestUtilities/SynchronousStorage.swift +++ b/_SharedTestUtilities/SynchronousStorage.swift @@ -2,48 +2,20 @@ import Combine import GRDB +import TestUtilities @testable import SessionUtilitiesKit -class SynchronousStorage: Storage, DependenciesSettable, InitialSetupable { - public var dependencies: Dependencies - private let initialData: ((ObservingDatabase) throws -> ())? - - public init( - customWriter: DatabaseWriter? = nil, - migrations: [Migration.Type]? = nil, - using dependencies: Dependencies, - initialData: ((ObservingDatabase) throws -> ())? = nil - ) { - self.dependencies = dependencies - self.initialData = initialData - - super.init(customWriter: customWriter, using: dependencies) - - if let migrations: [Migration.Type] = migrations { - perform( - migrations: migrations, - async: false, - onProgressUpdate: nil, - onComplete: { _ in } - ) - } - } +class SynchronousStorage: Storage, DependenciesSettable { + public var _dependencies: Dependencies! + public var dependencies: Dependencies { _dependencies } // MARK: - DependenciesSettable func setDependencies(_ dependencies: Dependencies?) { guard let dependencies: Dependencies = dependencies else { return } - self.dependencies = dependencies - } - - // MARK: - InitialSetupable - - func performInitialSetup() { - guard let closure: ((ObservingDatabase) throws -> ()) = initialData else { return } - - write { db in try closure(db) } + self._dependencies = dependencies } // MARK: - Overwritten Functions diff --git a/_SharedTestUtilities/TestDependencies.swift b/_SharedTestUtilities/TestDependencies.swift index 0815ea21df..2f68028da3 100644 --- a/_SharedTestUtilities/TestDependencies.swift +++ b/_SharedTestUtilities/TestDependencies.swift @@ -1,7 +1,9 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation +import _Concurrency import Quick +import TestUtilities @testable import SessionUtilitiesKit @@ -204,6 +206,22 @@ public class TestDependencies: Dependencies { // MARK: - Instance replacing + public func get(singleton: SingletonConfig) -> S? { + return _singletonInstances.performMap { $0[singleton.identifier] as? S } + } + + public func get(cache: CacheConfig) -> M? { + return _cacheInstances.performMap { $0[cache.identifier] as? M } + } + + public func get(defaults: UserDefaultsConfig) -> T? { + return _defaultsInstances.performMap { $0[defaults.identifier] as? T } + } + + public func get(feature: FeatureConfig) -> T? { + return _featureInstances.performMap { $0[feature.identifier] as? T } + } + public override func set(singleton: SingletonConfig, to instance: S) { _singletonInstances.performUpdate { $0.setting(singleton.identifier, instance) } } @@ -212,11 +230,27 @@ public class TestDependencies: Dependencies { _cacheInstances.performUpdate { $0.setting(cache.identifier, cache.mutableInstance(instance)) } } + public func set(defaults: UserDefaultsConfig, to instance: T) { + _defaultsInstances.performUpdate { $0.setting(defaults.identifier, instance) } + } + + public func set(feature: FeatureConfig, to instance: Feature) { + _featureInstances.performUpdate { $0.setting(feature.identifier, instance) } + } + public override func remove(cache: CacheConfig) { _cacheInstances.performUpdate { $0.setting(cache.identifier, nil) } } } +// MARK: - DependenciesSettable + +protocol DependenciesSettable { + var dependencies: Dependencies { get } + + func setDependencies(_ dependencies: Dependencies?) +} + // MARK: - TestState Convenience internal extension TestState { @@ -224,7 +258,7 @@ internal extension TestState { wrappedValue: @escaping @autoclosure () -> T?, cache: CacheConfig, in dependenciesRetriever: @escaping @autoclosure () -> TestDependencies? - ) where T: MutableCacheType { + ) async where T: MutableCacheType { self.init(wrappedValue: { let dependencies: TestDependencies? = dependenciesRetriever() let value: T? = wrappedValue() @@ -267,18 +301,4 @@ internal extension TestState { return value }()) } - - static func create( - closure: @escaping () async -> T? - ) -> T? { - var value: T? - let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) - Task { - value = await closure() - semaphore.signal() - } - semaphore.wait() - - return value - } } From 2d0d88c01384bcf1642cfe739936e1dee0003764 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 8 Sep 2025 14:29:09 +1000 Subject: [PATCH 37/59] Fixed unit test build errors (tests still broken) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fixed unit test build issues • Removed the "TestContext" as while it works for Quick/Nimble, it wouldn't work for Swift Testing without needing `withTestContext` boilerplate around every test (now missing mocks will show their errors against the mock function instead of the calling test) • Updated the `then{Return/Throw}` functions to throw so we can indicate failures during mocking without relying of the failureReporter --- Session.xcodeproj/project.pbxproj | 14 - .../xcshareddata/swiftpm/Package.resolved | 4 +- .../UIContextualAction+Utilities.swift | 4 +- .../Crypto/CryptoSMKSpec.swift | 13 +- .../Models/MessageDeduplicationSpec.swift | 5 +- .../Jobs/DisplayPictureDownloadJobSpec.swift | 78 ++- .../Jobs/MessageSendJobSpec.swift | 43 +- ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 127 +++- .../LibSession/LibSessionGroupInfoSpec.swift | 116 ++-- .../LibSessionGroupMembersSpec.swift | 72 ++- .../LibSession/LibSessionSpec.swift | 63 +- .../LibSession/LibSessionUtilSpec.swift | 3 +- .../Crypto/CryptoOpenGroupAPISpec.swift | 13 +- .../Open Groups/OpenGroupAPISpec.swift | 117 +++- .../Open Groups/OpenGroupManagerSpec.swift | 296 ++++++--- .../MessageReceiverGroupsSpec.swift | 215 +++---- .../MessageSenderGroupsSpec.swift | 599 +++++++++++------- .../MessageSenderSpec.swift | 29 +- .../NotificationsManagerSpec.swift | 26 +- .../Pollers/CommunityPollerManagerSpec.swift | 16 +- .../SessionThreadViewModelSpec.swift | 11 +- .../Utilities/ExtensionHelperSpec.swift | 26 +- .../Types/PreparedRequestSendingSpec.swift | 30 +- .../ThreadSettingsViewModelSpec.swift | 72 ++- SessionTests/Database/DatabaseSpec.swift | 91 ++- SessionTests/Onboarding/OnboardingSpec.swift | 222 +++---- .../NotificationContentViewModelSpec.swift | 40 +- .../Database/Models/IdentitySpec.swift | 7 +- TestUtilities/MockFunctionBuilder.swift | 19 +- TestUtilities/MockHandler.swift | 19 +- TestUtilities/TextContext.swift | 28 - _SharedTestUtilities/MockGeneralCache.swift | 13 + _SharedTestUtilities/Quick+TestContext.swift | 35 - _SharedTestUtilities/TestDependencies.swift | 16 - 34 files changed, 1386 insertions(+), 1096 deletions(-) delete mode 100644 TestUtilities/TextContext.swift delete mode 100644 _SharedTestUtilities/Quick+TestContext.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 2d528f1939..d5bde3222d 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -755,11 +755,6 @@ FD6B92672E697012004463B5 /* Mocked+SUK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92632E696EDC004463B5 /* Mocked+SUK.swift */; }; FD6B926C2E6A7644004463B5 /* Async+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B926B2E6A763D004463B5 /* Async+Utilities.swift */; }; FD6B92722E6AB045004463B5 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = FD6B92712E6AB045004463B5 /* Quick */; }; - FD6B92742E6AB097004463B5 /* TextContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92732E6AB095004463B5 /* TextContext.swift */; }; - FD6B92752E6AB25F004463B5 /* Quick+TestContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B926F2E6AB01F004463B5 /* Quick+TestContext.swift */; }; - FD6B92762E6AB25F004463B5 /* Quick+TestContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B926F2E6AB01F004463B5 /* Quick+TestContext.swift */; }; - FD6B92772E6AB25F004463B5 /* Quick+TestContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B926F2E6AB01F004463B5 /* Quick+TestContext.swift */; }; - FD6B92782E6AB25F004463B5 /* Quick+TestContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B926F2E6AB01F004463B5 /* Quick+TestContext.swift */; }; FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */; }; FD6D9CF92CA152B300F706A8 /* Session+SNUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754BB2C9B9A8E002A2623 /* Session+SNUIKit.swift */; }; FD6DA9CF2D015B440092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9CE2D015B440092085A /* Lucide */; }; @@ -2100,8 +2095,6 @@ FD6B925D2E695ACD004463B5 /* FixtureBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixtureBase.swift; sourceTree = ""; }; FD6B92632E696EDC004463B5 /* Mocked+SUK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mocked+SUK.swift"; sourceTree = ""; }; FD6B926B2E6A763D004463B5 /* Async+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Async+Utilities.swift"; sourceTree = ""; }; - FD6B926F2E6AB01F004463B5 /* Quick+TestContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Quick+TestContext.swift"; sourceTree = ""; }; - FD6B92732E6AB095004463B5 /* TextContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextContext.swift; sourceTree = ""; }; FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopSessionCallManager.swift; sourceTree = ""; }; FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _021_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = ""; }; @@ -4203,7 +4196,6 @@ FD1BDBDE2E655734008EF998 /* MockFunctionHandler.swift */, FD1BDBB12E6532DB008EF998 /* MockHandler.swift */, FD1BDBD62E65384F008EF998 /* RecordedCall.swift */, - FD6B92732E6AB095004463B5 /* TextContext.swift */, FD1BDBFA2E656538008EF998 /* TestFailureReporter.swift */, ); path = TestUtilities; @@ -4682,7 +4674,6 @@ FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */, FD23EA6028ED0B260058676E /* CombineExtensions.swift */, FD0150272CA23DB7005B08A1 /* GRDBExtensions.swift */, - FD6B926F2E6AB01F004463B5 /* Quick+TestContext.swift */, FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */, ); path = _SharedTestUtilities; @@ -7061,7 +7052,6 @@ FD1BDBDF2E655735008EF998 /* MockFunctionHandler.swift in Sources */, FD1BDBD92E653868008EF998 /* MockError.swift in Sources */, FD1BDBDB2E6538B4008EF998 /* MockFunctionBuilder.swift in Sources */, - FD6B92742E6AB097004463B5 /* TextContext.swift in Sources */, FD1BDBD32E653660008EF998 /* MockFunction.swift in Sources */, FD1BDBD72E653852008EF998 /* RecordedCall.swift in Sources */, FD1BDBD02E653625008EF998 /* Mockable.swift in Sources */, @@ -7109,7 +7099,6 @@ FDB11A572DD17D0600BEF49F /* MockLogger.swift in Sources */, FD49E2462B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, FD01502C2CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, - FD6B92752E6AB25F004463B5 /* Quick+TestContext.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7118,7 +7107,6 @@ buildActionMask = 2147483647; files = ( FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */, - FD6B92782E6AB25F004463B5 /* Quick+TestContext.swift in Sources */, FD01504E2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift in Sources */, FD65318D2AA025C500DFEEAA /* TestDependencies.swift in Sources */, FD49E2492B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, @@ -7186,7 +7174,6 @@ FD0150282CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, FDB5DB152A981FB0002C8721 /* SynchronousStorage.swift in Sources */, FDE754AC2C9B967E002A2623 /* FileUploadResponseSpec.swift in Sources */, - FD6B92772E6AB25F004463B5 /* Quick+TestContext.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7243,7 +7230,6 @@ FD481A902CAD16F100ECC4CF /* LibSessionGroupInfoSpec.swift in Sources */, FDE754A92C9B964D002A2623 /* MessageSenderGroupsSpec.swift in Sources */, FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */, - FD6B92762E6AB25F004463B5 /* Quick+TestContext.swift in Sources */, FD3FAB672AF0C47000DC5421 /* DisplayPictureDownloadJobSpec.swift in Sources */, FD72BDA42BE3690B00CF6CF6 /* CryptoSMKSpec.swift in Sources */, FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */, diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c6d1f1017c..28dc54d998 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/session-foundation/session-lucide.git", "state" : { - "revision" : "af00ad53d714823e07f984aadd7af38bafaae69e", - "version" : "0.473.0" + "revision" : "7da7fc6a2c42ee8549b0b9804455b43c61a0e63f", + "version" : "0.473.1" } }, { diff --git a/Session/Utilities/UIContextualAction+Utilities.swift b/Session/Utilities/UIContextualAction+Utilities.swift index 5b5ce426a4..488e266e1c 100644 --- a/Session/Utilities/UIContextualAction+Utilities.swift +++ b/Session/Utilities/UIContextualAction+Utilities.swift @@ -122,7 +122,7 @@ public extension UIContextualAction { case .clear: return UIContextualAction( title: "clear".localized(), - icon: Lucide.image(icon: .trash2, size: 24, color: .white), + icon: Lucide.image(icon: .trash2, size: 24), themeTintColor: .white, themeBackgroundColor: themeBackgroundColor, side: side, @@ -615,7 +615,7 @@ public extension UIContextualAction { case .delete: return UIContextualAction( title: "delete".localized(), - icon: Lucide.image(icon: .trash2, size: 24, color: .white), + icon: Lucide.image(icon: .trash2, size: 24), themeTintColor: .white, themeBackgroundColor: themeBackgroundColor, accessibility: Accessibility(identifier: "Delete button"), diff --git a/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift b/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift index 7de5f146de..4376e41fca 100644 --- a/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift +++ b/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift @@ -14,12 +14,13 @@ class CryptoSMKSpec: QuickSpec { @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .crypto, in: dependencies) var crypto: Crypto! = Crypto(using: dependencies) - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - cache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) - } - ) + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + } // MARK: - Crypto for SessionMessagingKit describe("Crypto for SessionMessagingKit") { diff --git a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift index 22fb802b43..050f072c6d 100644 --- a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift +++ b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift @@ -18,7 +18,6 @@ class MessageDeduplicationSpec: AsyncSpec { @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, using: dependencies ) @TestState(singleton: .extensionHelper, in: dependencies) var mockExtensionHelper: MockExtensionHelper! = MockExtensionHelper( @@ -45,6 +44,10 @@ class MessageDeduplicationSpec: AsyncSpec { return result }() + beforeEach { + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + } + // MARK: - MessageDeduplication - Inserting describe("MessageDeduplication") { // MARK: -- when inserting diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index 130facfd6c..f5569fbd25 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -10,7 +10,7 @@ import Nimble @testable import SessionMessagingKit @testable import SessionUtilitiesKit -class DisplayPictureDownloadJobSpec: QuickSpec { +class DisplayPictureDownloadJobSpec: AsyncSpec { override class func spec() { // MARK: Configuration @@ -22,19 +22,10 @@ class DisplayPictureDownloadJobSpec: QuickSpec { dependencies.forceSynchronous = true dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) } - @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { $0.defaultInitialSetup() } - ) + @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, - using: dependencies, - initialData: { db in - try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) - try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - } + using: dependencies ) @TestState var imageData: Data! = Data( hex: "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c" + @@ -51,7 +42,16 @@ class DisplayPictureDownloadJobSpec: QuickSpec { @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( initialSetup: { network in network - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.response(data: encryptedData)) } ) @@ -89,15 +89,24 @@ class DisplayPictureDownloadJobSpec: QuickSpec { .thenReturn(nil) } ) - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - cache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) - cache - .when { $0.ed25519Seed } - .thenReturn(Array(Array(Data(hex: TestConstants.edSecretKey)).prefix(upTo: 32))) + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + + mockLibSessionCache.defaultInitialSetup() + dependencies.set(cache: .libSession, to: mockLibSessionCache) + + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + try await mockStorage.writeAsync { db in + try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) + try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) } - ) + } // MARK: - a DisplayPictureDownloadJob describe("a DisplayPictureDownloadJob") { @@ -506,10 +515,12 @@ class DisplayPictureDownloadJobSpec: QuickSpec { expect(mockNetwork) .to(call(.exactly(times: 1), matchingParameters: .all) { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, + endpoint: expectedRequest.endpoint, + destination: expectedRequest.destination, + body: expectedRequest.body, + category: .upload, requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + overallTimeout: expectedRequest.overallTimeout ) }) } @@ -569,10 +580,12 @@ class DisplayPictureDownloadJobSpec: QuickSpec { expect(mockNetwork) .to(call(.exactly(times: 1), matchingParameters: .all) { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, + endpoint: expectedRequest.endpoint, + destination: expectedRequest.destination, + body: expectedRequest.body, + category: .upload, requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + overallTimeout: expectedRequest.overallTimeout ) }) } @@ -1089,7 +1102,16 @@ class DisplayPictureDownloadJobSpec: QuickSpec { // SOGS doesn't encrypt it's images so replace the encrypted mock response mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.response(data: imageData)) } diff --git a/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift b/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift index e549e2c221..ca27aa187f 100644 --- a/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift @@ -13,7 +13,7 @@ extension Job: @retroactive MutableIdentifiable { public mutating func setId(_ id: Int64?) { self.id = id } } -class MessageSendJobSpec: QuickSpec { +class MessageSendJobSpec: AsyncSpec { override class func spec() { // MARK: Configuration @@ -30,26 +30,10 @@ class MessageSendJobSpec: QuickSpec { @TestState var dependencies: TestDependencies! = TestDependencies { dependencies in dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) } - @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { $0.defaultInitialSetup() } - ) + @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, - using: dependencies, - initialData: { db in - try SessionThread.upsert( - db, - id: "Test1", - variant: .contact, - values: SessionThread.TargetValues( - creationDateTimestamp: .setTo(1234567890), - // False is the default and will mean we don't need libSession loaded - shouldBeVisible: .setTo(false) - ), - using: dependencies - ) - } + using: dependencies ) @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( initialSetup: { jobRunner in @@ -75,6 +59,27 @@ class MessageSendJobSpec: QuickSpec { } ) + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockLibSessionCache.defaultInitialSetup() + dependencies.set(cache: .libSession, to: mockLibSessionCache) + + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + try await mockStorage.writeAsync { db in + try SessionThread.upsert( + db, + id: "Test1", + variant: .contact, + values: SessionThread.TargetValues( + creationDateTimestamp: .setTo(1234567890), + // False is the default and will mean we don't need libSession loaded + shouldBeVisible: .setTo(false) + ), + using: dependencies + ) + } + } + // MARK: - a MessageSendJob describe("a MessageSendJob") { // MARK: -- fails when not given any details diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index 79692802c7..9119e7a537 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -10,7 +10,7 @@ import Nimble @testable import SessionMessagingKit @testable import SessionUtilitiesKit -class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { +class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { override class func spec() { // MARK: Configuration @@ -20,14 +20,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, - using: dependencies, - initialData: { db in - try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) - try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - } + using: dependencies ) @TestState(defaults: .appGroup, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults( initialSetup: { defaults in @@ -37,7 +30,16 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( initialSetup: { network in network - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn( MockNetwork.batchResponseData( with: [ @@ -78,25 +80,30 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { .thenReturn([:]) } ) - @TestState(cache: .openGroupManager, in: dependencies) var mockOGMCache: MockOGMCache! = MockOGMCache( - initialSetup: { cache in - cache.when { $0.setDefaultRoomInfo(.any) }.thenReturn(()) - } - ) - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - cache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) - cache - .when { $0.ed25519Seed } - .thenReturn(Array(Array(Data(hex: TestConstants.edSecretKey)).prefix(upTo: 32))) - } - ) + @TestState var mockOGMCache: MockOGMCache! = MockOGMCache() + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() @TestState var job: Job! = Job(variant: .retrieveDefaultOpenGroupRooms) @TestState var error: Error? = nil @TestState var permanentFailure: Bool! = false @TestState var wasDeferred: Bool! = false + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + + mockOGMCache.when { $0.setDefaultRoomInfo(.any) }.thenReturn(()) + dependencies.set(cache: .openGroupManager, to: mockOGMCache) + + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + try await mockStorage.writeAsync { db in + try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) + try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) + } + } + // MARK: - a RetrieveDefaultOpenGroupRoomsJob describe("a RetrieveDefaultOpenGroupRoomsJob") { // MARK: -- defers the job if the main app is not running @@ -177,7 +184,16 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { // MARK: -- creates an inactive entry in the database if one does not exist it("creates an inactive entry in the database if one does not exist") { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.errorResponse()) RetrieveDefaultOpenGroupRoomsJob.run( @@ -201,7 +217,16 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { // MARK: -- does not create a new entry if one already exists it("does not create a new entry if one already exists") { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.errorResponse()) mockStorage.write { db in @@ -275,10 +300,12 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { expect(mockNetwork) .to(call { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, + endpoint: OpenGroupAPI.Endpoint.sequence, + destination: expectedRequest.destination, + body: expectedRequest.body, + category: .standard, requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + overallTimeout: expectedRequest.overallTimeout ) }) } @@ -286,7 +313,16 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { // MARK: -- will retry 8 times before it fails it("will retry 8 times before it fails") { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.nullResponse()) RetrieveDefaultOpenGroupRoomsJob.run( @@ -304,7 +340,14 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { expect(error).to(matchError(NetworkError.parsingFailed)) expect(mockNetwork) // First attempt + 8 retries .to(call(.exactly(times: 9)) { network in - network.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) + network.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) }) } @@ -368,7 +411,16 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { .insert(db) } mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn( MockNetwork.batchResponseData( with: [ @@ -496,7 +548,16 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { // MARK: -- does not schedule a display picture download if there is no imageId it("does not schedule a display picture download if there is no imageId") { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn( MockNetwork.batchResponseData( with: [ diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift index 309aa452d8..204f4b4068 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift @@ -12,7 +12,7 @@ import Nimble @testable import SessionNetworkingKit @testable import SessionMessagingKit -class LibSessionGroupInfoSpec: QuickSpec { +class LibSessionGroupInfoSpec: AsyncSpec { override class func spec() { // MARK: Configuration @@ -20,27 +20,24 @@ class LibSessionGroupInfoSpec: QuickSpec { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) dependencies.forceSynchronous = true } - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - cache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) - } - ) + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, - using: dependencies, - initialData: { db in - try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) - try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - } + using: dependencies ) @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( initialSetup: { network in network - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.response(data: Data([1, 2, 3]))) } ) @@ -70,23 +67,36 @@ class LibSessionGroupInfoSpec: QuickSpec { ) } }() - @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { cache in - var conf: UnsafeMutablePointer! - var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) - _ = user_groups_init(&conf, &secretKey, nil, 0, nil) - - cache.defaultInitialSetup( - configs: [ - .userGroups: .userGroups(conf), - .groupInfo: createGroupOutput.groupState[.groupInfo], - .groupMembers: createGroupOutput.groupState[.groupMembers], - .groupKeys: createGroupOutput.groupState[.groupKeys] - ] - ) - cache.when { $0.configNeedsDump(.any) }.thenReturn(true) + @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() + + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + + var conf: UnsafeMutablePointer! + var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) + _ = user_groups_init(&conf, &secretKey, nil, 0, nil) + + mockLibSessionCache.defaultInitialSetup( + configs: [ + .userGroups: .userGroups(conf), + .groupInfo: createGroupOutput.groupState[.groupInfo], + .groupMembers: createGroupOutput.groupState[.groupMembers], + .groupKeys: createGroupOutput.groupState[.groupKeys] + ] + ) + mockLibSessionCache.when { $0.configNeedsDump(.any) }.thenReturn(true) + dependencies.set(cache: .libSession, to: mockLibSessionCache) + + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + try await mockStorage.writeAsync { db in + try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) + try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) } - ) + } // MARK: - LibSessionGroupInfo describe("LibSessionGroupInfo") { @@ -882,22 +892,24 @@ class LibSessionGroupInfoSpec: QuickSpec { ) } - let expectedRequest: Network.PreparedRequest<[String: Bool]> = try SnodeAPI.preparedDeleteMessages( - serverHashes: ["1234"], - requireSuccessfulDeletion: false, - authMethod: Authentication.groupAdmin( - groupSessionId: createGroupOutput.groupSessionId, - ed25519SecretKey: createGroupOutput.identityKeyPair.secretKey - ), - using: dependencies - ) expect(mockNetwork) .to(call(.exactly(times: 1), matchingParameters: .all) { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, - requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + endpoint: SnodeAPI.Endpoint.deleteMessages, + destination: .randomSnode(swarmPublicKey: createGroupOutput.groupSessionId.hexString), + body: try! JSONEncoder(using: dependencies).encode( + SnodeAPI.DeleteMessagesRequest( + messageHashes: ["1234"], + requireSuccessfulDeletion: false, + authMethod: Authentication.groupAdmin( + groupSessionId: createGroupOutput.groupSessionId, + ed25519SecretKey: createGroupOutput.identityKeyPair.secretKey + ) + ) + ), + category: .standard, + requestTimeout: Network.defaultTimeout, + overallTimeout: nil ) }) } @@ -956,10 +968,16 @@ class LibSessionGroupInfoSpec: QuickSpec { } expect(result?.count).to(equal(1)) expect(result?.map { $0.variant }).to(equal([.standardIncomingDeleted])) - expect(mockNetwork) - .toNot(call { network in - network.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) - }) + expect(mockNetwork).toNot(call { network in + network.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + }) } } } diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift index 43f6c045de..8854316acc 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift @@ -11,7 +11,7 @@ import Nimble @testable import SessionNetworkingKit @testable import SessionMessagingKit -class LibSessionGroupMembersSpec: QuickSpec { +class LibSessionGroupMembersSpec: AsyncSpec { override class func spec() { // MARK: Configuration @@ -19,27 +19,24 @@ class LibSessionGroupMembersSpec: QuickSpec { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) dependencies.forceSynchronous = true } - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - cache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) - } - ) + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, - using: dependencies, - initialData: { db in - try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) - try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - } + using: dependencies ) @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( initialSetup: { network in network - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.response(data: Data([1, 2, 3]))) } ) @@ -69,22 +66,35 @@ class LibSessionGroupMembersSpec: QuickSpec { ) } }() - @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { cache in - var conf: UnsafeMutablePointer! - var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) - _ = user_groups_init(&conf, &secretKey, nil, 0, nil) - - cache.defaultInitialSetup( - configs: [ - .userGroups: .userGroups(conf), - .groupInfo: createGroupOutput.groupState[.groupInfo], - .groupMembers: createGroupOutput.groupState[.groupMembers], - .groupKeys: createGroupOutput.groupState[.groupKeys] - ] - ) + @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() + + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + + var conf: UnsafeMutablePointer! + var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) + _ = user_groups_init(&conf, &secretKey, nil, 0, nil) + + mockLibSessionCache.defaultInitialSetup( + configs: [ + .userGroups: .userGroups(conf), + .groupInfo: createGroupOutput.groupState[.groupInfo], + .groupMembers: createGroupOutput.groupState[.groupMembers], + .groupKeys: createGroupOutput.groupState[.groupKeys] + ] + ) + dependencies.set(cache: .libSession, to: mockLibSessionCache) + + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + try await mockStorage.writeAsync { db in + try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) + try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) } - ) + } // MARK: - LibSessionGroupMembers describe("LibSessionGroupMembers") { diff --git a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift index cb8ff17dd8..adaa01b2e4 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift @@ -11,7 +11,7 @@ import Nimble @testable import SessionNetworkingKit @testable import SessionMessagingKit -class LibSessionSpec: QuickSpec { +class LibSessionSpec: AsyncSpec { override class func spec() { // MARK: Configuration @@ -19,22 +19,10 @@ class LibSessionSpec: QuickSpec { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) dependencies.forceSynchronous = true } - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - cache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) - } - ) + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, - using: dependencies, - initialData: { db in - try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) - try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - } + using: dependencies ) @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( initialSetup: { crypto in @@ -80,24 +68,37 @@ class LibSessionSpec: QuickSpec { ) } }() - @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { cache in - var conf: UnsafeMutablePointer! - var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) - _ = user_groups_init(&conf, &secretKey, nil, 0, nil) - - cache.defaultInitialSetup( - configs: [ - .userGroups: .userGroups(conf), - .groupInfo: createGroupOutput.groupState[.groupInfo], - .groupMembers: createGroupOutput.groupState[.groupMembers], - .groupKeys: createGroupOutput.groupState[.groupKeys] - ] - ) - } - ) + @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() @TestState var userGroupsConfig: LibSession.Config! + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + + var conf: UnsafeMutablePointer! + var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) + _ = user_groups_init(&conf, &secretKey, nil, 0, nil) + + mockLibSessionCache.defaultInitialSetup( + configs: [ + .userGroups: .userGroups(conf), + .groupInfo: createGroupOutput.groupState[.groupInfo], + .groupMembers: createGroupOutput.groupState[.groupMembers], + .groupKeys: createGroupOutput.groupState[.groupKeys] + ] + ) + dependencies.set(cache: .libSession, to: mockLibSessionCache) + + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + try await mockStorage.writeAsync { db in + try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) + try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) + } + } + // MARK: - LibSession describe("LibSession") { // MARK: -- when parsing a community url diff --git a/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift index 56ab349c08..954ff67a98 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift @@ -10,10 +10,9 @@ import Nimble @testable import SessionMessagingKit -class LibSessionUtilSpec: QuickSpec { +class LibSessionUtilSpec: AsyncSpec { static let maxMessageSizeBytes: Int = 76800 // Storage server's limit, should match `config.hpp` in libSession - // FIXME: Would be good to move the identity generation into the libSession-util instead of using Sodium separately static let userSeed: Data = Data(hex: "0123456789abcdef0123456789abcdef") static let seed: Data = Data( hex: "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210" diff --git a/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift index 35b3ba1679..8169b6aebb 100644 --- a/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift @@ -14,12 +14,13 @@ class CryptoOpenGroupAPISpec: QuickSpec { @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .crypto, in: dependencies) var crypto: Crypto! = Crypto(using: dependencies) - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - cache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) - } - ) + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + } // MARK: - Crypto for OpenGroupAPI describe("Crypto for OpenGroupAPI") { diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index 73ae2a3cc6..2ad9a482d3 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -64,21 +64,20 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(Array(Data(hex: TestConstants.privateKey))) } ) - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - cache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) - cache - .when { $0.ed25519Seed } - .thenReturn(Array(Array(Data(hex: TestConstants.edSecretKey)).prefix(upTo: 32))) - } - ) - @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { $0.defaultInitialSetup() } - ) + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() @TestState var disposables: [AnyCancellable]! = [] @TestState var error: Error? + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + + mockLibSessionCache.defaultInitialSetup() + dependencies.set(cache: .libSession, to: mockLibSessionCache) + } + // MARK: - an OpenGroupAPI describe("an OpenGroupAPI") { // MARK: -- when preparing a poll request @@ -636,7 +635,16 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- processes a valid response correctly it("processes a valid response correctly") { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(Network.BatchResponse.mockCapabilitiesAndRoomResponse) var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomResponse)? @@ -657,7 +665,7 @@ class OpenGroupAPISpec: QuickSpec { ) }.toNot(throwError()) - preparedRequest + preparedRequest? .send(using: dependencies) .handleEvents(receiveOutput: { result in response = result }) .mapError { error.setting(to: $0) } @@ -672,7 +680,16 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ errors when not given a room response it("errors when not given a room response") { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(Network.BatchResponse.mockCapabilitiesAndBanResponse) var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomResponse)? @@ -693,7 +710,7 @@ class OpenGroupAPISpec: QuickSpec { ) }.toNot(throwError()) - preparedRequest + preparedRequest? .send(using: dependencies) .handleEvents(receiveOutput: { result in response = result }) .mapError { error.setting(to: $0) } @@ -706,7 +723,16 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ errors when not given a capabilities response it("errors when not given a capabilities response") { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(Network.BatchResponse.mockBanAndRoomResponse) var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomResponse)? @@ -727,7 +753,7 @@ class OpenGroupAPISpec: QuickSpec { ) }.toNot(throwError()) - preparedRequest + preparedRequest? .send(using: dependencies) .handleEvents(receiveOutput: { result in response = result }) .mapError { error.setting(to: $0) } @@ -775,7 +801,16 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- processes a valid response correctly it("processes a valid response correctly") { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(Network.BatchResponse.mockCapabilitiesAndRoomsResponse) var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomsResponse)? @@ -795,7 +830,7 @@ class OpenGroupAPISpec: QuickSpec { ) }.toNot(throwError()) - preparedRequest + preparedRequest? .send(using: dependencies) .handleEvents(receiveOutput: { result in response = result }) .mapError { error.setting(to: $0) } @@ -810,7 +845,16 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ errors when not given a room response it("errors when not given a room response") { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn( MockNetwork.batchResponseData(with: [ (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), @@ -838,7 +882,7 @@ class OpenGroupAPISpec: QuickSpec { ) }.toNot(throwError()) - preparedRequest + preparedRequest? .send(using: dependencies) .handleEvents(receiveOutput: { result in response = result }) .mapError { error.setting(to: $0) } @@ -851,7 +895,16 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ errors when not given a capabilities response it("errors when not given a capabilities response") { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(Network.BatchResponse.mockBanAndRoomsResponse) var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomsResponse)? @@ -871,7 +924,7 @@ class OpenGroupAPISpec: QuickSpec { ) }.toNot(throwError()) - preparedRequest + preparedRequest? .send(using: dependencies) .handleEvents(receiveOutput: { result in response = result }) .mapError { error.setting(to: $0) } @@ -2246,7 +2299,16 @@ class OpenGroupAPISpec: QuickSpec { beforeEach { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.response(type: [OpenGroupAPI.Room].self)) } @@ -2331,15 +2393,14 @@ private extension Network.Destination { switch self { case .cached: return nil case .snode(_, let swarmPublicKey): return swarmPublicKey - case .randomSnode(let swarmPublicKey, _), .randomSnodeLatestNetworkTimeTarget(let swarmPublicKey, _, _): - return swarmPublicKey + case .randomSnode(let swarmPublicKey): return swarmPublicKey case .server(let info), .serverDownload(let info), .serverUpload(let info, _): return info.x25519PublicKey } } var testHeaders: [HTTPHeader: String]? { switch self { - case .cached, .snode, .randomSnode, .randomSnodeLatestNetworkTimeTarget: return nil + case .cached, .snode, .randomSnode: return nil case .server(let info), .serverDownload(let info), .serverUpload(let info, _): return info.headers } } diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 2cd025dc58..eaf8655f47 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -6,13 +6,14 @@ import GRDB import SessionUtil import SessionNetworkingKit import SessionUtilitiesKit +import TestUtilities import Quick import Nimble @testable import SessionMessagingKit -class OpenGroupManagerSpec: QuickSpec { +class OpenGroupManagerSpec: AsyncSpec { override class func spec() { // MARK: Configuration @@ -110,18 +111,7 @@ class OpenGroupManagerSpec: QuickSpec { }() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, - using: dependencies, - initialData: { db in - try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) - try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - - try testGroupThread.insert(db) - try testOpenGroup.insert(db) - try Capability(openGroupServer: testOpenGroup.server, variant: .sogs, isMissing: false).insert(db) - } + using: dependencies ) @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( initialSetup: { jobRunner in @@ -139,7 +129,16 @@ class OpenGroupManagerSpec: QuickSpec { @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( initialSetup: { network in network - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.errorResponse()) } ) @@ -190,7 +189,7 @@ class OpenGroupManagerSpec: QuickSpec { @TestState(defaults: .standard, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults( initialSetup: { defaults in defaults.when { $0.integer(forKey: .any) }.thenReturn(0) - defaults.when { $0.set(.any, forKey: .any) }.thenReturn(()) + defaults.when { $0.set(Int.any, forKey: .any) }.thenReturn(()) } ) @TestState(defaults: .appGroup, in: dependencies) var mockAppGroupDefaults: MockUserDefaults! = MockUserDefaults( @@ -198,41 +197,11 @@ class OpenGroupManagerSpec: QuickSpec { defaults.when { $0.bool(forKey: .any) }.thenReturn(false) } ) - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - cache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) - cache - .when { $0.ed25519Seed } - .thenReturn(Array(Array(Data(hex: TestConstants.edSecretKey)).prefix(upTo: 32))) - } - ) - @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { $0.defaultInitialSetup() } - ) - @TestState(cache: .openGroupManager, in: dependencies) var mockOGMCache: MockOGMCache! = MockOGMCache( - initialSetup: { cache in - cache.when { $0.pendingChanges }.thenReturn([]) - cache.when { $0.pendingChanges = .any }.thenReturn(()) - cache.when { $0.getLastSuccessfulCommunityPollTimestamp() }.thenReturn(0) - cache.when { $0.setDefaultRoomInfo(.any) }.thenReturn(()) - } - ) - @TestState var mockPoller: MockCommunityPoller! = MockCommunityPoller( - initialSetup: { poller in - poller.when { $0.startIfNeeded() }.thenReturn(()) - poller.when { $0.stop() }.thenReturn(()) - } - ) - @TestState(cache: .communityPollers, in: dependencies) var mockCommunityPollerCache: MockCommunityPollerCache! = MockCommunityPollerCache( - initialSetup: { cache in - cache.when { $0.serversBeingPolled }.thenReturn([]) - cache.when { $0.startAllPollers() }.thenReturn(()) - cache.when { $0.getOrCreatePoller(for: .any) }.thenReturn(mockPoller) - cache.when { $0.stopAndRemovePoller(for: .any) }.thenReturn(()) - cache.when { $0.stopAndRemoveAllPollers() }.thenReturn(()) - } - ) + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() + @TestState var mockOGMCache: MockOGMCache! = MockOGMCache() + @TestState var mockPoller: MockPoller! = .create() + @TestState(singleton: .communityPollerManager, in: dependencies) var mockCommunityPollerManager: MockCommunityPollerManager! = .create() @TestState(singleton: .keychain, in: dependencies) var mockKeychain: MockKeychain! = MockKeychain( initialSetup: { keychain in keychain @@ -262,9 +231,47 @@ class OpenGroupManagerSpec: QuickSpec { @TestState var cache: OpenGroupManager.Cache! = OpenGroupManager.Cache(using: dependencies) @TestState var openGroupManager: OpenGroupManager! = OpenGroupManager(using: dependencies) + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + + mockLibSessionCache.defaultInitialSetup() + dependencies.set(cache: .libSession, to: mockLibSessionCache) + + mockOGMCache.when { $0.pendingChanges }.thenReturn([]) + mockOGMCache.when { $0.pendingChanges = .any }.thenReturn(()) + mockOGMCache.when { $0.getLastSuccessfulCommunityPollTimestamp() }.thenReturn(0) + mockOGMCache.when { $0.setDefaultRoomInfo(.any) }.thenReturn(()) + dependencies.set(cache: .openGroupManager, to: mockOGMCache) + + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + try await mockStorage.writeAsync { db in + try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) + try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) + + try testGroupThread.insert(db) + try testOpenGroup.insert(db) + try Capability(openGroupServer: testOpenGroup.server, variant: .sogs, isMissing: false).insert(db) + } + } + // MARK: - an OpenGroupManager describe("an OpenGroupManager") { beforeEach { + try await mockPoller.when { await $0.startIfNeeded() }.thenReturn(()) + try await mockPoller.when { await $0.stop() }.thenReturn(()) + + try await mockCommunityPollerManager.when { await $0.serversBeingPolled }.thenReturn([]) + try await mockCommunityPollerManager.when { await $0.startAllPollers() }.thenReturn(()) + try await mockCommunityPollerManager + .when { await $0.getOrCreatePoller(for: .any) } + .thenReturn(mockPoller) + try await mockCommunityPollerManager.when { await $0.stopAndRemovePoller(for: .any) }.thenReturn(()) + try await mockCommunityPollerManager.when { await $0.stopAndRemoveAllPollers() }.thenReturn(()) + _ = userGroupsInitResult } @@ -405,7 +412,9 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- when there is a thread for the room and the cache has a poller context("when there is a thread for the room and the cache has a poller") { beforeEach { - mockCommunityPollerCache.when { $0.serversBeingPolled }.thenReturn(["http://127.0.0.1"]) + try await mockCommunityPollerManager + .when { await $0.serversBeingPolled } + .thenReturn(["http://127.0.0.1"]) } // MARK: ------ for the no-scheme variant @@ -548,7 +557,9 @@ class OpenGroupManagerSpec: QuickSpec { context("when given the legacy DNS host and there is a cached poller for the default server") { // MARK: ------ returns true it("returns true") { - mockCommunityPollerCache.when { $0.serversBeingPolled }.thenReturn(["http://116.203.70.33"]) + try await mockCommunityPollerManager + .when { await $0.serversBeingPolled } + .thenReturn(["http://116.203.70.33"]) mockStorage.write { db in try SessionThread( id: OpenGroup.idFor(roomToken: "testRoom", server: "http://116.203.70.33"), @@ -580,7 +591,9 @@ class OpenGroupManagerSpec: QuickSpec { context("when given the default server and there is a cached poller for the legacy DNS host") { // MARK: ------ returns true it("returns true") { - mockCommunityPollerCache.when { $0.serversBeingPolled }.thenReturn(["http://open.getsession.org"]) + try await mockCommunityPollerManager + .when { await $0.serversBeingPolled } + .thenReturn(["http://open.getsession.org"]) mockStorage.write { db in try SessionThread( id: OpenGroup.idFor(roomToken: "testRoom", server: "http://open.getsession.org"), @@ -624,7 +637,9 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- returns false if there is not a poller for the server in the cache it("returns false if there is not a poller for the server in the cache") { - mockCommunityPollerCache.when { $0.serversBeingPolled }.thenReturn([]) + try await mockCommunityPollerManager + .when { await $0.serversBeingPolled } + .thenReturn([]) expect( mockStorage.read { db -> Bool in @@ -668,7 +683,16 @@ class OpenGroupManagerSpec: QuickSpec { } mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(Network.BatchResponse.mockCapabilitiesAndRoomResponse) mockUserDefaults @@ -735,22 +759,25 @@ class OpenGroupManagerSpec: QuickSpec { } .sinkAndStore(in: &disposables) - expect(mockCommunityPollerCache) - .to(call(matchingParameters: .all) { - $0.getOrCreatePoller( + await mockCommunityPollerManager + .verify { + await $0.getOrCreatePoller( for: CommunityPoller.Info( server: "http://127.0.0.1", pollFailureCount: 0 ) ) - }) - expect(mockPoller).to(call { $0.startIfNeeded() }) + } + .wasCalled() + await mockPoller.verify { await $0.startIfNeeded() }.wasCalled() } // MARK: ---- an existing room context("an existing room") { beforeEach { - mockCommunityPollerCache.when { $0.serversBeingPolled }.thenReturn(["http://127.0.0.1"]) + try await mockCommunityPollerManager + .when { await $0.serversBeingPolled } + .thenReturn(["http://127.0.0.1"]) mockStorage.write { db in try testOpenGroup.insert(db) } @@ -806,7 +833,16 @@ class OpenGroupManagerSpec: QuickSpec { context("with an invalid response") { beforeEach { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.response(data: Data())) mockUserDefaults @@ -906,8 +942,9 @@ class OpenGroupManagerSpec: QuickSpec { ) } - expect(mockCommunityPollerCache) - .to(call(matchingParameters: .all) { $0.stopAndRemovePoller(for: "http://127.0.0.1") }) + await mockCommunityPollerManager + .verify { await $0.stopAndRemovePoller(for: "http://127.0.0.1") } + .wasCalled() } // MARK: ------ removes the open group @@ -2555,10 +2592,12 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockNetwork) .to(call { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, + endpoint: OpenGroupAPI.Endpoint.sequence, + destination: expectedRequest.destination, + body: expectedRequest.body, + category: .standard, requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + overallTimeout: expectedRequest.overallTimeout ) }) } @@ -2569,8 +2608,16 @@ class OpenGroupManagerSpec: QuickSpec { cache.setDefaultRoomInfo([(room: OpenGroupAPI.Room.mock, openGroup: OpenGroup.mock)]) cache.defaultRoomsPublisher.sinkUntilComplete() - expect(mockNetwork) - .toNot(call { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) }) + expect(mockNetwork).toNot(call { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + }) } } } @@ -2647,8 +2694,17 @@ extension OpenGroupAPI.RoomPollInfo { // MARK: - Mock Types -extension OpenGroup: Mocked { - static var mock: OpenGroup = OpenGroup( +extension OpenGroup: @retroactive Mocked { + public static var any: OpenGroup = OpenGroup( + server: .any, + roomToken: .any, + publicKey: .any, + isActive: .any, + name: .any, + userCount: .any, + infoUpdates: .any + ) + public static var mock: OpenGroup = OpenGroup( server: "testserver", roomToken: "testRoom", publicKey: TestConstants.serverPublicKey, @@ -2659,12 +2715,41 @@ extension OpenGroup: Mocked { ) } -extension OpenGroupAPI.Capabilities: Mocked { - static var mock: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [], missing: nil) +extension OpenGroupAPI.Capabilities: @retroactive Mocked { + public static var any: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: .any, missing: .any) + public static var mock: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [], missing: nil) } -extension OpenGroupAPI.Room: Mocked { - static var mock: OpenGroupAPI.Room = OpenGroupAPI.Room( +extension OpenGroupAPI.Room: @retroactive Mocked { + public static var any: OpenGroupAPI.Room = OpenGroupAPI.Room( + token: .any, + name: .any, + roomDescription: .any, + infoUpdates: .any, + messageSequence: .any, + created: .any, + activeUsers: .any, + activeUsersCutoff: .any, + imageId: .any, + pinnedMessages: .any, + admin: .any, + globalAdmin: .any, + admins: .any, + hiddenAdmins: .any, + moderator: .any, + globalModerator: .any, + moderators: .any, + hiddenModerators: .any, + read: .any, + defaultRead: .any, + defaultAccessible: .any, + write: .any, + defaultWrite: .any, + upload: .any, + defaultUpload: .any + ) + + public static var mock: OpenGroupAPI.Room = OpenGroupAPI.Room( token: "test", name: "testRoom", roomDescription: nil, @@ -2693,8 +2778,24 @@ extension OpenGroupAPI.Room: Mocked { ) } -extension OpenGroupAPI.RoomPollInfo: Mocked { - static var mock: OpenGroupAPI.RoomPollInfo = OpenGroupAPI.RoomPollInfo( +extension OpenGroupAPI.RoomPollInfo: @retroactive Mocked { + public static var any: OpenGroupAPI.RoomPollInfo = OpenGroupAPI.RoomPollInfo( + token: .any, + activeUsers: .any, + admin: .any, + globalAdmin: .any, + moderator: .any, + globalModerator: .any, + read: .any, + defaultRead: .any, + defaultAccessible: .any, + write: .any, + defaultWrite: .any, + upload: .any, + defaultUpload: .any, + details: .any + ) + public static var mock: OpenGroupAPI.RoomPollInfo = OpenGroupAPI.RoomPollInfo( token: "test", activeUsers: 1, admin: false, @@ -2712,8 +2813,22 @@ extension OpenGroupAPI.RoomPollInfo: Mocked { ) } -extension OpenGroupAPI.Message: Mocked { - static var mock: OpenGroupAPI.Message = OpenGroupAPI.Message( +extension OpenGroupAPI.Message: @retroactive Mocked { + public static var any: OpenGroupAPI.Message = OpenGroupAPI.Message( + id: .any, + sender: .any, + posted: .any, + edited: .any, + deleted: .any, + seqNo: .any, + whisper: .any, + whisperMods: .any, + whisperTo: .any, + base64EncodedData: .any, + base64EncodedSignature: .any, + reactions: .any + ) + public static var mock: OpenGroupAPI.Message = OpenGroupAPI.Message( id: 100, sender: TestConstants.blind15PublicKey, posted: 1, @@ -2729,8 +2844,15 @@ extension OpenGroupAPI.Message: Mocked { ) } -extension OpenGroupAPI.SendDirectMessageResponse: Mocked { - static var mock: OpenGroupAPI.SendDirectMessageResponse = OpenGroupAPI.SendDirectMessageResponse( +extension OpenGroupAPI.SendDirectMessageResponse: @retroactive Mocked { + public static var any: OpenGroupAPI.SendDirectMessageResponse = OpenGroupAPI.SendDirectMessageResponse( + id: .any, + sender: .any, + recipient: .any, + posted: .any, + expires: .any + ) + public static var mock: OpenGroupAPI.SendDirectMessageResponse = OpenGroupAPI.SendDirectMessageResponse( id: 1, sender: TestConstants.blind15PublicKey, recipient: "testRecipient", @@ -2739,8 +2861,16 @@ extension OpenGroupAPI.SendDirectMessageResponse: Mocked { ) } -extension OpenGroupAPI.DirectMessage: Mocked { - static var mock: OpenGroupAPI.DirectMessage = OpenGroupAPI.DirectMessage( +extension OpenGroupAPI.DirectMessage: @retroactive Mocked { + public static var any: OpenGroupAPI.DirectMessage = OpenGroupAPI.DirectMessage( + id: .any, + sender: .any, + recipient: .any, + posted: .any, + expires: .any, + base64EncodedMessage: .any + ) + public static var mock: OpenGroupAPI.DirectMessage = OpenGroupAPI.DirectMessage( id: 101, sender: TestConstants.blind15PublicKey, recipient: "testRecipient", diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index 53efb1153d..4ff4e39fc5 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -26,7 +26,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: -- when receiving a group invitation context("when receiving a group invitation") { // MARK: ---- throws if the admin signature fails to verify - itTracked("throws if the admin signature fails to verify") { + it("throws if the admin signature fails to verify") { fixture.mockCrypto .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(false) @@ -50,7 +50,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ---- with profile information context("with profile information") { // MARK: ------ updates the profile name - itTracked("updates the profile name") { + it("updates the profile name") { fixture.inviteMessage.profile = VisibleMessage.VMProfile(displayName: "TestName") fixture.mockStorage.write { db in @@ -72,8 +72,8 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ------ with a profile picture context("with a profile picture") { // MARK: ------ schedules and starts a displayPictureDownload job if running the main app - itTracked("schedules and starts a displayPictureDownload job if running the main app") { - await fixture.mockAppContext.when { $0.isMainApp }.thenReturn(true) + it("schedules and starts a displayPictureDownload job if running the main app") { + try await fixture.mockAppContext.when { $0.isMainApp }.thenReturn(true) fixture.inviteMessage.profile = VisibleMessage.VMProfile( displayName: "TestName", @@ -117,7 +117,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { } // MARK: ------ schedules but does not start a displayPictureDownload job when not the main app - itTracked("schedules but does not start a displayPictureDownload job when not the main app") { + it("schedules but does not start a displayPictureDownload job when not the main app") { fixture.inviteMessage.profile = VisibleMessage.VMProfile( displayName: "TestName", profileKey: Data((0.. = mockStorage.write { db in + mockStorage.write { db in // Need the auth data to exist in the database to prepare the request _ = try SessionThread.upsert( db, @@ -444,38 +458,10 @@ class MessageSenderGroupsSpec: QuickSpec { invited: nil ).upsert(db) - let preparedRequest: Network.PreparedRequest = try SnodeAPI.preparedSequence( - requests: [ - try SnodeAPI - .preparedSendMessage( - message: SnodeMessage( - recipient: groupId.hexString, - data: Data([1, 2, 3]), - ttl: ConfigDump.Variant.groupInfo.ttl, - timestampMs: 1234567890 - ), - in: ConfigDump.Variant.groupInfo.namespace, - authMethod: try Authentication.with( - db, - swarmPublicKey: groupId.hexString, - using: dependencies - ), - using: dependencies - ) - ], - requireAllBatchResponses: false, - swarmPublicKey: groupId.hexString, - snodeRetrievalRetryCount: 0, - requestAndPathBuildTimeout: Network.defaultTimeout, - using: dependencies - ) - // Remove the debug group so it can be created during the actual test try ClosedGroup.filter(id: groupId.hexString).deleteAll(db) try SessionThread.filter(id: groupId.hexString).deleteAll(db) - - return preparedRequest - }! + } MessageSender .createGroup( @@ -492,10 +478,32 @@ class MessageSenderGroupsSpec: QuickSpec { expect(mockNetwork) .to(call(.exactly(times: 1), matchingParameters: .all) { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, - requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + endpoint: SnodeAPI.Endpoint.sequence, + destination: .randomSnode(swarmPublicKey: groupId.hexString), + body: try! JSONEncoder(using: dependencies).encode( + Network.BatchRequest( + requestsKey: .requests, + requests: [ + try SnodeAPI.preparedSendMessage( + message: SnodeMessage( + recipient: groupId.hexString, + data: Data([1, 2, 3]), + ttl: ConfigDump.Variant.groupInfo.ttl, + timestampMs: 1234567890 + ), + in: ConfigDump.Variant.groupInfo.namespace, + authMethod: try Authentication.with( + swarmPublicKey: groupId.hexString, + using: dependencies + ), + using: dependencies + ) + ] + ) + ), + category: .standard, + requestTimeout: Network.defaultTimeout, + overallTimeout: Network.defaultTimeout ) }) } @@ -504,7 +512,16 @@ class MessageSenderGroupsSpec: QuickSpec { context("and the group configuration sync fails") { beforeEach { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.errorResponse()) } @@ -590,16 +607,19 @@ class MessageSenderGroupsSpec: QuickSpec { .mapError { error.setting(to: $0) } .sinkAndStore(in: &disposables) - let expectedRequest: Network.PreparedRequest = try Network - .preparedUpload(data: TestConstants.validImageData, using: dependencies) - expect(mockNetwork) .toNot(call { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, - requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + endpoint: Network.FileServer.Endpoint.file, + destination: .serverUpload( + server: Network.FileServer.fileServer, + x25519PublicKey: Network.FileServer.fileServerPublicKey, + fileName: nil + ), + body: TestConstants.validImageData, + category: .upload, + requestTimeout: Network.fileUploadTimeout, + overallTimeout: nil ) }) } @@ -609,7 +629,16 @@ class MessageSenderGroupsSpec: QuickSpec { // MARK: ------ uploads the image it("uploads the image") { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1"))) MessageSender @@ -628,17 +657,23 @@ class MessageSenderGroupsSpec: QuickSpec { let expectedRequest: Network.PreparedRequest = try Network .preparedUpload( data: TestConstants.validImageData, - requestAndPathBuildTimeout: Network.fileUploadTimeout, + overallTimeout: Network.fileUploadTimeout, using: dependencies ) expect(mockNetwork) .to(call(.exactly(times: 1), matchingParameters: .all) { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, - requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + endpoint: Network.FileServer.Endpoint.file, + destination: .serverUpload( + server: Network.FileServer.fileServer, + x25519PublicKey: Network.FileServer.fileServerPublicKey, + fileName: nil + ), + body: TestConstants.validImageData, + category: .upload, + requestTimeout: Network.fileUploadTimeout, + overallTimeout: Network.fileUploadTimeout ) }) } @@ -648,7 +683,16 @@ class MessageSenderGroupsSpec: QuickSpec { // Prevent the ConfigSyncJob network request by making the libSession cache appear empty mockLibSessionCache.when { $0.isEmpty }.thenReturn(true) mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1"))) MessageSender @@ -674,7 +718,16 @@ class MessageSenderGroupsSpec: QuickSpec { // MARK: ------ fails if the image fails to upload it("fails if the image fails to upload") { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(Fail(error: NetworkError.unknown).eraseToAnyPublisher()) MessageSender @@ -731,8 +784,6 @@ class MessageSenderGroupsSpec: QuickSpec { // MARK: ------ and trying to subscribe for push notifications context("and trying to subscribe for push notifications") { - @TestState var expectedRequest: Network.PreparedRequest! - beforeEach { // Need to set `isUsingFullAPNs` to true to generate the `expectedRequest` mockUserDefaults @@ -741,7 +792,7 @@ class MessageSenderGroupsSpec: QuickSpec { mockUserDefaults .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } .thenReturn(true) - expectedRequest = mockStorage.write { db in + mockStorage.write { db in _ = try SessionThread.upsert( db, id: groupId.hexString, @@ -760,18 +811,10 @@ class MessageSenderGroupsSpec: QuickSpec { groupIdentityPrivateKey: groupSecretKey, invited: nil ).upsert(db) - let result = try PushNotificationAPI.preparedSubscribe( - db, - token: Data([5, 4, 3, 2, 1]), - sessionIds: [groupId], - using: dependencies - ) // Remove the debug group so it can be created during the actual test try ClosedGroup.filter(id: groupId.hexString).deleteAll(db) try SessionThread.filter(id: groupId.hexString).deleteAll(db) - - return result }! } @@ -799,10 +842,42 @@ class MessageSenderGroupsSpec: QuickSpec { expect(mockNetwork) .to(call(.exactly(times: 1), matchingParameters: .all) { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, - requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + endpoint: PushNotificationAPI.Endpoint.subscribe, + destination: .server( + method: .post, + server: PushNotificationAPI.server, + queryParameters: [:], + headers: [:], + x25519PublicKey: PushNotificationAPI.serverPublicKey + ), + body: try! JSONEncoder(using: dependencies).encode( + PushNotificationAPI.SubscribeRequest( + subscriptions: [ + PushNotificationAPI.SubscribeRequest.Subscription( + namespaces: [ + .groupMessages, + .configGroupKeys, + .configGroupInfo, + .configGroupMembers, + .revokedRetrievableGroupMessages + ], + includeMessageData: true, + serviceInfo: PushNotificationAPI.ServiceInfo( + token: Data([5, 4, 3, 2, 1]).toHexString() + ), + notificationsEncryptionKey: Data([1, 2, 3]), + authMethod: try! Authentication.with( + swarmPublicKey: groupId.hexString, + using: dependencies + ), + timestamp: 1234567890 + ) + ] + ) + ), + category: .standard, + requestTimeout: Network.defaultTimeout, + overallTimeout: nil ) }) } @@ -832,10 +907,12 @@ class MessageSenderGroupsSpec: QuickSpec { expect(mockNetwork).toNot(call { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, - requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any ) }) } @@ -865,10 +942,12 @@ class MessageSenderGroupsSpec: QuickSpec { expect(mockNetwork).toNot(call { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, - requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any ) }) } @@ -879,7 +958,16 @@ class MessageSenderGroupsSpec: QuickSpec { context("when adding members to a group") { beforeEach { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(Network.BatchResponse.mockAddMemberConfigSyncResponse) // Rekey a couple of times to increase the key generation to 1 @@ -987,7 +1075,16 @@ class MessageSenderGroupsSpec: QuickSpec { context("and granting access to historic messages") { beforeEach { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(Network.BatchResponse.mockAddMemberHistoricConfigSyncResponse) } @@ -1024,45 +1121,6 @@ class MessageSenderGroupsSpec: QuickSpec { "LPczVOFKOPs+rrB3aUpMsNUnJHOEhW9g6zi/UPjuCWTnnvpxlMTpHaTFlMTp+NjQ6dKi86jZJ" + "l3oiJEA5h5pBE5oOJHQNvtF8GOcsYwrIFTZKnI7AGkBSu1TxP0xLWwTUzjOGMgmKvlIgkQ6e9" + "r3JBmU=" - let expectedRequest: Network.PreparedRequest = try SnodeAPI.preparedSequence( - requests: [] - .appending(try SnodeAPI.preparedUnrevokeSubaccounts( - subaccountsToUnrevoke: [Array("TestSubAccountToken".data(using: .utf8)!)], - authMethod: Authentication.groupAdmin( - groupSessionId: groupId, - ed25519SecretKey: Array(groupSecretKey) - ), - using: dependencies - )) - .appending(try SnodeAPI.preparedSendMessage( - message: SnodeMessage( - recipient: groupId.hexString, - data: Data(base64Encoded: requestDataString)!, - ttl: ConfigDump.Variant.groupKeys.ttl, - timestampMs: UInt64(1234567890000) - ), - in: .configGroupKeys, - authMethod: Authentication.groupAdmin( - groupSessionId: groupId, - ed25519SecretKey: Array(groupSecretKey) - ), - using: dependencies - )) - .appending(try SnodeAPI.preparedDeleteMessages( - serverHashes: ["testHash"], - requireSuccessfulDeletion: false, - authMethod: Authentication.groupAdmin( - groupSessionId: groupId, - ed25519SecretKey: Array(groupSecretKey) - ), - using: dependencies - )), - requireAllBatchResponses: true, - swarmPublicKey: groupId.hexString, - snodeRetrievalRetryCount: 0, // This job has it's own retry mechanism - requestAndPathBuildTimeout: Network.defaultTimeout, - using: dependencies - ) MessageSender.addGroupMembers( groupSessionId: groupId.hexString, @@ -1086,10 +1144,49 @@ class MessageSenderGroupsSpec: QuickSpec { expect(mockNetwork) .to(call(.exactly(times: 1), matchingParameters: .all) { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, - requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + endpoint: SnodeAPI.Endpoint.sequence, + destination: .randomSnode(swarmPublicKey: groupId.hexString), + body: try! JSONEncoder(using: dependencies).encode( + Network.BatchRequest( + requestsKey: .requests, + requests: [ + try SnodeAPI.preparedUnrevokeSubaccounts( + subaccountsToUnrevoke: [Array("TestSubAccountToken".data(using: .utf8)!)], + authMethod: Authentication.groupAdmin( + groupSessionId: groupId, + ed25519SecretKey: Array(groupSecretKey) + ), + using: dependencies + ), + try SnodeAPI.preparedSendMessage( + message: SnodeMessage( + recipient: groupId.hexString, + data: Data(base64Encoded: requestDataString)!, + ttl: ConfigDump.Variant.groupKeys.ttl, + timestampMs: UInt64(1234567890000) + ), + in: .configGroupKeys, + authMethod: Authentication.groupAdmin( + groupSessionId: groupId, + ed25519SecretKey: Array(groupSecretKey) + ), + using: dependencies + ), + try SnodeAPI.preparedDeleteMessages( + serverHashes: ["testHash"], + requireSuccessfulDeletion: false, + authMethod: Authentication.groupAdmin( + groupSessionId: groupId, + ed25519SecretKey: Array(groupSecretKey) + ), + using: dependencies + ) + ] + ) + ), + category: .standard, + requestTimeout: Network.defaultTimeout, + overallTimeout: Network.defaultTimeout ) }) } @@ -1199,7 +1296,16 @@ class MessageSenderGroupsSpec: QuickSpec { context("and not granting access to historic messages") { beforeEach { mockNetwork - .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } .thenReturn(Network.BatchResponse.mockAddMemberConfigSyncResponse) } @@ -1239,34 +1345,6 @@ class MessageSenderGroupsSpec: QuickSpec { // MARK: ---- includes the unrevoke subaccounts as part of the config sync sequence it("includes the unrevoke subaccounts as part of the config sync sequence") { - let expectedRequest: Network.PreparedRequest = try SnodeAPI.preparedSequence( - requests: [] - .appending(try SnodeAPI - .preparedUnrevokeSubaccounts( - subaccountsToUnrevoke: [Array("TestSubAccountToken".data(using: .utf8)!)], - authMethod: Authentication.groupAdmin( - groupSessionId: groupId, - ed25519SecretKey: Array(groupSecretKey) - ), - using: dependencies - ) - ) - .appending(try SnodeAPI.preparedDeleteMessages( - serverHashes: ["testHash"], - requireSuccessfulDeletion: false, - authMethod: Authentication.groupAdmin( - groupSessionId: groupId, - ed25519SecretKey: Array(groupSecretKey) - ), - using: dependencies - )), - requireAllBatchResponses: true, - swarmPublicKey: groupId.hexString, - snodeRetrievalRetryCount: 0, // This job has it's own retry mechanism - requestAndPathBuildTimeout: Network.defaultTimeout, - using: dependencies - ) - MessageSender.addGroupMembers( groupSessionId: groupId.hexString, members: [ @@ -1279,10 +1357,37 @@ class MessageSenderGroupsSpec: QuickSpec { expect(mockNetwork) .to(call(.exactly(times: 1), matchingParameters: .all) { network in network.send( - expectedRequest.body, - to: expectedRequest.destination, - requestTimeout: expectedRequest.requestTimeout, - requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout + endpoint: SnodeAPI.Endpoint.sequence, + destination: .randomSnode(swarmPublicKey: groupId.hexString), + body: try! JSONEncoder(using: dependencies).encode( + Network.BatchRequest( + requestsKey: .requests, + requests: [ + try SnodeAPI.preparedUnrevokeSubaccounts( + subaccountsToUnrevoke: [ + Array("TestSubAccountToken".data(using: .utf8)!) + ], + authMethod: Authentication.groupAdmin( + groupSessionId: groupId, + ed25519SecretKey: Array(groupSecretKey) + ), + using: dependencies + ), + try SnodeAPI.preparedDeleteMessages( + serverHashes: ["testHash"], + requireSuccessfulDeletion: false, + authMethod: Authentication.groupAdmin( + groupSessionId: groupId, + ed25519SecretKey: Array(groupSecretKey) + ), + using: dependencies + ) + ] + ) + ), + category: .standard, + requestTimeout: Network.defaultTimeout, + overallTimeout: Network.defaultTimeout ) }) } @@ -1440,8 +1545,14 @@ class MessageSenderGroupsSpec: QuickSpec { // MARK: - Mock Types -extension SendMessagesResponse: Mocked { - static var mock: SendMessagesResponse = SendMessagesResponse( +extension SendMessagesResponse: @retroactive Mocked { + public static var any: SendMessagesResponse = SendMessagesResponse( + hash: .any, + swarm: .any, + hardFork: .any, + timeOffset: .any + ) + public static var mock: SendMessagesResponse = SendMessagesResponse( hash: "hash", swarm: [:], hardFork: [1, 2], @@ -1449,16 +1560,26 @@ extension SendMessagesResponse: Mocked { ) } -extension UnrevokeSubaccountResponse: Mocked { - static var mock: UnrevokeSubaccountResponse = UnrevokeSubaccountResponse( +extension UnrevokeSubaccountResponse: @retroactive Mocked { + public static var any: UnrevokeSubaccountResponse = UnrevokeSubaccountResponse( + swarm: .any, + hardFork: .any, + timeOffset: .any + ) + public static var mock: UnrevokeSubaccountResponse = UnrevokeSubaccountResponse( swarm: [:], hardFork: [], timeOffset: 0 ) } -extension DeleteMessagesResponse: Mocked { - static var mock: DeleteMessagesResponse = DeleteMessagesResponse( +extension DeleteMessagesResponse: @retroactive Mocked { + public static var any: DeleteMessagesResponse = DeleteMessagesResponse( + swarm: .any, + hardFork: .any, + timeOffset: .any + ) + public static var mock: DeleteMessagesResponse = DeleteMessagesResponse( swarm: [:], hardFork: [], timeOffset: 0 diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift index ee73e9fbc0..f9b5e21c4e 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift @@ -10,19 +10,14 @@ import Nimble @testable import SessionMessagingKit -class MessageSenderSpec: QuickSpec { +class MessageSenderSpec: AsyncSpec { override class func spec() { // MARK: Configuration @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, - using: dependencies, - initialData: { db in - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - } + using: dependencies ) @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( initialSetup: { crypto in @@ -42,15 +37,19 @@ class MessageSenderSpec: QuickSpec { ) } ) - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - cache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) - cache - .when { $0.ed25519Seed } - .thenReturn(Array(Array(Data(hex: TestConstants.edSecretKey)).prefix(upTo: 32))) + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + try await mockStorage.writeAsync { db in + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) } - ) + } // MARK: - a MessageSender describe("a MessageSender") { diff --git a/SessionMessagingKitTests/Sending & Receiving/Notifications/NotificationsManagerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Notifications/NotificationsManagerSpec.swift index 5d7db29b23..bcb7522892 100644 --- a/SessionMessagingKitTests/Sending & Receiving/Notifications/NotificationsManagerSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/Notifications/NotificationsManagerSpec.swift @@ -18,18 +18,7 @@ class NotificationsManagerSpec: QuickSpec { $0.dateNow = Date(timeIntervalSince1970: 1234567890) } ) - @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { - $0.defaultInitialSetup() - $0.when { - $0.conversationLastRead( - threadId: .any, - threadVariant: .any, - openGroupUrlInfo: .any - ) - }.thenReturn(1234567800) - } - ) + @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() @TestState(singleton: .extensionHelper, in: dependencies) var mockExtensionHelper: MockExtensionHelper! = MockExtensionHelper( initialSetup: { helper in helper.when { $0.hasDedupeRecordSinceLastCleared(threadId: .any) }.thenReturn(false) @@ -51,6 +40,19 @@ class NotificationsManagerSpec: QuickSpec { mutedUntil: nil ) + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockLibSessionCache.defaultInitialSetup() + mockLibSessionCache.when { + $0.conversationLastRead( + threadId: .any, + threadVariant: .any, + openGroupUrlInfo: .any + ) + }.thenReturn(1234567800) + dependencies.set(cache: .libSession, to: mockLibSessionCache) + } + // MARK: - a NotificationsManager - Ensure Should Show describe("a NotificationsManager when ensuring we should show notifications") { // MARK: -- throws if the message has no sender diff --git a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift index 2b575b5c84..1898e5ea1f 100644 --- a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift @@ -25,7 +25,7 @@ class CommunityPollerManagerSpec: AsyncSpec { context("when starting polling") { // MARK: ---- creates pollers for all of the communities it("creates pollers for all of the communities") { - await fixture.setupForActivePolling() + try await fixture.setupForActivePolling() await fixture.manager.startAllPollers() await expect { await fixture.manager.serversBeingPolled } @@ -34,7 +34,7 @@ class CommunityPollerManagerSpec: AsyncSpec { // MARK: ---- creates a poll task it("creates a poll task") { - await fixture.setupForActivePolling() + try await fixture.setupForActivePolling() await fixture.manager.startAllPollers() await expect { await fixture.manager.allPollers.count } .to(equal(2)) @@ -45,7 +45,7 @@ class CommunityPollerManagerSpec: AsyncSpec { // MARK: ---- does not create additional pollers if it's already polling it("does not create additional pollers if it's already polling") { - await fixture.setupForActivePolling() + try await fixture.setupForActivePolling() await fixture.manager .getOrCreatePoller(for: CommunityPoller.Info(server: "testserver", pollFailureCount: 0)) .startIfNeeded() @@ -116,7 +116,7 @@ private class CommunityPollerManagerTestFixture: FixtureBase { private func applyBaselineStubs() async throws { try await applyBaselineStorage() await applyBaselineNetwork() - await applyBaselineAppContext() + try await applyBaselineAppContext() await applyBaselineUserDefaults() await applyBaselineGeneralCache() await applyBaselineOGMCache() @@ -180,8 +180,8 @@ private class CommunityPollerManagerTestFixture: FixtureBase { ) } - private func applyBaselineAppContext() async { - await mockAppContext.when { await $0.isMainAppAndActive }.thenReturn(false) + private func applyBaselineAppContext() async throws { + try await mockAppContext.when { await $0.isMainAppAndActive }.thenReturn(false) } private func applyBaselineUserDefaults() async {} @@ -229,7 +229,7 @@ private class CommunityPollerManagerTestFixture: FixtureBase { // MARK: - Test Specific Configurations - @MainActor func setupForActivePolling() async { - await mockAppContext.when { $0.isMainAppAndActive }.thenReturn(true) + @MainActor func setupForActivePolling() async throws { + try await mockAppContext.when { $0.isMainAppAndActive }.thenReturn(true) } } diff --git a/SessionMessagingKitTests/Shared Models/SessionThreadViewModelSpec.swift b/SessionMessagingKitTests/Shared Models/SessionThreadViewModelSpec.swift index 38cc13e864..edb952ecf1 100644 --- a/SessionMessagingKitTests/Shared Models/SessionThreadViewModelSpec.swift +++ b/SessionMessagingKitTests/Shared Models/SessionThreadViewModelSpec.swift @@ -8,15 +8,18 @@ import SessionUtilitiesKit @testable import SessionMessagingKit -class SessionThreadViewModelSpec: QuickSpec { +class SessionThreadViewModelSpec: AsyncSpec { override class func spec() { // MARK: Configuration @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - using: dependencies, - initialData: { db in + using: dependencies + ) + + beforeEach { + try await mockStorage.writeAsync { db in try db.create(table: "testMessage") { t in t.column("body", .text).notNull() } @@ -28,7 +31,7 @@ class SessionThreadViewModelSpec: QuickSpec { t.column("body") } } - ) + } // MARK: - a SessionThreadViewModel describe("a SessionThreadViewModel") { diff --git a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift index 13dd801e31..3f12841e27 100644 --- a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift +++ b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift @@ -24,7 +24,6 @@ class ExtensionHelperSpec: AsyncSpec { @TestState(singleton: .extensionHelper, in: dependencies) var extensionHelper: ExtensionHelper! = ExtensionHelper(using: dependencies) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, using: dependencies ) @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( @@ -53,17 +52,21 @@ class ExtensionHelperSpec: AsyncSpec { .thenReturn(Data([1, 2, 3])) } ) - @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { $0.defaultInitialSetup() } - ) - - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - } - ) + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() @TestState var mockLogger: MockLogger! = MockLogger() + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + + mockLibSessionCache.defaultInitialSetup() + dependencies.set(cache: .libSession, to: mockLibSessionCache) + + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + } + // MARK: - an ExtensionHelper - File Management describe("an ExtensionHelper") { // MARK: -- can delete the entire cache @@ -2093,7 +2096,6 @@ class ExtensionHelperSpec: AsyncSpec { it("waits if messages have already been loaded but we indicate we will load them again") { await expect { try await extensionHelper.loadMessages() }.toNot(throwError()) - extensionHelper.willLoadMessages() await expect { await extensionHelper.waitUntilMessagesAreLoaded(timeout: .milliseconds(50)) }.to(beFalse()) @@ -2202,7 +2204,7 @@ class ExtensionHelperSpec: AsyncSpec { it("loads config messages before other messages") { await expect { try await extensionHelper.loadMessages() }.toNot(throwError()) - let key: FunctionConsumer.Key = FunctionConsumer.Key( + let key: FunctionConsumer_Old.Key = FunctionConsumer_Old.Key( name: "contentsOfDirectory(atPath:)", generics: [], paramCount: 1 diff --git a/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift index 94f7f59ac6..e16c3ff8ef 100644 --- a/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift +++ b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift @@ -18,8 +18,12 @@ class PreparedRequestSendingSpec: QuickSpec { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) } @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork() - @TestState var preparedRequest: Network.PreparedRequest! = { - let request = try! Request( + @TestState var preparedRequest: Network.PreparedRequest! + @TestState var error: Error? + @TestState var disposables: [AnyCancellable]! = [] + + beforeEach { + let request: Request = try Request( endpoint: .endpoint1, destination: .server( method: .post, @@ -28,15 +32,12 @@ class PreparedRequestSendingSpec: QuickSpec { ), body: nil ) - - return try! Network.PreparedRequest( + preparedRequest = try Network.PreparedRequest( request: request, responseType: Int.self, using: dependencies ) - }() - @TestState var error: Error? - @TestState var disposables: [AnyCancellable]! = [] + } // MARK: - a PreparedRequest sending Onion Requests describe("a PreparedRequest sending Onion Requests") { @@ -72,21 +73,6 @@ class PreparedRequestSendingSpec: QuickSpec { expect(error).to(beNil()) } - // MARK: ---- returns an error when the prepared request is null - it("returns an error when the prepared request is null") { - var response: (info: ResponseInfoType, data: Int)? - - preparedRequest = nil - preparedRequest - .send(using: dependencies) - .handleEvents(receiveOutput: { result in response = result }) - .mapError { error.setting(to: $0) } - .sinkAndStore(in: &disposables) - - expect(error).to(matchError(NetworkError.invalidPreparedRequest)) - expect(response).to(beNil()) - } - // MARK: ------ can return a cached response it("can return a cached response") { var response: (info: ResponseInfoType, data: Int)? diff --git a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift index 77514dedf8..fdf80044f5 100644 --- a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift @@ -29,22 +29,9 @@ class ThreadSettingsViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, - using: dependencies, - initialData: { db in - try Identity( - variant: .x25519PublicKey, - data: Data(hex: TestConstants.publicKey) - ).insert(db) - try Profile(id: userPubkey, name: "TestMe").insert(db) - try Profile(id: user2Pubkey, name: "TestUser").insert(db) - } - ) - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - } + using: dependencies ) + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( initialSetup: { jobRunner in jobRunner @@ -58,9 +45,7 @@ class ThreadSettingsViewModelSpec: AsyncSpec { .thenReturn([:]) } ) - @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { $0.defaultInitialSetup() } - ) + @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( initialSetup: { crypto in crypto @@ -68,22 +53,7 @@ class ThreadSettingsViewModelSpec: AsyncSpec { .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) } ) - @TestState(cache: .snodeAPI, in: dependencies) var mockSnodeAPICache: MockSnodeAPICache! = MockSnodeAPICache( - initialSetup: { cache in - var timestampMs: Int64 = 1234567890000 - - cache.when { $0.clockOffsetMs }.thenReturn(0) - cache - .when { $0.currentOffsetTimestampMs() } - .thenReturn { _, _ in - /// **Note:** We need to increment this value every time it's accessed because otherwise any functions which - /// insert multiple `Interaction` values can end up running into unique constraint conflicts due to the timestamp - /// being identical between different interactions - timestampMs += 1 - return timestampMs - } - } - ) + @TestState var mockSnodeAPICache: MockSnodeAPICache! = MockSnodeAPICache() @TestState var threadVariant: SessionThread.Variant! = .contact @TestState var didTriggerSearchCallbackTriggered: Bool! = false @TestState var viewModel: ThreadSettingsViewModel! @@ -117,7 +87,39 @@ class ThreadSettingsViewModelSpec: AsyncSpec { ) .store(in: &disposables) } - + + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + + mockLibSessionCache.defaultInitialSetup() + dependencies.set(cache: .libSession, to: mockLibSessionCache) + + var timestampMs: Int64 = 1234567890000 + mockSnodeAPICache.when { $0.clockOffsetMs }.thenReturn(0) + mockSnodeAPICache + .when { $0.currentOffsetTimestampMs() } + .thenReturn { _, _ in + /// **Note:** We need to increment this value every time it's accessed because otherwise any functions which + /// insert multiple `Interaction` values can end up running into unique constraint conflicts due to the timestamp + /// being identical between different interactions + timestampMs += 1 + return timestampMs + } + dependencies.set(cache: .snodeAPI, to: mockSnodeAPICache) + + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + try await mockStorage.writeAsync { db in + try Identity( + variant: .x25519PublicKey, + data: Data(hex: TestConstants.publicKey) + ).insert(db) + try Profile(id: userPubkey, name: "TestMe").insert(db) + try Profile(id: user2Pubkey, name: "TestUser").insert(db) + } + } + // MARK: - a ThreadSettingsViewModel describe("a ThreadSettingsViewModel") { beforeEach { diff --git a/SessionTests/Database/DatabaseSpec.swift b/SessionTests/Database/DatabaseSpec.swift index c9b824b196..47cd2e3240 100644 --- a/SessionTests/Database/DatabaseSpec.swift +++ b/SessionTests/Database/DatabaseSpec.swift @@ -12,7 +12,7 @@ import SessionNetworkingKit @testable import SessionMessagingKit @testable import SessionUtilitiesKit -class DatabaseSpec: QuickSpec { +class DatabaseSpec: AsyncSpec { fileprivate static let ignoredTables: Set = [ "sqlite_sequence", "grdb_migrations", "*_fts*" ] @@ -26,17 +26,11 @@ class DatabaseSpec: QuickSpec { customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - } - ) - @TestState(cache: .libSession, in: dependencies) var libSessionCache: LibSession.Cache! = LibSession.Cache( + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + @TestState var libSessionCache: LibSession.Cache! = LibSession.Cache( userSessionId: SessionId(.standard, hex: TestConstants.publicKey), using: dependencies ) - @TestState var initialResult: Result! = nil - @TestState var finalResult: Result! = nil let allMigrations: [Migration.Type] = SNMessagingKit.migrations let dynamicTests: [MigrationTest] = MigrationTest.extractTests(allMigrations) @@ -63,29 +57,34 @@ class DatabaseSpec: QuickSpec { snapshotCache.removeAll() } + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + + dependencies.set(cache: .libSession, to: libSessionCache) + } + // MARK: - a Database describe("a Database") { // MARK: -- can be created from an empty state it("can be created from an empty state") { - mockStorage.perform( - migrations: allMigrations, - async: false, - onProgressUpdate: nil, - onComplete: { result in initialResult = result } - ) - - expect(initialResult).to(beSuccess()) + expect { + try await mockStorage.perform( + migrations: allMigrations, + onProgressUpdate: nil + ) + }.toNot(throwError()) } // MARK: -- can still parse the database table types it("can still parse the database table types") { - mockStorage.perform( - migrations: allMigrations, - async: false, - onProgressUpdate: nil, - onComplete: { result in initialResult = result } - ) - expect(initialResult).to(beSuccess()) + expect { + try await mockStorage.perform( + migrations: allMigrations, + onProgressUpdate: nil + ) + }.toNot(throwError()) // Generate dummy data (fetching below won't do anything) expect(try MigrationTest.generateDummyData(mockStorage, nullsWherePossible: false)) @@ -102,13 +101,12 @@ class DatabaseSpec: QuickSpec { // MARK: -- can still parse the database types setting null where possible it("can still parse the database types setting null where possible") { - mockStorage.perform( - migrations: allMigrations, - async: false, - onProgressUpdate: nil, - onComplete: { result in initialResult = result } - ) - expect(initialResult).to(beSuccess()) + expect { + try await mockStorage.perform( + migrations: allMigrations, + onProgressUpdate: nil + ) + }.toNot(throwError()) // Generate dummy data (fetching below won't do anything) expect(try MigrationTest.generateDummyData(mockStorage, nullsWherePossible: true)) @@ -126,7 +124,7 @@ class DatabaseSpec: QuickSpec { // MARK: -- can migrate from X to Y dynamicTests.forEach { test in it("can migrate from \(test.initialMigrationIdentifier) to \(test.finalMigrationIdentifier)") { - let initialStateResult: Result = { + let initialStateResult: Result = await { if let cachedResult: Result = snapshotCache[test.initialMigrationIdentifier] { return cachedResult } @@ -139,14 +137,10 @@ class DatabaseSpec: QuickSpec { ) // Generate dummy data (otherwise structural issues or invalid foreign keys won't error) - var initialResult: Result! - storage.perform( + try await mockStorage.perform( migrations: test.initialMigrations, - async: false, - onProgressUpdate: nil, - onComplete: { result in initialResult = result } + onProgressUpdate: nil ) - try initialResult.get() // Generate dummy data (otherwise structural issues or invalid foreign keys won't error) try MigrationTest.generateDummyData(storage, nullsWherePossible: false) @@ -173,19 +167,14 @@ class DatabaseSpec: QuickSpec { mockStorage = SynchronousStorage(customWriter: testDb, using: dependencies) // Peform the target migrations to ensure the migrations themselves worked correctly - mockStorage.perform( - migrations: test.migrationsToTest, - async: false, - onProgressUpdate: nil, - onComplete: { result in finalResult = result } - ) - - switch finalResult { - case .success: break - case .failure(let error): - fail("Failed to migrate from '\(test.initialMigrationIdentifier)' to '\(test.finalMigrationIdentifier)'. Error: \(error)") - case .none: - fail("Failed to migrate from '\(test.initialMigrationIdentifier)' to '\(test.finalMigrationIdentifier)'. Error: No result") + do { + try await mockStorage.perform( + migrations: test.migrationsToTest, + onProgressUpdate: nil + ) + } + catch { + fail("Failed to migrate from '\(test.initialMigrationIdentifier)' to '\(test.finalMigrationIdentifier)'. Error: \(error)") } } } diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index 0efaa2dd9c..15c26ea4c8 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -7,6 +7,7 @@ import Nimble import SessionUtil import SessionUIKit import SessionUtilitiesKit +import TestUtilities @testable import Session @testable import SessionNetworkingKit @@ -24,7 +25,6 @@ class OnboardingSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, using: dependencies ) @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( @@ -59,21 +59,8 @@ class OnboardingSpec: AsyncSpec { .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) } ) - @TestState(cache: .libSession, in: dependencies) var mockLibSession: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { cache in - cache.defaultInitialSetup() - cache - .when { - $0.profile( - contactId: .any, - threadId: .any, - threadVariant: .any, - visibleMessage: .any - ) - } - .thenReturn(nil) - } - ) + @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + @TestState var mockLibSession: MockLibSessionCache! = MockLibSessionCache() @TestState(defaults: .standard, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults( initialSetup: { defaults in defaults.when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) }.thenReturn(true) @@ -83,17 +70,9 @@ class OnboardingSpec: AsyncSpec { defaults.when { $0.set(false, forKey: .any) }.thenReturn(()) } ) - @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( - initialSetup: { cache in - cache.when { $0.userExists }.thenReturn(true) - cache.when { $0.setSecretKey(ed25519SecretKey: .any) }.thenReturn(()) - cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - cache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) - } - ) @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( initialSetup: { network in - network.when { $0.getSwarm(for: .any) }.thenReturn([ + network.when { try await $0.getSwarm(for: .any) }.thenReturn([ LibSession.Snode( ed25519PubkeyHex: "1234", ip: "1.2.3.4", @@ -174,11 +153,33 @@ class OnboardingSpec: AsyncSpec { .thenReturn(()) } ) - @TestState(cache: .snodeAPI, in: dependencies) var mockSnodeAPICache: MockSnodeAPICache! = MockSnodeAPICache( - initialSetup: { $0.defaultInitialSetup() } - ) + @TestState var mockSnodeAPICache: MockSnodeAPICache! = MockSnodeAPICache() @TestState var disposables: [AnyCancellable]! = [] - @TestState var cache: Onboarding.Cache! + @TestState var manager: Onboarding.Manager! + + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + + mockLibSession.defaultInitialSetup() + mockLibSession + .when { + $0.profile( + contactId: .any, + threadId: .any, + threadVariant: .any, + visibleMessage: .any + ) + } + .thenReturn(nil) + dependencies.set(cache: .libSession, to: mockLibSession) + + mockSnodeAPICache.defaultInitialSetup() + dependencies.set(cache: .snodeAPI, to: mockSnodeAPICache) + + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + } // MARK: - an Onboarding Cache - Initialization describe("an Onboarding Cache when initialising") { @@ -189,7 +190,7 @@ class OnboardingSpec: AsyncSpec { } justBeforeEach { - cache = Onboarding.Cache( + manager = Onboarding.Manager( flow: .restore, using: dependencies ) @@ -197,12 +198,12 @@ class OnboardingSpec: AsyncSpec { // MARK: -- stores the initialFlow it("stores the initialFlow") { - Onboarding.Flow.allCases.forEach { flow in - cache = Onboarding.Cache( + for flow in Onboarding.Flow.allCases { + manager = Onboarding.Manager( flow: flow, using: dependencies ) - expect(cache.initialFlow).to(equal(flow)) + await expect { await manager.initialFlow }.to(equal(flow)) } } @@ -223,11 +224,11 @@ class OnboardingSpec: AsyncSpec { // MARK: ---- generates new key pairs it("generates new key pairs") { - expect(cache.ed25519KeyPair.publicKey.toHexString()).to(equal("010203")) - expect(cache.ed25519KeyPair.secretKey.toHexString()).to(equal("040506")) - expect(cache.x25519KeyPair.publicKey.toHexString()).to(equal("030201")) - expect(cache.x25519KeyPair.secretKey.toHexString()).to(equal("060504")) - expect(cache.userSessionId).to(equal(SessionId(.standard, hex: "030201"))) + await expect { await manager.ed25519KeyPair.publicKey.toHexString() }.to(equal("010203")) + await expect { await manager.ed25519KeyPair.secretKey.toHexString() }.to(equal("040506")) + await expect { await manager.x25519KeyPair.publicKey.toHexString() }.to(equal("030201")) + await expect { await manager.x25519KeyPair.secretKey.toHexString() }.to(equal("060504")) + await expect { await manager.userSessionId }.to(equal(SessionId(.standard, hex: "030201"))) } } @@ -249,14 +250,14 @@ class OnboardingSpec: AsyncSpec { // MARK: ---- does not generate a seed it("does not generate a seed") { - expect(cache.seed.isEmpty).to(beTrue()) + await expect { await manager.seed.isEmpty }.to(beTrue()) } // MARK: ---- loads the ed25519 key pair from the database it("loads the ed25519 key pair from the database") { - expect(cache.ed25519KeyPair.publicKey.toHexString()) + await expect { await manager.ed25519KeyPair.publicKey.toHexString() } .to(equal(TestConstants.edPublicKey)) - expect(cache.ed25519KeyPair.secretKey.toHexString()) + await expect { await manager.ed25519KeyPair.secretKey.toHexString() } .to(equal(TestConstants.edSecretKey)) } @@ -269,15 +270,15 @@ class OnboardingSpec: AsyncSpec { $0.generate(.x25519(ed25519Seckey: Array(Data(hex: TestConstants.edSecretKey)))) }) - expect(cache.x25519KeyPair.publicKey.toHexString()) - .to(equal(TestConstants.publicKey)) - expect(cache.x25519KeyPair.secretKey.toHexString()) + await expect { await manager.x25519KeyPair.publicKey.toHexString() } + .to(equal(TestConstants.publicKey)) + await expect { await manager.x25519KeyPair.secretKey.toHexString() } .to(equal(TestConstants.privateKey)) } // MARK: ---- generates the sessionId from the generated x25519PublicKey it("generates the sessionId from the generated x25519PublicKey") { - expect(cache.userSessionId) + await expect { await manager.userSessionId } .to(equal(SessionId(.standard, hex: TestConstants.publicKey))) } @@ -288,7 +289,7 @@ class OnboardingSpec: AsyncSpec { mockCrypto.removeMocksFor { $0.generate(.ed25519Seed(ed25519SecretKey: .any)) } mockCrypto .when { try $0.tryGenerate(.ed25519Seed(ed25519SecretKey: .any)) } - .thenThrow(MockError.mockedData) + .thenThrow(MockError.mock) mockCrypto .when { $0.generate(.ed25519KeyPair( @@ -315,23 +316,24 @@ class OnboardingSpec: AsyncSpec { // MARK: ------ generates new credentials it("generates new credentials") { - expect(cache.state).to(equal(.noUserInvalidKeyPair)) - expect(cache.seed).to(equal(Data([1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8]))) - expect(cache.ed25519KeyPair.publicKey.toHexString()).to(equal("010203")) - expect(cache.ed25519KeyPair.secretKey.toHexString()).to(equal("040506")) - expect(cache.x25519KeyPair.publicKey.toHexString()).to(equal("04030201")) - expect(cache.x25519KeyPair.secretKey.toHexString()).to(equal("07060504")) - expect(cache.userSessionId).to(equal(SessionId(.standard, hex: "04030201"))) + await expect { await manager.state.first() }.to(equal(.noUserInvalidKeyPair)) + await expect { await manager.seed } + .to(equal(Data([1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8]))) + await expect { await manager.ed25519KeyPair.publicKey.toHexString() }.to(equal("010203")) + await expect { await manager.ed25519KeyPair.secretKey.toHexString() }.to(equal("040506")) + await expect { await manager.x25519KeyPair.publicKey.toHexString() }.to(equal("04030201")) + await expect { await manager.x25519KeyPair.secretKey.toHexString() }.to(equal("07060504")) + await expect { await manager.userSessionId }.to(equal(SessionId(.standard, hex: "04030201"))) } // MARK: ------ goes into an invalid state when generating a seed fails it("goes into an invalid state when generating a seed fails") { mockCrypto.when { $0.generate(.randomBytes(.any)) }.thenReturn(nil as Data?) - cache = Onboarding.Cache( + manager = Onboarding.Manager( flow: .restore, using: dependencies ) - expect(cache.state).to(equal(.noUserInvalidSeedGeneration)) + await expect{ await manager.state.first() }.to(equal(.noUserInvalidSeedGeneration)) } // MARK: ------ does not load the useAPNs flag from user defaults @@ -372,13 +374,13 @@ class OnboardingSpec: AsyncSpec { // MARK: ------ stores the loaded displayName it("stores the loaded displayName") { - expect(cache.displayName).to(equal("TestProfileName")) + await expect{ await manager.displayName.first() }.to(equal("TestProfileName")) } // MARK: ------ loads the useAPNs setting from user defaults it("loads the useAPNs setting from user defaults") { expect(mockUserDefaults).to(call { $0.bool(forKey: .any) }) - expect(cache.useAPNS).to(beTrue()) + await expect{ await manager.useAPNS }.to(beTrue()) } // MARK: ------ after generating new credentials @@ -388,7 +390,7 @@ class OnboardingSpec: AsyncSpec { mockCrypto.removeMocksFor { $0.generate(.ed25519Seed(ed25519SecretKey: .any)) } mockCrypto .when { try $0.tryGenerate(.ed25519Seed(ed25519SecretKey: .any)) } - .thenThrow(MockError.mockedData) + .thenThrow(MockError.mock) mockCrypto .when { $0.generate(.ed25519KeyPair( @@ -415,7 +417,7 @@ class OnboardingSpec: AsyncSpec { // MARK: -------- has an empty display name it("has an empty display name") { - expect(cache.displayName).to(equal("")) + await expect { await manager.displayName.first() }.to(equal("")) } } } @@ -430,13 +432,13 @@ class OnboardingSpec: AsyncSpec { // MARK: ------ has an empty display name it("has an empty display name") { - expect(cache.displayName).to(equal("")) + await expect { await manager.displayName.first() }.to(equal("")) } // MARK: ------ loads the useAPNs setting from user defaults it("loads the useAPNs setting from user defaults") { expect(mockUserDefaults).to(call { $0.bool(forKey: .any) }) - expect(cache.useAPNS).to(beTrue()) + await expect { await manager.useAPNS }.to(beTrue()) } } } @@ -456,36 +458,35 @@ class OnboardingSpec: AsyncSpec { } justBeforeEach { - cache = Onboarding.Cache( + manager = Onboarding.Manager( flow: .register, using: dependencies ) - cache.displayNamePublisher.sinkAndStore(in: &disposables) - try? cache.setSeedData(Data(hex: TestConstants.edKeySeed).prefix(upTo: 16)) + try? await manager.setSeedData(Data(hex: TestConstants.edKeySeed).prefix(upTo: 16)) } // MARK: -- throws if the seed is the wrong length it("throws if the seed is the wrong length") { - expect { try cache.setSeedData(Data([1, 2, 3])) } + expect { try await manager.setSeedData(Data([1, 2, 3])) } .to(throwError(CryptoError.invalidSeed)) } // MARK: -- stores the seed it("stores the seed") { - expect(cache.seed).to(equal(Data(hex: TestConstants.edKeySeed).prefix(upTo: 16))) + await expect { await manager.seed }.to(equal(Data(hex: TestConstants.edKeySeed).prefix(upTo: 16))) } // MARK: -- stores the generated identity it("stores the generated identity") { - expect(cache.ed25519KeyPair.publicKey.toHexString()) + await expect { await manager.ed25519KeyPair.publicKey.toHexString() } .to(equal(TestConstants.edPublicKey)) - expect(cache.ed25519KeyPair.secretKey.toHexString()) + await expect { await manager.ed25519KeyPair.secretKey.toHexString() } .to(equal(TestConstants.edSecretKey)) - expect(cache.x25519KeyPair.publicKey.toHexString()) + await expect { await manager.x25519KeyPair.publicKey.toHexString() } .to(equal(TestConstants.publicKey)) - expect(cache.x25519KeyPair.secretKey.toHexString()) + await expect { await manager.x25519KeyPair.secretKey.toHexString() } .to(equal(TestConstants.privateKey)) - expect(cache.userSessionId) + await expect { await manager.userSessionId } .to(equal(SessionId(.standard, hex: TestConstants.publicKey))) } @@ -516,29 +517,16 @@ class OnboardingSpec: AsyncSpec { }) } - // MARK: -- the display name to be set to the successful result - it("the display name to be set to the successful result") { - await expect(cache.displayName).toEventually(equal("TestPolledName")) - } - - // MARK: -- the publisher to emit the display name - it("the publisher to emit the display name") { - var value: String? - cache - .displayNamePublisher - .sink( - receiveCompletion: { _ in }, - receiveValue: { value = $0 } - ) - .store(in: &disposables) - await expect(value).toEventually(equal("TestPolledName")) + // MARK: -- the display name stream to output the correct value + it("the display name stream to output the correct value") { + await expect { await manager.displayName.first() }.toEventually(equal("TestPolledName")) } } // MARK: - an Onboarding Cache - Setting values describe("an Onboarding Cache when setting values") { justBeforeEach { - cache = Onboarding.Cache( + manager = Onboarding.Manager( flow: .register, using: dependencies ) @@ -546,28 +534,28 @@ class OnboardingSpec: AsyncSpec { // MARK: -- stores the useAPNs setting it("stores the useAPNs setting") { - expect(cache.useAPNS).to(beFalse()) - cache.setUseAPNS(true) - expect(cache.useAPNS).to(beTrue()) + await expect { await manager.useAPNS }.to(beFalse()) + await manager.setUseAPNS(true) + await expect { await manager.useAPNS }.to(beTrue()) } // MARK: -- stores the display name it("stores the display name") { - expect(cache.displayName).to(equal("")) - cache.setDisplayName("TestName") - expect(cache.displayName).to(equal("TestName")) + await expect { await manager.displayName.first() }.to(equal("")) + await manager.setDisplayName("TestName") + await expect { await manager.displayName.first() }.to(equal("TestName")) } } // MARK: - an Onboarding Cache - Complete Registration describe("an Onboarding Cache when completing registration") { justBeforeEach { - cache = Onboarding.Cache( + manager = Onboarding.Manager( flow: .register, using: dependencies ) - cache.setDisplayName("TestCompleteName") - cache.completeRegistration() + await manager.setDisplayName("TestCompleteName") + await manager.completeRegistration() } // MARK: -- stores the ed25519 secret key in the general cache @@ -696,7 +684,7 @@ class OnboardingSpec: AsyncSpec { // MARK: -- updates the onboarding state to 'completed' it("updates the onboarding state to 'completed'") { - expect(cache.state).to(equal(.completed)) + await expect { await manager.state.first() }.to(equal(.completed)) } // MARK: -- updates the hasViewedSeed value only when restoring @@ -705,12 +693,12 @@ class OnboardingSpec: AsyncSpec { await expect(dependencies[cache: .libSession]?.get(.hasViewedSeed)).toEventually(beFalse()) // Then the `restore` case - cache = Onboarding.Cache( + manager = Onboarding.Manager( flow: .restore, using: dependencies ) - cache.setDisplayName("TestCompleteName") - cache.completeRegistration() + await manager.setDisplayName("TestCompleteName") + await manager.completeRegistration() await expect(dependencies[cache: .libSession]?.get(.hasViewedSeed)).toEventually(beTrue()) } @@ -733,35 +721,17 @@ class OnboardingSpec: AsyncSpec { }) } - // MARK: -- emits an event from the completion publisher - it("emits an event from the completion publisher") { - var didEmitInPublisher: Bool = false - - cache = Onboarding.Cache( - flow: .register, - using: dependencies - ) - cache.setDisplayName("TestCompleteName") - cache.onboardingCompletePublisher - .sink(receiveValue: { _ in didEmitInPublisher = true }) - .store(in: &disposables) - cache.completeRegistration() - - await expect(didEmitInPublisher).toEventually(beTrue()) - } - - // MARK: -- calls the onComplete callback - it("calls the onComplete callback") { - var didCallOnComplete: Bool = false - - cache = Onboarding.Cache( + // MARK: -- emits the complete status + it("emits the complete status") { + manager = Onboarding.Manager( flow: .register, using: dependencies ) - cache.setDisplayName("TestCompleteName") - cache.completeRegistration { didCallOnComplete = true } + await expect { await manager.state.first() }.toNot(equal(.completed)) + await manager.setDisplayName("TestCompleteName") + await manager.completeRegistration() - await expect(didCallOnComplete).toEventually(beTrue()) + await expect { await manager.state.first() }.to(equal(.completed)) } } } diff --git a/SessionTests/Settings/NotificationContentViewModelSpec.swift b/SessionTests/Settings/NotificationContentViewModelSpec.swift index 2a55c6ffae..a9a81ed535 100644 --- a/SessionTests/Settings/NotificationContentViewModelSpec.swift +++ b/SessionTests/Settings/NotificationContentViewModelSpec.swift @@ -21,7 +21,6 @@ class NotificationContentViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: SNMessagingKit.migrations, using: dependencies ) @TestState var secretKey: [UInt8]! = Array(Data(hex: TestConstants.edSecretKey)) @@ -31,25 +30,30 @@ class NotificationContentViewModelSpec: AsyncSpec { return .local(conf) }() - @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { - $0.defaultInitialSetup( - configs: [ - .local: localConfig - ] - ) - } - ) - @TestState var viewModel: NotificationContentViewModel! = TestState.create { - await NotificationContentViewModel(using: dependencies) - } - @TestState var dataChangeCancellable: AnyCancellable? = viewModel.tableDataPublisher - .sink( - receiveCompletion: { _ in }, - receiveValue: { viewModel.updateTableData($0) } - ) + @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() + @TestState var viewModel: NotificationContentViewModel! + @TestState var dataChangeCancellable: AnyCancellable? @TestState var dismissCancellable: AnyCancellable? + beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockLibSessionCache.defaultInitialSetup( + configs: [ + .local: localConfig + ] + ) + dependencies.set(cache: .libSession, to: mockLibSessionCache) + + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + + viewModel = await NotificationContentViewModel(using: dependencies) + dataChangeCancellable = viewModel.tableDataPublisher + .sink( + receiveCompletion: { _ in }, + receiveValue: { viewModel.updateTableData($0) } + ) + } + // MARK: - a NotificationContentViewModel describe("a NotificationContentViewModel") { beforeEach { diff --git a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift index 5101300fc7..e94d94a73c 100644 --- a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift +++ b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift @@ -8,17 +8,20 @@ import Nimble @testable import SessionUtilitiesKit -class IdentitySpec: QuickSpec { +class IdentitySpec: AsyncSpec { override class func spec() { // MARK: Configuration @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrations: [_001_SUK_InitialSetupMigration.self], using: dependencies ) + beforeEach { + try await mockStorage.perform(migrations: [_001_SUK_InitialSetupMigration.self]) + } + // MARK: - an Identity describe("an Identity") { // MARK: -- correctly retrieves the user key pair diff --git a/TestUtilities/MockFunctionBuilder.swift b/TestUtilities/MockFunctionBuilder.swift index 0e849408f6..db207f155c 100644 --- a/TestUtilities/MockFunctionBuilder.swift +++ b/TestUtilities/MockFunctionBuilder.swift @@ -36,26 +36,21 @@ public class MockFunctionBuilder { return self } - public func thenReturn(_ value: R) async { + public func thenReturn(_ value: R) async throws { self.returnValue = value - await finalize() + try await finalize() } - public func thenThrow(_ error: Error) async { + public func thenThrow(_ error: Error) async throws { self.returnError = error - await finalize() + try await finalize() } // MARK: - Internal Functions - private func finalize() async { - do { - let function = try await self.build() - handler.register(stub: function) - } catch { - /// If the build fails (e.g., invalid `when` block), we should fail the test - handler.reportFailureFromBuilder(error) - } + private func finalize() async throws { + let function = try await self.build() + handler.register(stub: function) } private func captureDetails() async { diff --git a/TestUtilities/MockHandler.swift b/TestUtilities/MockHandler.swift index 0303dbd019..ad58c8f733 100644 --- a/TestUtilities/MockHandler.swift +++ b/TestUtilities/MockHandler.swift @@ -49,15 +49,6 @@ public final class MockHandler { stubs[key, default: []].append(stub) } - internal func reportFailureFromBuilder( - _ error: Error, - fileID: String = #fileID, - file: String = #file, - line: UInt = #line - ) { - failureReporter.reportFailure("Mocking Framework Error during stubbing: \(error)", fileID: fileID, file: file, line: line) - } - // MARK: - Verification func recordedCalls(for functionBlock: @escaping (T) async throws -> R) async -> [RecordedCall]? { @@ -257,15 +248,11 @@ public extension MockHandler { case .failure(let error): /// Log if the failure was due to a missing mock if case MockError.noStubFound(_, _) = error { - let targetFileID: String = (TestContext.current?.fileID ?? fileID) - let targetFile: String = (TestContext.current?.file ?? file) - let targetLine: UInt = (TestContext.current?.line ?? line) - failureReporter.reportFailure( "Mocking Error: An unstubbed function was called: `\(funcName)`", - fileID: targetFileID, - file: targetFile, - line: targetLine + fileID: fileID, + file: file, + line: line ) } diff --git a/TestUtilities/TextContext.swift b/TestUtilities/TextContext.swift deleted file mode 100644 index a33914fa01..0000000000 --- a/TestUtilities/TextContext.swift +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -public final class TestContext { - public var fileID: String = #fileID - public var file: String = #filePath - public var line: UInt = #line - - @TaskLocal public static var current: TestContext? -} - -public func withTestContext( - fileID: String = #fileID, - file: String = #file, - line: UInt = #line, - _ body: () async throws -> T -) async rethrows -> T { - let context: TestContext = TestContext() - context.fileID = fileID - context.file = file - context.line = line - - return try await TestContext.$current.withValue(context) { - try await body() - } -} - diff --git a/_SharedTestUtilities/MockGeneralCache.swift b/_SharedTestUtilities/MockGeneralCache.swift index 18d30ffe9f..413dba0fb8 100644 --- a/_SharedTestUtilities/MockGeneralCache.swift +++ b/_SharedTestUtilities/MockGeneralCache.swift @@ -38,3 +38,16 @@ class MockGeneralCache: Mock, GeneralCacheType { mockNoReturn(args: [ed25519SecretKey]) } } + +// MARK: - Convenience + +extension Mock where T == GeneralCacheType { + func defaultInitialSetup() { + self.when { $0.userExists }.thenReturn(true) + self.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) + self.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) + self + .when { $0.ed25519Seed } + .thenReturn(Array(Array(Data(hex: TestConstants.edSecretKey)).prefix(upTo: 32))) + } +} diff --git a/_SharedTestUtilities/Quick+TestContext.swift b/_SharedTestUtilities/Quick+TestContext.swift deleted file mode 100644 index deefc3726e..0000000000 --- a/_SharedTestUtilities/Quick+TestContext.swift +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import Quick -import TestUtilities - -public extension AsyncSpec { - static func itTracked( - _ description: String, - fileID: String = #fileID, - file: String = #file, - line: UInt = #line, - closure: @escaping () async throws -> Void - ) { - it(description, file: file, line: line) { - try await withTestContext(fileID: fileID, file: file, line: line) { - try await closure() - } - } - } - - static func fitTracked( - _ description: String, - fileID: String = #fileID, - file: String = #file, - line: UInt = #line, - closure: @escaping () async throws -> Void - ) { - fit(description, file: file, line: line) { - try await withTestContext(fileID: fileID, file: file, line: line) { - try await closure() - } - } - } -} diff --git a/_SharedTestUtilities/TestDependencies.swift b/_SharedTestUtilities/TestDependencies.swift index 2f68028da3..0b4f580966 100644 --- a/_SharedTestUtilities/TestDependencies.swift +++ b/_SharedTestUtilities/TestDependencies.swift @@ -254,22 +254,6 @@ protocol DependenciesSettable { // MARK: - TestState Convenience internal extension TestState { - init( - wrappedValue: @escaping @autoclosure () -> T?, - cache: CacheConfig, - in dependenciesRetriever: @escaping @autoclosure () -> TestDependencies? - ) async where T: MutableCacheType { - self.init(wrappedValue: { - let dependencies: TestDependencies? = dependenciesRetriever() - let value: T? = wrappedValue() - (value as? DependenciesSettable)?.setDependencies(dependencies) - dependencies?[cache: cache] = (value as! M) - (value as? (any InitialSetupable))?.performInitialSetup() - - return value - }()) - } - init( wrappedValue: @escaping @autoclosure () -> T?, singleton: SingletonConfig, From b6086c180cccd9802fe96b8eb38f39577bc1e462 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 8 Sep 2025 16:35:40 +1000 Subject: [PATCH 38/59] Fixed a number of test crashes, updated MockCrypto to be Mockable --- .../LibSession+SessionMessagingKit.swift | 6 +- .../Pollers/CommunityPoller.swift | 6 +- .../Jobs/DisplayPictureDownloadJobSpec.swift | 113 +++++++------- .../LibSession/LibSessionGroupInfoSpec.swift | 28 ++-- .../LibSessionGroupMembersSpec.swift | 40 +++-- .../LibSession/LibSessionSpec.swift | 90 +++++------ .../Open Groups/Models/SOGSMessageSpec.swift | 31 ++-- .../Open Groups/OpenGroupAPISpec.swift | 102 ++++++------ .../Open Groups/OpenGroupManagerSpec.swift | 108 ++++++------- .../MessageReceiverGroupsSpec.swift | 30 ++-- .../MessageSenderGroupsSpec.swift | 93 ++++++----- .../MessageSenderSpec.swift | 38 +++-- .../Pollers/CommunityPollerManagerSpec.swift | 16 +- .../Utilities/ExtensionHelperSpec.swift | 147 +++++++++--------- .../_TestUtilities/Mocked+SMK.swift | 10 ++ SessionNetworkingKit/Types/Network.swift | 6 +- .../ThreadSettingsViewModelSpec.swift | 12 +- SessionTests/Database/DatabaseSpec.swift | 30 +--- SessionTests/Onboarding/OnboardingSpec.swift | 118 +++++++------- .../General/GeneralCacheSpec.swift | 36 ++--- TestUtilities/MockHandler.swift | 50 ++++-- TestUtilities/Mockable.swift | 4 + _SharedTestUtilities/MockCrypto.swift | 17 +- _SharedTestUtilities/MockGeneralCache.swift | 1 + _SharedTestUtilities/SynchronousStorage.swift | 13 +- _SharedTestUtilities/TestDependencies.swift | 4 + 26 files changed, 589 insertions(+), 560 deletions(-) diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index af8cf93315..04babf5755 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -1169,15 +1169,15 @@ public extension LibSessionCacheType { return try perform(for: variant, sessionId: userSessionId, change: { _ in try change() }) } - func addEvent(key: ObservableKey, value: AnyHashable?) { + func addEvent(key: ObservableKey, value: T?) { addEvent(ObservedEvent(key: key, value: value)) } - func addEvent(key: Setting.BoolKey, value: AnyHashable?) { + func addEvent(key: Setting.BoolKey, value: T?) { addEvent(ObservedEvent(key: .setting(key), value: value)) } - func addEvent(key: Setting.EnumKey, value: AnyHashable?) { + func addEvent(key: Setting.EnumKey, value: T?) { addEvent(ObservedEvent(key: .setting(key), value: value)) } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift index 1616a5dc22..1bf3db6017 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift @@ -691,7 +691,11 @@ actor CommunityPollerManager: CommunityPollerManagerType { /// We manually handle thread-safety using the `NSLock` so can ensure this is `Sendable` public final class CommunityPollerManagerSyncState: @unchecked Sendable { private let lock = NSLock() - private var _serversBeingPolled: Set = [] + private var _serversBeingPolled: Set + + public init(serversBeingPolled: Set = []) { + self._serversBeingPolled = serversBeingPolled + } public var serversBeingPolled: Set { lock.withLock { _serversBeingPolled } } diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index f5569fbd25..2ad09df9f5 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -2,6 +2,7 @@ import Foundation import GRDB +import TestUtilities import Quick import Nimble @@ -58,30 +59,7 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { @TestState(singleton: .fileManager, in: dependencies) var mockFileManager: MockFileManager! = MockFileManager( initialSetup: { $0.defaultInitialSetup() } ) - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( - initialSetup: { crypto in - crypto.when { $0.generate(.uuid()) }.thenReturn(UUID(uuidString: "00000000-0000-0000-0000-000000001234")) - crypto - .when { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } - .thenReturn(imageData) - crypto.when { $0.generate(.hash(message: .any, length: .any)) }.thenReturn("TestHash".bytes) - crypto - .when { $0.generate(.blinded15KeyPair(serverPublicKey: .any, ed25519SecretKey: .any)) } - .thenReturn( - KeyPair( - publicKey: Data(hex: TestConstants.publicKey).bytes, - secretKey: Data(hex: TestConstants.edSecretKey).bytes - ) - ) - crypto - .when { $0.generate(.randomBytes(16)) } - .thenReturn(Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!.bytes) - crypto - .when { $0.generate(.signatureBlind15(message: .any, serverPublicKey: .any, ed25519SecretKey: .any)) } - .thenReturn("TestSogsSignature".bytes) - } - ) - + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() @TestState(singleton: .imageDataManager, in: dependencies) var mockImageDataManager: MockImageDataManager! = MockImageDataManager( initialSetup: { imageDataManager in imageDataManager @@ -106,6 +84,26 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) } + + try await mockCrypto.when { $0.generate(.uuid()) }.thenReturn(UUID(uuidString: "00000000-0000-0000-0000-000000001234")) + try await mockCrypto + .when { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } + .thenReturn(imageData) + try await mockCrypto.when { $0.generate(.hash(message: .any, length: .any)) }.thenReturn("TestHash".bytes) + try await mockCrypto + .when { $0.generate(.blinded15KeyPair(serverPublicKey: .any, ed25519SecretKey: .any)) } + .thenReturn( + KeyPair( + publicKey: Data(hex: TestConstants.publicKey).bytes, + secretKey: Data(hex: TestConstants.edSecretKey).bytes + ) + ) + try await mockCrypto + .when { $0.generate(.randomBytes(16)) } + .thenReturn(Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!.bytes) + try await mockCrypto + .when { $0.generate(.signatureBlind15(message: .any, serverPublicKey: .any, ed25519SecretKey: .any)) } + .thenReturn("TestSogsSignature".bytes) } // MARK: - a DisplayPictureDownloadJob @@ -631,7 +629,7 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: ---- when it fails to decrypt the data context("when it fails to decrypt the data") { beforeEach { - mockCrypto + try await mockCrypto .when { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } .thenReturn(nil) } @@ -650,7 +648,7 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: ---- when it decrypts invalid image data context("when it decrypts invalid image data") { beforeEach { - mockCrypto + try await mockCrypto .when { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } .thenReturn(Data([1, 2, 3])) } @@ -746,13 +744,12 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: -------- does not save the picture it("does not save the picture") { - expect(mockCrypto) - .toNot(call { - $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) - }) + await mockCrypto + .verify { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } + .wasNotCalled() expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - expect(mockImageDataManager).toNotEventually(call { + await expect(mockImageDataManager).toNotEventually(call { await $0.load(.any) }) expect(mockStorage.read { db in try Profile.fetchOne(db) }).to(beNil()) @@ -774,13 +771,12 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: -------- does not save the picture it("does not save the picture") { - expect(mockCrypto) - .toNot(call { - $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) - }) + await mockCrypto + .verify { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } + .wasNotCalled() expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - expect(mockImageDataManager).toNotEventually(call { + await expect(mockImageDataManager).toNotEventually(call { await $0.load(.any) }) expect(mockStorage.read { db in try Profile.fetchOne(db) }) @@ -811,13 +807,12 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: -------- does not save the picture it("does not save the picture") { - expect(mockCrypto) - .toNot(call { - $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) - }) + await mockCrypto + .verify { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } + .wasNotCalled() expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - expect(mockImageDataManager).toNotEventually(call { + await expect(mockImageDataManager).toNotEventually(call { await $0.load(.any) }) expect(mockStorage.read { db in try Profile.fetchOne(db) }) @@ -847,10 +842,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: -------- saves the picture it("saves the picture") { - expect(mockCrypto) - .to(call { - $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) - }) + await mockCrypto + .verify { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } + .wasCalled(exactly: 1) expect(mockFileManager).to(call(.exactly(times: 1), matchingParameters: .all) { $0.createFile( atPath: "/test/DisplayPictures/5465737448617368", @@ -859,7 +853,7 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { ) }) - expect(mockImageDataManager) + await expect(mockImageDataManager) .toEventually(call(.exactly(times: 1), matchingParameters: .all) { await $0.load( .url(URL(fileURLWithPath: "/test/DisplayPictures/5465737448617368")) @@ -944,13 +938,12 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: -------- does not save the picture it("does not save the picture") { - expect(mockCrypto) - .toNot(call { - $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) - }) + await mockCrypto + .verify { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } + .wasNotCalled() expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - expect(mockImageDataManager).toNotEventually(call { + await expect(mockImageDataManager).toNotEventually(call { await $0.load(.any) }) expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) }).to(beNil()) @@ -971,13 +964,12 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: -------- does not save the picture it("does not save the picture") { - expect(mockCrypto) - .toNot(call { - $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) - }) + await mockCrypto + .verify { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } + .wasNotCalled() expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - expect(mockImageDataManager).toNotEventually(call { + await expect(mockImageDataManager).toNotEventually(call { await $0.load(.any) }) expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) }) @@ -1012,13 +1004,12 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: -------- does not save the picture it("does not save the picture") { - expect(mockCrypto) - .toNot(call { - $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) - }) + await mockCrypto + .verify { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } + .wasNotCalled() expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - expect(mockImageDataManager).toNotEventually(call { + await expect(mockImageDataManager).toNotEventually(call { await $0.load(.any) }) expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) }) diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift index 204f4b4068..d3dd1d9faa 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift @@ -54,9 +54,18 @@ class LibSessionGroupInfoSpec: AsyncSpec { .thenReturn([:]) } ) - @TestState var createGroupOutput: LibSession.CreatedGroupInfo! = { - mockStorage.write { db in - try LibSession.createGroup( + @TestState var createGroupOutput: LibSession.CreatedGroupInfo! + @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() + + beforeEach { + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + try await mockStorage.writeAsync { db in + try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) + try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) + + createGroupOutput = try LibSession.createGroup( db, name: "TestGroup", description: nil, @@ -66,10 +75,7 @@ class LibSessionGroupInfoSpec: AsyncSpec { using: dependencies ) } - }() - @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() - - beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) @@ -88,14 +94,6 @@ class LibSessionGroupInfoSpec: AsyncSpec { ) mockLibSessionCache.when { $0.configNeedsDump(.any) }.thenReturn(true) dependencies.set(cache: .libSession, to: mockLibSessionCache) - - try await mockStorage.perform(migrations: SNMessagingKit.migrations) - try await mockStorage.writeAsync { db in - try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) - try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - } } // MARK: - LibSessionGroupInfo diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift index 8854316acc..33a5b727a9 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift @@ -53,22 +53,28 @@ class LibSessionGroupMembersSpec: AsyncSpec { .thenReturn([:]) } ) - @TestState var createGroupOutput: LibSession.CreatedGroupInfo! = { - mockStorage.write { db in - try LibSession.createGroup( - db, - name: "TestGroup", - description: nil, - displayPictureUrl: nil, - displayPictureEncryptionKey: nil, - members: [], - using: dependencies - ) - } - }() + @TestState var createGroupOutput: LibSession.CreatedGroupInfo! @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() beforeEach { + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + try await mockStorage.writeAsync { db in + try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) + try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) + + createGroupOutput = try LibSession.createGroup( + db, + name: "TestGroup", + description: nil, + displayPictureUrl: nil, + displayPictureEncryptionKey: nil, + members: [], + using: dependencies + ) + } + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) @@ -86,14 +92,6 @@ class LibSessionGroupMembersSpec: AsyncSpec { ] ) dependencies.set(cache: .libSession, to: mockLibSessionCache) - - try await mockStorage.perform(migrations: SNMessagingKit.migrations) - try await mockStorage.writeAsync { db in - try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) - try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - } } // MARK: - LibSessionGroupMembers diff --git a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift index adaa01b2e4..16fdbc883b 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift @@ -24,40 +24,20 @@ class LibSessionSpec: AsyncSpec { customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( - initialSetup: { crypto in - crypto - .when { $0.generate(.ed25519KeyPair()) } - .thenReturn( - KeyPair( - publicKey: Array(Data(hex: "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece")), - secretKey: Array(Data( - hex: "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210" + - "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece" - )) - ) - ) - crypto - .when { $0.generate(.ed25519KeyPair(seed: .any)) } - .thenReturn( - KeyPair( - publicKey: Array(Data(hex: "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece")), - secretKey: Array(Data( - hex: "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210" + - "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece" - )) - ) - ) - crypto - .when { try $0.tryGenerate(.signature(message: .any, ed25519SecretKey: .any)) } - .thenReturn( - Authentication.Signature.standard(signature: Array("TestSignature".data(using: .utf8)!)) - ) - } - ) - @TestState var createGroupOutput: LibSession.CreatedGroupInfo! = { - mockStorage.write { db in - try LibSession.createGroup( + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() + @TestState var createGroupOutput: LibSession.CreatedGroupInfo! + @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() + @TestState var userGroupsConfig: LibSession.Config! + + beforeEach { + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + try await mockStorage.writeAsync { db in + try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) + try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) + + createGroupOutput = try LibSession.createGroup( db, name: "TestGroup", description: nil, @@ -67,11 +47,7 @@ class LibSessionSpec: AsyncSpec { using: dependencies ) } - }() - @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() - @TestState var userGroupsConfig: LibSession.Config! - - beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) @@ -90,13 +66,33 @@ class LibSessionSpec: AsyncSpec { ) dependencies.set(cache: .libSession, to: mockLibSessionCache) - try await mockStorage.perform(migrations: SNMessagingKit.migrations) - try await mockStorage.writeAsync { db in - try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) - try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - } + try await mockCrypto + .when { $0.generate(.ed25519KeyPair()) } + .thenReturn( + KeyPair( + publicKey: Array(Data(hex: "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece")), + secretKey: Array(Data( + hex: "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210" + + "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece" + )) + ) + ) + try await mockCrypto + .when { $0.generate(.ed25519KeyPair(seed: .any)) } + .thenReturn( + KeyPair( + publicKey: Array(Data(hex: "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece")), + secretKey: Array(Data( + hex: "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210" + + "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece" + )) + ) + ) + try await mockCrypto + .when { try $0.tryGenerate(.signature(message: .any, ed25519SecretKey: .any)) } + .thenReturn( + Authentication.Signature.standard(signature: Array("TestSignature".data(using: .utf8)!)) + ) } // MARK: - LibSession @@ -347,7 +343,7 @@ class LibSessionSpec: AsyncSpec { it("throws when it fails to generate a new identity ed25519 keyPair") { var resultError: Error? = nil - mockCrypto.when { $0.generate(.ed25519KeyPair()) }.thenReturn(nil) + try await mockCrypto.when { $0.generate(.ed25519KeyPair()) }.thenReturn(nil) mockStorage.write { db in do { diff --git a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift index ee3f4d94ac..6d696ffd5d 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift @@ -3,13 +3,14 @@ import Foundation import SessionNetworkingKit import SessionUtilitiesKit +import TestUtilities import Quick import Nimble @testable import SessionMessagingKit -class SOGSMessageSpec: QuickSpec { +class SOGSMessageSpec: AsyncSpec { override class func spec() { // MARK: Configuration @@ -28,7 +29,7 @@ class SOGSMessageSpec: QuickSpec { """ @TestState var messageData: Data! = messageJson.data(using: .utf8)! @TestState var dependencies: TestDependencies! = TestDependencies() - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto() + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() @TestState var decoder: JSONDecoder! = JSONDecoder(using: dependencies) // MARK: - a SOGSMessage @@ -199,7 +200,7 @@ class SOGSMessageSpec: QuickSpec { // MARK: -------- succeeds if it succeeds verification it("succeeds if it succeeds verification") { - mockCrypto + try await mockCrypto .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(true) @@ -211,14 +212,14 @@ class SOGSMessageSpec: QuickSpec { // MARK: -------- provides the correct values as parameters it("provides the correct values as parameters") { - mockCrypto + try await mockCrypto .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(true) _ = try? decoder.decode(OpenGroupAPI.Message.self, from: messageData) - expect(mockCrypto) - .to(call(matchingParameters: .all) { + await mockCrypto + .verify { $0.verify( .signature( message: Data(base64Encoded: "VGVzdERhdGE=")!.bytes, @@ -226,12 +227,13 @@ class SOGSMessageSpec: QuickSpec { signature: Data(base64Encoded: "VGVzdFNpZ25hdHVyZQ==")!.bytes ) ) - }) + } + .wasCalled(exactly: 1) } // MARK: -------- throws if it fails verification it("throws if it fails verification") { - mockCrypto + try await mockCrypto .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(false) @@ -246,7 +248,7 @@ class SOGSMessageSpec: QuickSpec { context("that is unblinded") { // MARK: -------- succeeds if it succeeds verification it("succeeds if it succeeds verification") { - mockCrypto + try await mockCrypto .when { $0.verify(.signatureXed25519(.any, curve25519PublicKey: .any, data: .any)) } .thenReturn(true) @@ -258,14 +260,14 @@ class SOGSMessageSpec: QuickSpec { // MARK: -------- provides the correct values as parameters it("provides the correct values as parameters") { - mockCrypto + try await mockCrypto .when { $0.verify(.signatureXed25519(.any, curve25519PublicKey: .any, data: .any)) } .thenReturn(true) _ = try? decoder.decode(OpenGroupAPI.Message.self, from: messageData) - expect(mockCrypto) - .to(call(matchingParameters: .all) { + await mockCrypto + .verify { $0.verify( .signatureXed25519( Data(base64Encoded: "VGVzdFNpZ25hdHVyZQ==")!, @@ -273,12 +275,13 @@ class SOGSMessageSpec: QuickSpec { data: Data(base64Encoded: "VGVzdERhdGE=")! ) ) - }) + } + .wasCalled(exactly: 1) } // MARK: -------- throws if it fails verification it("throws if it fails verification") { - mockCrypto + try await mockCrypto .when { $0.verify(.signatureXed25519(.any, curve25519PublicKey: .any, data: .any)) } .thenReturn(false) diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index 2ad9a482d3..674c98d937 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -11,7 +11,7 @@ import Nimble @testable import SessionMessagingKit -class OpenGroupAPISpec: QuickSpec { +class OpenGroupAPISpec: AsyncSpec { override class func spec() { // MARK: Configuration @@ -20,50 +20,7 @@ class OpenGroupAPISpec: QuickSpec { dependencies.forceSynchronous = true } @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork() - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( - initialSetup: { crypto in - crypto - .when { $0.generate(.hash(message: .any, key: .any, length: .any)) } - .thenReturn([]) - crypto - .when { $0.generate(.blinded15KeyPair(serverPublicKey: .any, ed25519SecretKey: .any)) } - .thenReturn( - KeyPair( - publicKey: Data(hex: TestConstants.publicKey).bytes, - secretKey: Data(hex: TestConstants.edSecretKey).bytes - ) - ) - crypto - .when { $0.generate(.signatureBlind15(message: .any, serverPublicKey: .any, ed25519SecretKey: .any)) } - .thenReturn("TestSogsSignature".bytes) - crypto - .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } - .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) - crypto - .when { $0.generate(.signatureXed25519(data: .any, curve25519PrivateKey: .any)) } - .thenReturn("TestStandardSignature".bytes) - crypto - .when { $0.generate(.randomBytes(16)) } - .thenReturn(Array(Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!)) - crypto - .when { $0.generate(.randomBytes(24)) } - .thenReturn(Array(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!)) - crypto - .when { $0.generate(.ed25519KeyPair(seed: .any)) } - .thenReturn( - KeyPair( - publicKey: Array(Data(hex: TestConstants.edPublicKey)), - secretKey: Array(Data(hex: TestConstants.edSecretKey)) - ) - ) - crypto - .when { $0.generate(.x25519(ed25519Pubkey: .any)) } - .thenReturn(Array(Data(hex: TestConstants.publicKey))) - crypto - .when { $0.generate(.x25519(ed25519Seckey: .any)) } - .thenReturn(Array(Data(hex: TestConstants.privateKey))) - } - ) + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() @TestState var disposables: [AnyCancellable]! = [] @@ -76,6 +33,47 @@ class OpenGroupAPISpec: QuickSpec { mockLibSessionCache.defaultInitialSetup() dependencies.set(cache: .libSession, to: mockLibSessionCache) + + try await mockCrypto + .when { $0.generate(.hash(message: .any, key: .any, length: .any)) } + .thenReturn([]) + try await mockCrypto + .when { $0.generate(.blinded15KeyPair(serverPublicKey: .any, ed25519SecretKey: .any)) } + .thenReturn( + KeyPair( + publicKey: Data(hex: TestConstants.publicKey).bytes, + secretKey: Data(hex: TestConstants.edSecretKey).bytes + ) + ) + try await mockCrypto + .when { $0.generate(.signatureBlind15(message: .any, serverPublicKey: .any, ed25519SecretKey: .any)) } + .thenReturn("TestSogsSignature".bytes) + try await mockCrypto + .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } + .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) + try await mockCrypto + .when { $0.generate(.signatureXed25519(data: .any, curve25519PrivateKey: .any)) } + .thenReturn("TestStandardSignature".bytes) + try await mockCrypto + .when { $0.generate(.randomBytes(16)) } + .thenReturn(Array(Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!)) + try await mockCrypto + .when { $0.generate(.randomBytes(24)) } + .thenReturn(Array(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!)) + try await mockCrypto + .when { $0.generate(.ed25519KeyPair(seed: .any)) } + .thenReturn( + KeyPair( + publicKey: Array(Data(hex: TestConstants.edPublicKey)), + secretKey: Array(Data(hex: TestConstants.edSecretKey)) + ) + ) + try await mockCrypto + .when { $0.generate(.x25519(ed25519Pubkey: .any)) } + .thenReturn(Array(Data(hex: TestConstants.publicKey))) + try await mockCrypto + .when { $0.generate(.x25519(ed25519Seckey: .any)) } + .thenReturn(Array(Data(hex: TestConstants.privateKey))) } // MARK: - an OpenGroupAPI @@ -1052,7 +1050,7 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ fails to sign if no signature is generated it("fails to sign if no signature is generated") { - mockCrypto + try await mockCrypto .when { $0.generate(.signatureXed25519(data: .any, curve25519PrivateKey: .any)) } .thenReturn(nil) @@ -1166,7 +1164,7 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ fails to sign if no signature is generated it("fails to sign if no signature is generated") { - mockCrypto + try await mockCrypto .when { $0.generate(.signatureBlind15(message: .any, serverPublicKey: .any, ed25519SecretKey: .any)) } .thenReturn(nil) @@ -1335,7 +1333,7 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ fails to sign if no signature is generated it("fails to sign if no signature is generated") { - mockCrypto + try await mockCrypto .when { $0.generate(.signatureXed25519(data: .any, curve25519PrivateKey: .any)) } .thenReturn(nil) @@ -1445,7 +1443,7 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ fails to sign if no signature is generated it("fails to sign if no signature is generated") { - mockCrypto + try await mockCrypto .when { $0.generate(.signatureBlind15(message: .any, serverPublicKey: .any, ed25519SecretKey: .any)) } .thenReturn(nil) @@ -2186,7 +2184,7 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ fails when the signature is not generated it("fails when the signature is not generated") { - mockCrypto + try await mockCrypto .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } .thenThrow(CryptoError.failedToGenerateOutput) @@ -2245,7 +2243,7 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ fails when the blindedKeyPair is not generated it("fails when the blindedKeyPair is not generated") { - mockCrypto + try await mockCrypto .when { $0.generate(.blinded15KeyPair(serverPublicKey: .any, ed25519SecretKey: .any)) } .thenReturn(nil) @@ -2269,7 +2267,7 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ fails when the sogsSignature is not generated it("fails when the sogsSignature is not generated") { - mockCrypto + try await mockCrypto .when { $0.generate(.blinded15KeyPair(serverPublicKey: .any, ed25519SecretKey: .any)) } .thenReturn(nil) diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index eaf8655f47..c12fd3922e 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -140,52 +140,10 @@ class OpenGroupManagerSpec: AsyncSpec { ) } .thenReturn(MockNetwork.errorResponse()) + network.when { $0.syncState }.thenReturn(NetworkSyncState(isSuspended: false)) } ) - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( - initialSetup: { crypto in - crypto.when { $0.generate(.hash(message: .any, length: .any)) }.thenReturn([]) - crypto - .when { $0.generate(.blinded15KeyPair(serverPublicKey: .any, ed25519SecretKey: .any)) } - .thenReturn( - KeyPair( - publicKey: Data(hex: TestConstants.publicKey).bytes, - secretKey: Data(hex: TestConstants.edSecretKey).bytes - ) - ) - crypto - .when { $0.generate(.blinded25KeyPair(serverPublicKey: .any, ed25519SecretKey: .any)) } - .thenReturn( - KeyPair( - publicKey: Data(hex: TestConstants.publicKey).bytes, - secretKey: Data(hex: TestConstants.edSecretKey).bytes - ) - ) - crypto - .when { $0.generate(.signatureBlind15(message: .any, serverPublicKey: .any, ed25519SecretKey: .any)) } - .thenReturn("TestSogsSignature".bytes) - crypto - .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } - .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) - crypto - .when { $0.generate(.randomBytes(16)) } - .thenReturn(Array(Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!)) - crypto - .when { $0.generate(.randomBytes(24)) } - .thenReturn(Array(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!)) - crypto - .when { $0.generate(.ed25519KeyPair(seed: .any)) } - .thenReturn( - KeyPair( - publicKey: Array(Data(hex: TestConstants.edPublicKey)), - secretKey: Array(Data(hex: TestConstants.edSecretKey)) - ) - ) - crypto - .when { $0.generate(.ciphertextWithXChaCha20(plaintext: .any, encKey: .any)) } - .thenReturn(Data([1, 2, 3])) - } - ) + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() @TestState(defaults: .standard, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults( initialSetup: { defaults in defaults.when { $0.integer(forKey: .any) }.thenReturn(0) @@ -256,6 +214,47 @@ class OpenGroupManagerSpec: AsyncSpec { try testOpenGroup.insert(db) try Capability(openGroupServer: testOpenGroup.server, variant: .sogs, isMissing: false).insert(db) } + + try await mockCrypto.when { $0.generate(.hash(message: .any, length: .any)) }.thenReturn([]) + try await mockCrypto + .when { $0.generate(.blinded15KeyPair(serverPublicKey: .any, ed25519SecretKey: .any)) } + .thenReturn( + KeyPair( + publicKey: Data(hex: TestConstants.publicKey).bytes, + secretKey: Data(hex: TestConstants.edSecretKey).bytes + ) + ) + try await mockCrypto + .when { $0.generate(.blinded25KeyPair(serverPublicKey: .any, ed25519SecretKey: .any)) } + .thenReturn( + KeyPair( + publicKey: Data(hex: TestConstants.publicKey).bytes, + secretKey: Data(hex: TestConstants.edSecretKey).bytes + ) + ) + try await mockCrypto + .when { $0.generate(.signatureBlind15(message: .any, serverPublicKey: .any, ed25519SecretKey: .any)) } + .thenReturn("TestSogsSignature".bytes) + try await mockCrypto + .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } + .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) + try await mockCrypto + .when { $0.generate(.randomBytes(16)) } + .thenReturn(Array(Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!)) + try await mockCrypto + .when { $0.generate(.randomBytes(24)) } + .thenReturn(Array(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!)) + try await mockCrypto + .when { $0.generate(.ed25519KeyPair(seed: .any)) } + .thenReturn( + KeyPair( + publicKey: Array(Data(hex: TestConstants.edPublicKey)), + secretKey: Array(Data(hex: TestConstants.edSecretKey)) + ) + ) + try await mockCrypto + .when { $0.generate(.ciphertextWithXChaCha20(plaintext: .any, encKey: .any)) } + .thenReturn(Data([1, 2, 3])) } // MARK: - an OpenGroupManager @@ -271,6 +270,9 @@ class OpenGroupManagerSpec: AsyncSpec { .thenReturn(mockPoller) try await mockCommunityPollerManager.when { await $0.stopAndRemovePoller(for: .any) }.thenReturn(()) try await mockCommunityPollerManager.when { await $0.stopAndRemoveAllPollers() }.thenReturn(()) + try await mockCommunityPollerManager + .when { await $0.syncState } + .thenReturn(CommunityPollerManagerSyncState()) _ = userGroupsInitResult } @@ -1979,7 +1981,7 @@ class OpenGroupManagerSpec: AsyncSpec { // MARK: -- when handling direct messages context("when handling direct messages") { beforeEach { - mockCrypto + try await mockCrypto .when { $0.generate( .plaintextWithSessionBlindingProtocol( @@ -1996,7 +1998,7 @@ class OpenGroupManagerSpec: AsyncSpec { Data([UInt8](repeating: 0, count: 32)), senderSessionIdHex: "05\(TestConstants.publicKey)" )) - mockCrypto + try await mockCrypto .when { $0.generate(.x25519(ed25519Pubkey: .any)) } .thenReturn(Data(hex: TestConstants.publicKey).bytes) } @@ -2092,7 +2094,7 @@ class OpenGroupManagerSpec: AsyncSpec { // MARK: ---- for the inbox context("for the inbox") { beforeEach { - mockCrypto + try await mockCrypto .when { $0.verify(.sessionId(.any, matchesBlindedId: .any, serverPublicKey: .any)) } .thenReturn(false) } @@ -2121,7 +2123,7 @@ class OpenGroupManagerSpec: AsyncSpec { // MARK: ------ ignores a message with invalid data it("ignores a message with invalid data") { - mockCrypto + try await mockCrypto .when { $0.generate( .plaintextWithSessionBlindingProtocol( @@ -2194,7 +2196,7 @@ class OpenGroupManagerSpec: AsyncSpec { // MARK: ---- for the outbox context("for the outbox") { beforeEach { - mockCrypto + try await mockCrypto .when { $0.verify(.sessionId(.any, matchesBlindedId: .any, serverPublicKey: .any)) } .thenReturn(false) } @@ -2277,7 +2279,7 @@ class OpenGroupManagerSpec: AsyncSpec { // MARK: ------ ignores a message with invalid data it("ignores a message with invalid data") { - mockCrypto + try await mockCrypto .when { $0.generate( .plaintextWithSessionBlindingProtocol( @@ -2547,9 +2549,9 @@ class OpenGroupManagerSpec: AsyncSpec { ) } - expect(mockCrypto).to(call(.exactly(times: 1), matchingParameters: .all) { - $0.generate(.ed25519KeyPair(seed: [4, 5, 6])) - }) + await mockCrypto + .verify { $0.generate(.ed25519KeyPair(seed: [4, 5, 6])) } + .wasCalled(exactly: 1) } } } diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index 4ff4e39fc5..8adcb0506c 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -27,7 +27,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { context("when receiving a group invitation") { // MARK: ---- throws if the admin signature fails to verify it("throws if the admin signature fails to verify") { - fixture.mockCrypto + try await fixture.mockCrypto .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(false) @@ -789,7 +789,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ---- fails if it cannot convert the group seed to a groupIdentityKeyPair it("fails if it cannot convert the group seed to a groupIdentityKeyPair") { - fixture.mockCrypto.when { $0.generate(.ed25519KeyPair(seed: .any)) }.thenReturn(nil) + try await fixture.mockCrypto.when { $0.generate(.ed25519KeyPair(seed: .any)) }.thenReturn(nil) fixture.mockStorage.write { db in result = Result(catching: { @@ -810,7 +810,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ---- updates the GROUP_KEYS state correctly it("updates the GROUP_KEYS state correctly") { - fixture.mockCrypto + try await fixture.mockCrypto .when { $0.generate(.ed25519KeyPair(seed: .any)) } .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [4, 5, 6])) @@ -924,7 +924,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ---- throws if the admin signature fails to verify it("throws if the admin signature fails to verify") { - fixture.mockCrypto + try await fixture.mockCrypto .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(false) @@ -1109,7 +1109,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ---- throws if the admin signature fails to verify it("throws if the admin signature fails to verify") { - fixture.mockCrypto + try await fixture.mockCrypto .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(false) @@ -2180,7 +2180,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ---- throws if the admin signature fails to verify it("throws if the admin signature fails to verify") { - fixture.mockCrypto + try await fixture.mockCrypto .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(false) @@ -3236,7 +3236,7 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { var mockJobRunner: MockJobRunner { mock(for: .jobRunner) { MockJobRunner() } } var mockAppContext: MockAppContext { mock(for: .appContext) } var mockUserDefaults: MockUserDefaults { mock(for: .standard) { MockUserDefaults() } } - var mockCrypto: MockCrypto { mock(for: .crypto) { MockCrypto() } } + var mockCrypto: MockCrypto { mock(for: .crypto) } var mockKeychain: MockKeychain { mock(for: .keychain) { MockKeychain() } } var mockFileManager: MockFileManager { mock(for: .fileManager) { MockFileManager() } } var mockExtensionHelper: MockExtensionHelper { mock(for: .extensionHelper) { MockExtensionHelper() } } @@ -3523,7 +3523,7 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { await applyBaselineJobRunner() try await applyBaselineAppContext() await applyBaselineUserDefaults() - await applyBaselineCrypto() + try await applyBaselineCrypto() await applyBaselineKeychain() await applyBaselineFileManager() await applyBaselineExtensionHelper() @@ -3608,25 +3608,25 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { mockUserDefaults.when { $0.string(forKey: .any) }.thenReturn(nil) } - private func applyBaselineCrypto() async { - mockCrypto + private func applyBaselineCrypto() async throws { + try await mockCrypto .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) - mockCrypto + try await mockCrypto .when { $0.generate(.signatureSubaccount(config: .any, verificationBytes: .any, memberAuthData: .any)) } .thenReturn(Authentication.Signature.subaccount( subaccount: "TestSubAccount".bytes, subaccountSig: "TestSubAccountSignature".bytes, signature: "TestSignature".bytes )) - mockCrypto + try await mockCrypto .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(true) - mockCrypto.when { $0.generate(.ed25519KeyPair(seed: .any)) }.thenReturn(groupKeyPair) - mockCrypto + try await mockCrypto.when { $0.generate(.ed25519KeyPair(seed: .any)) }.thenReturn(groupKeyPair) + try await mockCrypto .when { $0.verify(.memberAuthData(groupSessionId: .any, ed25519SecretKey: .any, memberAuthData: .any)) } .thenReturn(true) - mockCrypto + try await mockCrypto .when { $0.generate(.hash(message: .any, key: .any, length: .any)) } .thenReturn("TestHash".bytes) } diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift index a34cd8f193..afe0dfd5ec 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift @@ -52,53 +52,7 @@ class MessageSenderGroupsSpec: AsyncSpec { } ) @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork() - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( - initialSetup: { crypto in - crypto - .when { $0.generate(.ed25519KeyPair()) } - .thenReturn( - KeyPair( - publicKey: Data(hex: groupId.hexString).bytes, - secretKey: groupSecretKey.bytes - ) - ) - crypto - .when { $0.generate(.ed25519KeyPair(seed: .any)) } - .thenReturn( - KeyPair( - publicKey: Data(hex: groupId.hexString).bytes, - secretKey: groupSecretKey.bytes - ) - ) - crypto - .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } - .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) - crypto - .when { $0.generate(.memberAuthData(config: .any, groupSessionId: .any, memberId: .any)) } - .thenReturn(Authentication.Info.groupMember( - groupSessionId: SessionId(.standard, hex: TestConstants.publicKey), - authData: "TestAuthData".data(using: .utf8)! - )) - crypto - .when { $0.generate(.tokenSubaccount(config: .any, groupSessionId: .any, memberId: .any)) } - .thenReturn(Array("TestSubAccountToken".data(using: .utf8)!)) - crypto - .when { try $0.tryGenerate(.randomBytes(.any)) } - .thenReturn(Data((0..? beforeEach { - mockCrypto + try await mockCrypto .when { $0.generate(.ciphertextWithSessionProtocol(plaintext: .any, destination: .any)) } .thenReturn(Data([1, 2, 3])) - mockCrypto + try await mockCrypto .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } .thenReturn(Authentication.Signature.standard(signature: [])) } diff --git a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift index 1898e5ea1f..b01c62d60e 100644 --- a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift @@ -101,7 +101,7 @@ private class CommunityPollerManagerTestFixture: FixtureBase { var mockUserDefaults: MockUserDefaults { mock(for: .standard) { MockUserDefaults() } } var mockGeneralCache: MockGeneralCache { mock(cache: .general) { MockGeneralCache() } } var mockOGMCache: MockOGMCache { mock(cache: .openGroupManager) { MockOGMCache() } } - var mockCrypto: MockCrypto { mock(for: .crypto) { MockCrypto() } } + var mockCrypto: MockCrypto { mock(for: .crypto) } lazy var manager: CommunityPollerManager = CommunityPollerManager(using: dependencies) static func create() async throws -> CommunityPollerManagerTestFixture { @@ -120,7 +120,7 @@ private class CommunityPollerManagerTestFixture: FixtureBase { await applyBaselineUserDefaults() await applyBaselineGeneralCache() await applyBaselineOGMCache() - await applyBaselineCrypto() + try await applyBaselineCrypto() } private func applyBaselineStorage() async throws { @@ -199,11 +199,11 @@ private class CommunityPollerManagerTestFixture: FixtureBase { mockOGMCache.when { $0.getLastSuccessfulCommunityPollTimestamp() }.thenReturn(0) } - private func applyBaselineCrypto() async { - mockCrypto + private func applyBaselineCrypto() async throws { + try await mockCrypto .when { $0.generate(.hash(message: .any, key: .any, length: .any)) } .thenReturn([]) - mockCrypto + try await mockCrypto .when { $0.generate(.blinded15KeyPair(serverPublicKey: .any, ed25519SecretKey: .any)) } .thenReturn( KeyPair( @@ -211,13 +211,13 @@ private class CommunityPollerManagerTestFixture: FixtureBase { secretKey: Data(hex: TestConstants.edSecretKey).bytes ) ) - mockCrypto + try await mockCrypto .when { $0.generate(.signatureBlind15(message: .any, serverPublicKey: .any, ed25519SecretKey: .any)) } .thenReturn("TestSogsSignature".bytes) - mockCrypto + try await mockCrypto .when { $0.generate(.randomBytes(16)) } .thenReturn(Array(Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!)) - mockCrypto + try await mockCrypto .when { $0.generate(.ed25519KeyPair(seed: .any)) } .thenReturn( KeyPair( diff --git a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift index 3f12841e27..ab3f16f420 100644 --- a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift +++ b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift @@ -5,6 +5,7 @@ import GRDB import SessionUtil import SessionNetworkingKit import SessionUtilitiesKit +import TestUtilities import Quick import Nimble @@ -26,14 +27,7 @@ class ExtensionHelperSpec: AsyncSpec { customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( - initialSetup: { crypto in - crypto.when { $0.generate(.hash(message: .any)) }.thenReturn([1, 2, 3]) - crypto - .when { $0.generate(.ciphertextWithXChaCha20(plaintext: .any, encKey: .any)) } - .thenReturn(Data([4, 5, 6])) - } - ) + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() @TestState(singleton: .fileManager, in: dependencies) var mockFileManager: MockFileManager! = MockFileManager( initialSetup: { $0.defaultInitialSetup() } ) @@ -65,6 +59,11 @@ class ExtensionHelperSpec: AsyncSpec { dependencies.set(cache: .libSession, to: mockLibSessionCache) try await mockStorage.perform(migrations: SNMessagingKit.migrations) + + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn([1, 2, 3]) + try await mockCrypto + .when { $0.generate(.ciphertextWithXChaCha20(plaintext: .any, encKey: .any)) } + .thenReturn(Data([4, 5, 6])) } // MARK: - an ExtensionHelper - File Management @@ -167,7 +166,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- throws encryption errors it("throws encryption errors") { - mockCrypto + try await mockCrypto .when { try $0.tryGenerate( .ciphertextWithXChaCha20(plaintext: .any, encKey: .any) @@ -272,7 +271,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- loads the data correctly it("loads the data correctly") { mockFileManager.when { $0.contents(atPath: .any) }.thenReturn(Data([1, 2, 3])) - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn( try! JSONEncoder(using: dependencies) @@ -305,7 +304,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- returns null if it fails to decrypt the file it("returns null if it fails to decrypt the file") { mockFileManager.when { $0.contents(atPath: .any) }.thenReturn(Data([1, 2, 3])) - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn(nil) @@ -317,7 +316,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- returns null if it fails to decode the data it("returns null if it fails to decode the data") { mockFileManager.when { $0.contents(atPath: .any) }.thenReturn(Data([1, 2, 3])) - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn(Data([1, 2, 3])) @@ -334,7 +333,7 @@ class ExtensionHelperSpec: AsyncSpec { context("when checking whether it has a dedupe record since the last clear") { // MARK: ---- returns true when at least one record exists that is newer than the last cleared timestamp it("returns true when at least one record exists that is newer than the last cleared timestamp") { - mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn([1, 2, 3]) + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn([1, 2, 3]) mockFileManager.when { try $0.contentsOfDirectory(atPath: .any) }.thenReturn(["Test1234"]) mockFileManager.when { $0.fileExists(atPath: .any) }.thenReturn(true) mockFileManager @@ -349,14 +348,14 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- returns false when it cannot get the conversation path it("returns false when it cannot get the conversation path") { - mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) expect(extensionHelper.hasDedupeRecordSinceLastCleared(threadId: "threadId")).to(beFalse()) } // MARK: ---- returns false when a record does not exist it("returns false when a record does not exist") { - mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn([1, 2, 3]) + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn([1, 2, 3]) mockFileManager.when { try $0.contentsOfDirectory(atPath: .any) }.thenReturn([]) mockFileManager.when { $0.fileExists(atPath: .any) }.thenReturn(true) mockFileManager @@ -368,7 +367,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- returns false when a record exists but is too old it("returns false when a record exists but is too old") { - mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn([1, 2, 3]) + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn([1, 2, 3]) mockFileManager.when { try $0.contentsOfDirectory(atPath: .any) }.thenReturn(["Test1234"]) mockFileManager.when { $0.fileExists(atPath: .any) }.thenReturn(true) mockFileManager @@ -383,7 +382,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- ignores the lastCleared file when comparing dedupe records it("ignores the lastCleared file when comparing dedupe records") { - mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn([1, 2, 3]) + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn([1, 2, 3]) mockFileManager.when { $0.fileExists(atPath: .any) }.thenReturn(true) mockFileManager .when { try $0.contentsOfDirectory(atPath: .any) } @@ -397,7 +396,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- returns true when at least one record exists and there is no last cleared timestamp it("returns true when at least one record exists and there is no last cleared timestamp") { - mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn([1, 2, 3]) + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn([1, 2, 3]) mockFileManager.when { try $0.contentsOfDirectory(atPath: .any) }.thenReturn(["Test1234"]) mockFileManager.when { $0.fileExists(atPath: .any) }.thenReturn(true) mockFileManager @@ -435,7 +434,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- returns false when failing to generate a hash it("returns false when failing to generate a hash") { - mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) expect(extensionHelper.dedupeRecordExists( threadId: "threadId", @@ -463,7 +462,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- throws when failing to generate a hash it("throws when failing to generate a hash") { - mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) expect { try extensionHelper.createDedupeRecord( @@ -530,7 +529,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- does nothing when failing to generate a hash it("does nothing when failing to generate a hash") { - mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) expect { try extensionHelper.removeDedupeRecord( @@ -560,7 +559,7 @@ class ExtensionHelperSpec: AsyncSpec { context("when upserting a last cleared record") { // MARK: ---- creates the file successfully it("creates the file successfully") { - mockCrypto + try await mockCrypto .when { $0.generate(.ciphertextWithXChaCha20(plaintext: .any, encKey: .any)) } .thenReturn(Data()) @@ -581,7 +580,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- throws when failing to generate a hash it("throws when failing to generate a hash") { - mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) expect { try extensionHelper.upsertLastClearedRecord(threadId: "threadId") @@ -629,7 +628,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- returns zero when it fails to generate a hash it("returns zero when it fails to generate a hash") { - mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) expect(extensionHelper.lastUpdatedTimestamp( for: SessionId(.standard, hex: TestConstants.publicKey), @@ -682,7 +681,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- does nothing when failing to generate a hash it("does nothing when failing to generate a hash") { - mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) extensionHelper.replicate( dump: ConfigDump( @@ -801,14 +800,14 @@ class ExtensionHelperSpec: AsyncSpec { mockFileManager .when { try $0.attributesOfItem(atPath: .any) } .thenReturn([.modificationDate: Date(timeIntervalSince1970: 1234567800)]) - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn(Data([1, 2, 3])) - mockValues.forEach { value in - mockCrypto + for value in mockValues { + try await mockCrypto .when { $0.generate(.hash(message: Array(value.key.data(using: .utf8)!))) } .thenReturn(value.hashValue) - mockCrypto + try await mockCrypto .when { $0.generate(.ciphertextWithXChaCha20(plaintext: value.plaintext, encKey: .any)) } .thenReturn(value.ciphertext) } @@ -953,8 +952,8 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- does nothing when failing to generate a hash it("does nothing when failing to generate a hash") { - mockValues.forEach { value in - mockCrypto + for value in mockValues { + try await mockCrypto .when { $0.generate(.hash(message: Array(value.key.data(using: .utf8)!))) } .thenReturn(nil) } @@ -971,7 +970,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- does nothing if valid dumps already exist it("does nothing if valid dumps already exist") { mockFileManager.when { $0.contents(atPath: .any) }.thenReturn(Data([1, 2, 3])) - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn(Data([1, 2, 3])) @@ -1016,10 +1015,10 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- does nothing if it fails to replicate it("does nothing if it fails to replicate") { - mockCrypto + try await mockCrypto .when { $0.generate(.ciphertextWithXChaCha20(plaintext: Data([2, 3, 4]), encKey: .any)) } .thenThrow(TestError.mock) - mockCrypto + try await mockCrypto .when { $0.generate(.ciphertextWithXChaCha20(plaintext: Data([5, 6, 7]), encKey: .any)) } .thenThrow(TestError.mock) @@ -1056,7 +1055,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- does nothing when it fails to generate a hash it("does nothing when it fails to generate a hash") { - mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) extensionHelper.refreshDumpModifiedDate( sessionId: SessionId(.standard, hex: "05\(TestConstants.publicKey)"), @@ -1082,7 +1081,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: -- when loading user configs context("when loading user configs") { beforeEach { - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn(Data([1, 2, 3])) } @@ -1201,7 +1200,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: -- when loading group configs context("when loading group configs") { beforeEach { - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn(Data([1, 2, 3])) } @@ -1268,8 +1267,8 @@ class ExtensionHelperSpec: AsyncSpec { .groupKeys(keysPtr, info: ptr, members: ptr), .groupInfo(ptr) ] - mockCrypto.removeMocksFor { $0.generate(.hash(message: .any)) } - configs.forEach { config in + await mockCrypto.removeMocksFor { $0.generate(.hash(message: .any)) } + for config in configs { mockLibSessionCache .when { try $0.loadState( @@ -1281,14 +1280,14 @@ class ExtensionHelperSpec: AsyncSpec { ) } .thenReturn(config) - mockCrypto + try await mockCrypto .when { $0.generate(.hash(message: Array("DumpSalt-\(config.variant)".utf8))) } .thenReturn([0, 1, 2]) } - mockCrypto + try await mockCrypto .when { $0.generate(.hash(message: Array("ConvoIdSalt-03\(TestConstants.publicKey)".utf8))) } .thenReturn([4, 5, 6]) - mockCrypto + try await mockCrypto .when { $0.generate(.hash(message: Array("DumpSalt-\(ConfigDump.Variant.groupMembers)".utf8))) } @@ -1359,10 +1358,10 @@ class ExtensionHelperSpec: AsyncSpec { mockFileManager .when { try $0.attributesOfItem(atPath: .any) } .thenReturn([.modificationDate: Date(timeIntervalSince1970: 1234567800)]) - mockCrypto + try await mockCrypto .when { $0.generate(.hash(message: .any)) } .thenReturn([0, 1, 2]) - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn(Data([1, 2, 3])) } @@ -1418,15 +1417,17 @@ class ExtensionHelperSpec: AsyncSpec { ], replaceExisting: true ) - expect(mockCrypto).to(call(.exactly(times: 1), matchingParameters: .all) { - $0.generate( - .ciphertextWithXChaCha20( - plaintext: try JSONEncoder(using: dependencies) - .encode(expectedResult), - encKey: [1, 2, 3] + await mockCrypto + .verify { + $0.generate( + .ciphertextWithXChaCha20( + plaintext: try JSONEncoder(using: dependencies) + .encode(expectedResult), + encKey: [1, 2, 3] + ) ) - ) - }) + } + .wasCalled(exactly: 1) } // MARK: ---- does nothing if the settings already exist and we do not want to replace existing @@ -1451,10 +1452,10 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- does nothing if it fails to replicate it("does nothing if it fails to replicate") { - mockCrypto + try await mockCrypto .when { $0.generate(.ciphertextWithXChaCha20(plaintext: Data([2, 3, 4]), encKey: .any)) } .thenThrow(TestError.mock) - mockCrypto + try await mockCrypto .when { $0.generate(.ciphertextWithXChaCha20(plaintext: Data([5, 6, 7]), encKey: .any)) } .thenThrow(TestError.mock) @@ -1480,7 +1481,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- loads the data correctly it("loads the data correctly") { mockFileManager.when { $0.contents(atPath: .any) }.thenReturn(Data([1, 2, 3])) - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn( try! JSONEncoder(using: dependencies) @@ -1533,7 +1534,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- returns null if it fails to decrypt the file it("returns null if it fails to decrypt the file") { mockFileManager.when { $0.contents(atPath: .any) }.thenReturn(Data([1, 2, 3])) - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn(nil) @@ -1548,7 +1549,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- returns null if it fails to decode the data it("returns null if it fails to decode the data") { mockFileManager.when { $0.contents(atPath: .any) }.thenReturn(Data([1, 2, 3])) - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn(Data([1, 2, 3])) @@ -1582,7 +1583,7 @@ class ExtensionHelperSpec: AsyncSpec { ) } .thenReturn(["b", "c", "d", "e", "f"]) - mockCrypto + try await mockCrypto .when { $0.generate(.hash(message: Array("a".utf8) + Array("messageRequest".utf8))) } .thenReturn([3, 4, 5]) let validPaths: [String] = [ @@ -1668,7 +1669,7 @@ class ExtensionHelperSpec: AsyncSpec { ) } .thenReturn(["b1", "b1-legacy", "c1", "c1-legacy", "d1", "d1-legacy"]) - mockCrypto + try await mockCrypto .when { $0.generate(.hash(message: Array("a".utf8) + Array("messageRequest".utf8))) } .thenReturn([3, 4, 5]) mockFileManager.when { $0.fileExists(atPath: .any) }.thenReturn(true) @@ -1807,7 +1808,7 @@ class ExtensionHelperSpec: AsyncSpec { ) } .thenReturn(["b", "c", "d", "e", "f"]) - mockCrypto + try await mockCrypto .when { $0.generate(.hash(message: Array("a".utf8) + Array("messageRequest".utf8))) } .thenReturn([3, 4, 5]) mockFileManager.when { $0.fileExists(atPath: .any) }.thenReturn(true) @@ -1943,14 +1944,14 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- writes the message request stub file for unread message request messages it("writes the message request stub file for unread message request messages") { - mockCrypto.removeMocksFor { $0.generate(.hash(message: .any)) } - mockCrypto + await mockCrypto.removeMocksFor { $0.generate(.hash(message: .any)) } + try await mockCrypto .when { $0.generate(.hash(message: Array("ConvoIdSalt-05\(TestConstants.publicKey)".utf8))) } .thenReturn([1, 2, 3]) - mockCrypto + try await mockCrypto .when { $0.generate(.hash(message: Array("UnreadMessageSalt-TestHash".utf8))) } .thenReturn([2, 3, 4]) - mockCrypto + try await mockCrypto .when { $0.generate(.hash(message: [1, 2, 3] + Array("messageRequest".utf8))) } .thenReturn([3, 4, 5]) @@ -2014,7 +2015,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- does nothing when failing to generate a hash it("does nothing when failing to generate a hash") { - mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) + try await mockCrypto.when { $0.generate(.hash(message: .any)) }.thenReturn(nil) expect { try extensionHelper.saveMessage( @@ -2123,7 +2124,7 @@ class ExtensionHelperSpec: AsyncSpec { .when { try $0.contentsOfDirectory(atPath: key) } .thenReturn([value]) } - mockCrypto + try await mockCrypto .when { $0.generate(.hash( message: Array("ConvoIdSalt-05\(TestConstants.publicKey)".data(using: .utf8)!) @@ -2131,7 +2132,7 @@ class ExtensionHelperSpec: AsyncSpec { } .thenReturn([1, 2, 3]) mockFileManager.when { $0.contents(atPath: .any) }.thenReturn(Data([1, 2, 3])) - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn( try! JSONEncoder(using: dependencies) @@ -2158,7 +2159,7 @@ class ExtensionHelperSpec: AsyncSpec { let dataMessage = SNProtoDataMessage.builder() dataMessage.setBody("Test") content.setDataMessage(try! dataMessage.build()) - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithSessionProtocol(ciphertext: .any)) } .thenReturn((try! content.build().serializedData(), "05\(TestConstants.publicKey)")) } @@ -2174,7 +2175,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- always tries to load messages from the current users conversation it("always tries to load messages from the current users conversation") { - mockCrypto + try await mockCrypto .when { $0.generate(.hash( message: Array("ConvoIdSalt-05\(TestConstants.publicKey)".data(using: .utf8)!) @@ -2283,7 +2284,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- removes messages from disk it("removes messages from disk") { mockFileManager.when { try $0.contentsOfDirectory(atPath: .any) }.thenReturn([]) - mockCrypto + try await mockCrypto .when { $0.generate(.hash( message: Array("ConvoIdSalt-05\(TestConstants.publicKey)".data(using: .utf8)!) @@ -2357,7 +2358,7 @@ class ExtensionHelperSpec: AsyncSpec { mockFileManager .when { try $0.contentsOfDirectory(atPath: "/test/extensionCache/conversations/a/config") } .thenReturn(["b"]) - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn(nil) @@ -2410,7 +2411,7 @@ class ExtensionHelperSpec: AsyncSpec { mockFileManager .when { try $0.contentsOfDirectory(atPath: "/test/extensionCache/conversations/a/read") } .thenReturn(["c"]) - mockCrypto + try await mockCrypto .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn(nil) @@ -2464,7 +2465,7 @@ class ExtensionHelperSpec: AsyncSpec { .when { try $0.contentsOfDirectory(atPath: "/test/extensionCache/conversations/a/read") } .thenReturn(["c"]) mockFileManager.when { try $0.removeItem(atPath: .any) }.thenThrow(TestError.mock) - mockCrypto + try await mockCrypto .when { $0.generate(.hash( message: Array("ConvoIdSalt-05\(TestConstants.publicKey)".data(using: .utf8)!) diff --git a/SessionMessagingKitTests/_TestUtilities/Mocked+SMK.swift b/SessionMessagingKitTests/_TestUtilities/Mocked+SMK.swift index 3df3b53ee9..d3a168d949 100644 --- a/SessionMessagingKitTests/_TestUtilities/Mocked+SMK.swift +++ b/SessionMessagingKitTests/_TestUtilities/Mocked+SMK.swift @@ -245,3 +245,13 @@ extension PollerDestination: @retroactive Mocked { public static var any: PollerDestination { .swarm(.any) } public static var mock: PollerDestination { .swarm(TestConstants.publicKey) } } + +extension CommunityPollerManagerSyncState: @retroactive Mocked { + public static var any: CommunityPollerManagerSyncState = CommunityPollerManagerSyncState( + serversBeingPolled: .any + ) + + public static var mock: CommunityPollerManagerSyncState = CommunityPollerManagerSyncState( + serversBeingPolled: .mock + ) +} diff --git a/SessionNetworkingKit/Types/Network.swift b/SessionNetworkingKit/Types/Network.swift index 2f01ab586f..5153b22fef 100644 --- a/SessionNetworkingKit/Types/Network.swift +++ b/SessionNetworkingKit/Types/Network.swift @@ -69,7 +69,11 @@ public extension NetworkType { /// We manually handle thread-safety using the `NSLock` so can ensure this is `Sendable` public final class NetworkSyncState: @unchecked Sendable { private let lock = NSLock() - private var _isSuspended: Bool = false + private var _isSuspended: Bool + + public init(isSuspended: Bool = false) { + self._isSuspended = isSuspended + } public var isSuspended: Bool { lock.withLock { _isSuspended } } diff --git a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift index fdf80044f5..07c479740f 100644 --- a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift @@ -46,13 +46,7 @@ class ThreadSettingsViewModelSpec: AsyncSpec { } ) @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( - initialSetup: { crypto in - crypto - .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } - .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) - } - ) + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() @TestState var mockSnodeAPICache: MockSnodeAPICache! = MockSnodeAPICache() @TestState var threadVariant: SessionThread.Variant! = .contact @TestState var didTriggerSearchCallbackTriggered: Bool! = false @@ -118,6 +112,10 @@ class ThreadSettingsViewModelSpec: AsyncSpec { try Profile(id: userPubkey, name: "TestMe").insert(db) try Profile(id: user2Pubkey, name: "TestUser").insert(db) } + + try await mockCrypto + .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } + .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) } // MARK: - a ThreadSettingsViewModel diff --git a/SessionTests/Database/DatabaseSpec.swift b/SessionTests/Database/DatabaseSpec.swift index 47cd2e3240..304eadfbbe 100644 --- a/SessionTests/Database/DatabaseSpec.swift +++ b/SessionTests/Database/DatabaseSpec.swift @@ -70,21 +70,15 @@ class DatabaseSpec: AsyncSpec { // MARK: -- can be created from an empty state it("can be created from an empty state") { expect { - try await mockStorage.perform( - migrations: allMigrations, - onProgressUpdate: nil - ) + try await mockStorage.perform(migrations: allMigrations) }.toNot(throwError()) } // MARK: -- can still parse the database table types it("can still parse the database table types") { - expect { - try await mockStorage.perform( - migrations: allMigrations, - onProgressUpdate: nil - ) - }.toNot(throwError()) + await expect { + try await mockStorage.perform(migrations: allMigrations) + }.toEventuallyNot(throwError()) // Generate dummy data (fetching below won't do anything) expect(try MigrationTest.generateDummyData(mockStorage, nullsWherePossible: false)) @@ -101,12 +95,9 @@ class DatabaseSpec: AsyncSpec { // MARK: -- can still parse the database types setting null where possible it("can still parse the database types setting null where possible") { - expect { - try await mockStorage.perform( - migrations: allMigrations, - onProgressUpdate: nil - ) - }.toNot(throwError()) + await expect { + try await mockStorage.perform(migrations: allMigrations) + }.toEventuallyNot(throwError()) // Generate dummy data (fetching below won't do anything) expect(try MigrationTest.generateDummyData(mockStorage, nullsWherePossible: true)) @@ -135,12 +126,7 @@ class DatabaseSpec: AsyncSpec { customWriter: dbQueue, using: dependencies ) - - // Generate dummy data (otherwise structural issues or invalid foreign keys won't error) - try await mockStorage.perform( - migrations: test.initialMigrations, - onProgressUpdate: nil - ) + try await storage.perform(migrations: test.initialMigrations) // Generate dummy data (otherwise structural issues or invalid foreign keys won't error) try MigrationTest.generateDummyData(storage, nullsWherePossible: false) diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index 15c26ea4c8..aa0f921f7f 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -27,38 +27,7 @@ class OnboardingSpec: AsyncSpec { customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( - initialSetup: { crypto in - crypto - .when { $0.generate(.x25519(ed25519Pubkey: .any)) } - .thenReturn(Array(Data(hex: TestConstants.publicKey))) - crypto - .when { $0.generate(.x25519(ed25519Seckey: .any)) } - .thenReturn(Array(Data(hex: TestConstants.privateKey))) - crypto - .when { $0.generate(.randomBytes(.any)) } - .thenReturn(Data([1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8])) - crypto - .when { $0.generate(.ed25519Seed(ed25519SecretKey: .any)) } - .thenReturn(Data([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, - 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, - 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, - 1, 2 - ])) - crypto - .when { $0.generate(.ed25519KeyPair(seed: .any)) } - .thenReturn( - KeyPair( - publicKey: Array(Data(hex: TestConstants.edPublicKey)), - secretKey: Array(Data(hex: TestConstants.edSecretKey)) - ) - ) - crypto - .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } - .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) - } - ) + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() @TestState var mockLibSession: MockLibSessionCache! = MockLibSessionCache() @TestState(defaults: .standard, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults( @@ -179,6 +148,35 @@ class OnboardingSpec: AsyncSpec { dependencies.set(cache: .snodeAPI, to: mockSnodeAPICache) try await mockStorage.perform(migrations: SNMessagingKit.migrations) + + try await mockCrypto + .when { $0.generate(.x25519(ed25519Pubkey: .any)) } + .thenReturn(Array(Data(hex: TestConstants.publicKey))) + try await mockCrypto + .when { $0.generate(.x25519(ed25519Seckey: .any)) } + .thenReturn(Array(Data(hex: TestConstants.privateKey))) + try await mockCrypto + .when { $0.generate(.randomBytes(.any)) } + .thenReturn(Data([1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8])) + try await mockCrypto + .when { $0.generate(.ed25519Seed(ed25519SecretKey: .any)) } + .thenReturn(Data([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2 + ])) + try await mockCrypto + .when { $0.generate(.ed25519KeyPair(seed: .any)) } + .thenReturn( + KeyPair( + publicKey: Array(Data(hex: TestConstants.edPublicKey)), + secretKey: Array(Data(hex: TestConstants.edSecretKey)) + ) + ) + try await mockCrypto + .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } + .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) } // MARK: - an Onboarding Cache - Initialization @@ -211,13 +209,13 @@ class OnboardingSpec: AsyncSpec { context("without a stored secret key") { beforeEach { mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) - mockCrypto + try await mockCrypto .when { $0.generate(.ed25519KeyPair(seed: .any)) } .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [4, 5, 6])) - mockCrypto + try await mockCrypto .when { $0.generate(.x25519(ed25519Pubkey: .any)) } .thenReturn([3, 2, 1]) - mockCrypto + try await mockCrypto .when { $0.generate(.x25519(ed25519Seckey: .any)) } .thenReturn([6, 5, 4]) } @@ -238,7 +236,7 @@ class OnboardingSpec: AsyncSpec { mockGeneralCache .when { $0.ed25519SecretKey } .thenReturn(Array(Data(hex: TestConstants.edSecretKey))) - mockCrypto + try await mockCrypto .when { $0.generate(.ed25519KeyPair(seed: .any)) } .thenReturn( KeyPair( @@ -263,12 +261,12 @@ class OnboardingSpec: AsyncSpec { // MARK: ---- generates the x25519KeyPair from the loaded ed25519 key pair it("generates the x25519KeyPair from the loaded ed25519 key pair") { - expect(mockCrypto).to(call(.exactly(times: 1), matchingParameters: .all) { - $0.generate(.x25519(ed25519Pubkey: Array(Data(hex: TestConstants.edPublicKey)))) - }) - expect(mockCrypto).to(call(.exactly(times: 1), matchingParameters: .all) { - $0.generate(.x25519(ed25519Seckey: Array(Data(hex: TestConstants.edSecretKey)))) - }) + await mockCrypto + .verify { $0.generate(.x25519(ed25519Pubkey: Array(Data(hex: TestConstants.edPublicKey)))) } + .wasCalled(exactly: 1) + await mockCrypto + .verify { $0.generate(.x25519(ed25519Seckey: Array(Data(hex: TestConstants.edSecretKey)))) } + .wasCalled(exactly: 1) await expect { await manager.x25519KeyPair.publicKey.toHexString() } .to(equal(TestConstants.publicKey)) @@ -285,31 +283,31 @@ class OnboardingSpec: AsyncSpec { // MARK: ---- and failing to generate an x25519KeyPair context("and failing to generate an x25519KeyPair") { beforeEach { - mockCrypto.removeMocksFor { $0.generate(.ed25519KeyPair(seed: .any)) } - mockCrypto.removeMocksFor { $0.generate(.ed25519Seed(ed25519SecretKey: .any)) } - mockCrypto + await mockCrypto.removeMocksFor { $0.generate(.ed25519KeyPair(seed: .any)) } + await mockCrypto.removeMocksFor { $0.generate(.ed25519Seed(ed25519SecretKey: .any)) } + try await mockCrypto .when { try $0.tryGenerate(.ed25519Seed(ed25519SecretKey: .any)) } .thenThrow(MockError.mock) - mockCrypto + try await mockCrypto .when { $0.generate(.ed25519KeyPair( seed: Array(Data(hex: TestConstants.edSecretKey)) )) } .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [9, 8, 7])) - mockCrypto + try await mockCrypto .when { $0.generate(.ed25519KeyPair( seed: [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8] )) } .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [4, 5, 6])) - mockCrypto + try await mockCrypto .when { $0.generate(.x25519(ed25519Pubkey: [1, 2, 3])) } .thenReturn([4, 3, 2, 1]) - mockCrypto + try await mockCrypto .when { $0.generate(.x25519(ed25519Seckey: [4, 5, 6])) } .thenReturn([7, 6, 5, 4]) - mockCrypto + try await mockCrypto .when { $0.generate(.x25519(ed25519Pubkey: [9, 8, 7])) } .thenReturn(nil) } @@ -328,7 +326,7 @@ class OnboardingSpec: AsyncSpec { // MARK: ------ goes into an invalid state when generating a seed fails it("goes into an invalid state when generating a seed fails") { - mockCrypto.when { $0.generate(.randomBytes(.any)) }.thenReturn(nil as Data?) + try await mockCrypto.when { $0.generate(.randomBytes(.any)) }.thenReturn(nil as Data?) manager = Onboarding.Manager( flow: .restore, using: dependencies @@ -386,31 +384,31 @@ class OnboardingSpec: AsyncSpec { // MARK: ------ after generating new credentials context("after generating new credentials") { beforeEach { - mockCrypto.removeMocksFor { $0.generate(.ed25519KeyPair(seed: .any)) } - mockCrypto.removeMocksFor { $0.generate(.ed25519Seed(ed25519SecretKey: .any)) } - mockCrypto + await mockCrypto.removeMocksFor { $0.generate(.ed25519KeyPair(seed: .any)) } + await mockCrypto.removeMocksFor { $0.generate(.ed25519Seed(ed25519SecretKey: .any)) } + try await mockCrypto .when { try $0.tryGenerate(.ed25519Seed(ed25519SecretKey: .any)) } .thenThrow(MockError.mock) - mockCrypto + try await mockCrypto .when { $0.generate(.ed25519KeyPair( seed: Array(Data(hex: TestConstants.edSecretKey)) )) } .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [9, 8, 7])) - mockCrypto + try await mockCrypto .when { $0.generate(.ed25519KeyPair( seed: [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8] )) } .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [4, 5, 6])) - mockCrypto + try await mockCrypto .when { $0.generate(.x25519(ed25519Pubkey: [1, 2, 3])) } .thenReturn([4, 3, 2, 1]) - mockCrypto + try await mockCrypto .when { $0.generate(.x25519(ed25519Seckey: [4, 5, 6])) } .thenReturn([7, 6, 5, 4]) - mockCrypto + try await mockCrypto .when { $0.generate(.x25519(ed25519Pubkey: [9, 8, 7])) } .thenReturn(nil) } @@ -447,7 +445,7 @@ class OnboardingSpec: AsyncSpec { // MARK: - an Onboarding Cache - Seed Data describe("an Onboarding Cache when setting seed data") { beforeEach { - mockCrypto + try await mockCrypto .when { $0.generate(.ed25519KeyPair(seed: .any)) } .thenReturn( KeyPair( diff --git a/SessionUtilitiesKitTests/General/GeneralCacheSpec.swift b/SessionUtilitiesKitTests/General/GeneralCacheSpec.swift index 4eaa6a0480..c09bbcc568 100644 --- a/SessionUtilitiesKitTests/General/GeneralCacheSpec.swift +++ b/SessionUtilitiesKitTests/General/GeneralCacheSpec.swift @@ -7,26 +7,26 @@ import Nimble @testable import SessionUtilitiesKit -class GeneralCacheSpec: QuickSpec { +class GeneralCacheSpec: AsyncSpec { override class func spec() { // MARK: Configuration @TestState var dependencies: TestDependencies! = TestDependencies() - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( - initialSetup: { crypto in - crypto - .when { $0.generate(.ed25519KeyPair(seed: .any)) } - .thenReturn( - KeyPair( - publicKey: Array(Data(hex: TestConstants.edPublicKey)), - secretKey: Array(Data(hex: TestConstants.edSecretKey)) - ) + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() + + beforeEach { + try await mockCrypto + .when { $0.generate(.ed25519KeyPair(seed: .any)) } + .thenReturn( + KeyPair( + publicKey: Array(Data(hex: TestConstants.edPublicKey)), + secretKey: Array(Data(hex: TestConstants.edSecretKey)) ) - crypto - .when { $0.generate(.x25519(ed25519Pubkey: .any)) } - .thenReturn(Array(Data(hex: TestConstants.publicKey))) - } - ) + ) + try await mockCrypto + .when { $0.generate(.x25519(ed25519Pubkey: .any)) } + .thenReturn(Array(Data(hex: TestConstants.publicKey))) + } // MARK: - a General Cache describe("a General Cache") { @@ -57,7 +57,7 @@ class GeneralCacheSpec: QuickSpec { // MARK: -- remains invalid when given a seckey that is too short it("remains invalid when given a seckey that is too short") { - mockCrypto.when { $0.generate(.ed25519KeyPair(seed: .any)) }.thenReturn(nil) + try await mockCrypto.when { $0.generate(.ed25519KeyPair(seed: .any)) }.thenReturn(nil) let cache: General.Cache = General.Cache(using: dependencies) cache.setSecretKey(ed25519SecretKey: [1, 2, 3]) @@ -68,7 +68,7 @@ class GeneralCacheSpec: QuickSpec { // MARK: -- remains invalid when ed key pair generation fails it("remains invalid when ed key pair generation fails") { - mockCrypto.when { $0.generate(.ed25519KeyPair(seed: .any)) }.thenReturn(nil) + try await mockCrypto.when { $0.generate(.ed25519KeyPair(seed: .any)) }.thenReturn(nil) let cache: General.Cache = General.Cache(using: dependencies) cache.setSecretKey(ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey))) @@ -79,7 +79,7 @@ class GeneralCacheSpec: QuickSpec { // MARK: -- remains invalid when x25519 pubkey generation fails it("remains invalid when x25519 pubkey generation fails") { - mockCrypto.when { $0.generate(.x25519(ed25519Pubkey: .any)) }.thenReturn(nil) + try await mockCrypto.when { $0.generate(.x25519(ed25519Pubkey: .any)) }.thenReturn(nil) let cache: General.Cache = General.Cache(using: dependencies) cache.setSecretKey(ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey))) diff --git a/TestUtilities/MockHandler.swift b/TestUtilities/MockHandler.swift index ad58c8f733..7393b81ebf 100644 --- a/TestUtilities/MockHandler.swift +++ b/TestUtilities/MockHandler.swift @@ -43,10 +43,27 @@ public final class MockHandler { } internal func register(stub: MockFunction) { - lock.lock() - defer { lock.unlock() } let key: Key = Key(name: stub.name, generics: stub.generics, paramCount: stub.arguments.count) - stubs[key, default: []].append(stub) + + locked { + stubs[key, default: []].append(stub) + } + } + + internal func removeStubs(for functionBlock: @escaping (T) async throws -> R) async { + let builder: MockFunctionBuilder = createBuilder(for: functionBlock) + + guard let builtFunction: MockFunction = try? await builder.build() else { return } + + let key: Key = Key( + name: builtFunction.name, + generics: builtFunction.generics, + paramCount: builtFunction.arguments.count + ) + + locked { + stubs.removeValue(forKey: key) + } } // MARK: - Verification @@ -64,7 +81,7 @@ public final class MockHandler { paramCount: builtFunction.arguments.count ) - guard let callsForKey: [RecordedCall] = calls[key] else { return [] } + guard let callsForKey: [RecordedCall] = locked({ calls[key] }) else { return [] } return callsForKey.filter { builtFunction.matches(args: $0.args) } } @@ -82,18 +99,28 @@ public final class MockHandler { paramCount: builtFunction.arguments.count ) - return calls[key] + return locked { + calls[key] + } } // MARK: - Test Lifecycle public func reset() { - stubs.removeAll() - calls.removeAll() + locked { + stubs.removeAll() + calls.removeAll() + } } // MARK: - Internal Logic + @discardableResult private func locked(_ operation: () -> R) -> R { + lock.lock() + defer { lock.unlock() } + return operation() + } + private func findAndExecute( funcName: String, generics: [Any.Type], @@ -102,14 +129,15 @@ public final class MockHandler { file: String, line: UInt ) -> Result { - lock.lock() let key: Key = Key(name: funcName, generics: generics, paramCount: args.count) let recordedCall: RecordedCall = RecordedCall(name: funcName, args: args) - calls[key, default: []].append(recordedCall) /// Get the `last` value as it was the one called most recently - let maybeMatchingCall: MockFunction? = stubs[key]?.last(where: { $0.matches(args: args) }) - lock.unlock() + let maybeMatchingCall: MockFunction? = locked { + calls[key, default: []].append(recordedCall) + + return stubs[key]?.last(where: { $0.matches(args: args) }) + } guard let matchingCall: MockFunction = maybeMatchingCall else { return .failure(MockError.noStubFound(function: funcName, args: args)) diff --git a/TestUtilities/Mockable.swift b/TestUtilities/Mockable.swift index 823b8dc32d..4534e16b3f 100644 --- a/TestUtilities/Mockable.swift +++ b/TestUtilities/Mockable.swift @@ -25,4 +25,8 @@ public extension Mockable { func when(_ callBlock: @escaping (MockedType) async throws -> R) -> MockFunctionBuilder { return handler.createBuilder(for: callBlock) } + + func removeMocksFor(_ callBlock: @escaping (MockedType) async throws -> R) async { + await handler.removeStubs(for: callBlock) + } } diff --git a/_SharedTestUtilities/MockCrypto.swift b/_SharedTestUtilities/MockCrypto.swift index 458b5c164f..16a75762a7 100644 --- a/_SharedTestUtilities/MockCrypto.swift +++ b/_SharedTestUtilities/MockCrypto.swift @@ -2,13 +2,24 @@ import Foundation import SessionUtilitiesKit +import TestUtilities -class MockCrypto: Mock, CryptoType { +class MockCrypto: CryptoType, Mockable { + nonisolated let handler: MockHandler + + required init(handler: MockHandler) { + self.handler = handler + } + + required init(handlerForBuilder: any MockFunctionHandler) { + self.handler = MockHandler(forwardingHandler: handlerForBuilder) + } + func tryGenerate(_ generator: Crypto.Generator) throws -> R { - return try mockThrowing(funcName: "generate<\(R.self)>(\(generator.id))", args: generator.args) + return try handler.mockThrowing(funcName: "generate<\(R.self)>(\(generator.id))", args: generator.args) } func verify(_ verification: Crypto.Verification) -> Bool { - return mock(funcName: "verify(\(verification.id))", args: verification.args) + return handler.mock(funcName: "verify(\(verification.id))", args: verification.args) } } diff --git a/_SharedTestUtilities/MockGeneralCache.swift b/_SharedTestUtilities/MockGeneralCache.swift index 413dba0fb8..11dc5867f7 100644 --- a/_SharedTestUtilities/MockGeneralCache.swift +++ b/_SharedTestUtilities/MockGeneralCache.swift @@ -46,6 +46,7 @@ extension Mock where T == GeneralCacheType { self.when { $0.userExists }.thenReturn(true) self.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) self.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) + self.when { $0.setSecretKey(ed25519SecretKey: .any) }.thenReturn(()) self .when { $0.ed25519Seed } .thenReturn(Array(Array(Data(hex: TestConstants.edSecretKey)).prefix(upTo: 32))) diff --git a/_SharedTestUtilities/SynchronousStorage.swift b/_SharedTestUtilities/SynchronousStorage.swift index 63c6b0a5ac..b8c8a55248 100644 --- a/_SharedTestUtilities/SynchronousStorage.swift +++ b/_SharedTestUtilities/SynchronousStorage.swift @@ -6,16 +6,13 @@ import TestUtilities @testable import SessionUtilitiesKit -class SynchronousStorage: Storage, DependenciesSettable { - public var _dependencies: Dependencies! - public var dependencies: Dependencies { _dependencies } +class SynchronousStorage: Storage { + public let dependencies: Dependencies - // MARK: - DependenciesSettable - - func setDependencies(_ dependencies: Dependencies?) { - guard let dependencies: Dependencies = dependencies else { return } + public override init(customWriter: DatabaseWriter? = nil, using dependencies: Dependencies) { + self.dependencies = dependencies - self._dependencies = dependencies + super.init(customWriter: customWriter, using: dependencies) } // MARK: - Overwritten Functions diff --git a/_SharedTestUtilities/TestDependencies.swift b/_SharedTestUtilities/TestDependencies.swift index 0b4f580966..8dd9ef8965 100644 --- a/_SharedTestUtilities/TestDependencies.swift +++ b/_SharedTestUtilities/TestDependencies.swift @@ -223,18 +223,22 @@ public class TestDependencies: Dependencies { } public override func set(singleton: SingletonConfig, to instance: S) { + (instance as? DependenciesSettable)?.setDependencies(self) _singletonInstances.performUpdate { $0.setting(singleton.identifier, instance) } } public override func set(cache: CacheConfig, to instance: M) { + (instance as? DependenciesSettable)?.setDependencies(self) _cacheInstances.performUpdate { $0.setting(cache.identifier, cache.mutableInstance(instance)) } } public func set(defaults: UserDefaultsConfig, to instance: T) { + (instance as? DependenciesSettable)?.setDependencies(self) _defaultsInstances.performUpdate { $0.setting(defaults.identifier, instance) } } public func set(feature: FeatureConfig, to instance: Feature) { + (instance as? DependenciesSettable)?.setDependencies(self) _featureInstances.performUpdate { $0.setting(feature.identifier, instance) } } From 6606ffab5eca35b07f876a537b30e7a8a89f5ab4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 9 Sep 2025 14:29:55 +1000 Subject: [PATCH 39/59] Added dev setting to configure router, fixed a couple of other issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added the ability to select the "Router" which should be used for networking via the dev settings • Updated to the latest CocoaLumberjack • Updated console log to include source file and line • Fixed an issue where the RetrieveDefaultOpenGroupRoomsJob would just retry endlessly (also removed the auto-retry as it's probably not worth it) --- Session.xcodeproj/project.pbxproj | 20 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- Session/Home/HomeVC.swift | 1 + Session/Home/HomeViewModel.swift | 1 + .../Settings.bundle/ThirdPartyLicenses.plist | 2 +- .../DeveloperNetworkSettingsViewModel.swift | 332 ++++++++++++------ .../DeveloperSettingsViewModel+Testing.swift | 63 +++- .../DeveloperSettingsViewModel.swift | 1 + Session/Settings/SettingsViewModel.swift | 1 + .../RetrieveDefaultOpenGroupRoomsJob.swift | 222 ++++++------ .../Configuration/Router.swift | 43 +++ .../Configuration/ServiceNetwork.swift | 3 +- .../LibSession/LibSession+Networking.swift | 8 +- .../Modals & Toast/ConfirmationModal.swift | 5 + SessionUIKit/Components/RadioButton.swift | 55 ++- SessionUtilitiesKit/General/Logging.swift | 114 ++++-- .../LibSession/LibSession.swift | 90 +++-- 17 files changed, 664 insertions(+), 301 deletions(-) create mode 100644 SessionNetworkingKit/Configuration/Router.swift rename SessionUtilitiesKit/General/Feature+ServiceNetwork.swift => SessionNetworkingKit/Configuration/ServiceNetwork.swift (98%) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index d5bde3222d..a21304d79e 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -455,7 +455,6 @@ FD0B77B229B82B7A009169BA /* ArrayUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0B77B129B82B7A009169BA /* ArrayUtilitiesSpec.swift */; }; FD0E353C2AB9880B006A81F7 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0E353A2AB98773006A81F7 /* AppVersion.swift */; }; FD10AF0C2AF32B9A007709E5 /* SessionListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD10AF0B2AF32B9A007709E5 /* SessionListViewModel.swift */; }; - FD10AF122AF85D11007709E5 /* Feature+ServiceNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD10AF112AF85D11007709E5 /* Feature+ServiceNetwork.swift */; }; FD11E22D2CA4D12C001BAF58 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD2286782C38D4FF00BC06F7 /* DifferenceKit */; }; FD11E22E2CA4D12C001BAF58 /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = FDEF57292C3CF50B00131302 /* WebRTC */; }; FD12A83D2AD63BCC00EEBA0D /* EditableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A83C2AD63BCC00EEBA0D /* EditableState.swift */; }; @@ -755,6 +754,8 @@ FD6B92672E697012004463B5 /* Mocked+SUK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92632E696EDC004463B5 /* Mocked+SUK.swift */; }; FD6B926C2E6A7644004463B5 /* Async+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B926B2E6A763D004463B5 /* Async+Utilities.swift */; }; FD6B92722E6AB045004463B5 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = FD6B92712E6AB045004463B5 /* Quick */; }; + FD6B927A2E6F8B90004463B5 /* ServiceNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD10AF112AF85D11007709E5 /* ServiceNetwork.swift */; }; + FD6B927C2E6F8BB2004463B5 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B927B2E6F8BAC004463B5 /* Router.swift */; }; FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */; }; FD6D9CF92CA152B300F706A8 /* Session+SNUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754BB2C9B9A8E002A2623 /* Session+SNUIKit.swift */; }; FD6DA9CF2D015B440092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9CE2D015B440092085A /* Lucide */; }; @@ -1868,7 +1869,7 @@ FD0B77B129B82B7A009169BA /* ArrayUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUtilitiesSpec.swift; sourceTree = ""; }; FD0E353A2AB98773006A81F7 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = ""; }; FD10AF0B2AF32B9A007709E5 /* SessionListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionListViewModel.swift; sourceTree = ""; }; - FD10AF112AF85D11007709E5 /* Feature+ServiceNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Feature+ServiceNetwork.swift"; sourceTree = ""; }; + FD10AF112AF85D11007709E5 /* ServiceNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceNetwork.swift; sourceTree = ""; }; FD11E22F2CA4F498001BAF58 /* DestinationSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationSpec.swift; sourceTree = ""; }; FD12A83C2AD63BCC00EEBA0D /* EditableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableState.swift; sourceTree = ""; }; FD12A83E2AD63BDF00EEBA0D /* Navigatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigatable.swift; sourceTree = ""; }; @@ -2095,6 +2096,7 @@ FD6B925D2E695ACD004463B5 /* FixtureBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixtureBase.swift; sourceTree = ""; }; FD6B92632E696EDC004463B5 /* Mocked+SUK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mocked+SUK.swift"; sourceTree = ""; }; FD6B926B2E6A763D004463B5 /* Async+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Async+Utilities.swift"; sourceTree = ""; }; + FD6B927B2E6F8BAC004463B5 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopSessionCallManager.swift; sourceTree = ""; }; FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _021_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = ""; }; @@ -3086,7 +3088,6 @@ FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */, C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */, FD2272EB2C352155004D8A6C /* Feature.swift */, - FD10AF112AF85D11007709E5 /* Feature+ServiceNetwork.swift */, FD2272EF2C352200004D8A6C /* General.swift */, C3C2A5CE2553860700C340D1 /* Logging.swift */, C33FDAFD255A580600E217F9 /* LRUCache.swift */, @@ -3729,6 +3730,7 @@ isa = PBXGroup; children = ( C3C2A5B0255385C700C340D1 /* Meta */, + FD6B92792E6F8B7E004463B5 /* Configuration */, FDE754E22C9BAFF4002A2623 /* Crypto */, FD17D79D27F40CAA00122BE0 /* Database */, FD7F74682BAB8A5D006DDFD8 /* LibSession */, @@ -4430,6 +4432,15 @@ path = Utilities; sourceTree = ""; }; + FD6B92792E6F8B7E004463B5 /* Configuration */ = { + isa = PBXGroup; + children = ( + FD6B927B2E6F8BAC004463B5 /* Router.swift */, + FD10AF112AF85D11007709E5 /* ServiceNetwork.swift */, + ); + path = Configuration; + sourceTree = ""; + }; FD7115F528C8150600B47552 /* Combine */ = { isa = PBXGroup; children = ( @@ -6385,6 +6396,7 @@ FDF848DA29405C5B007DCAE5 /* GetMessagesResponse.swift in Sources */, FDF8489429405C1B007DCAE5 /* SnodeAPI.swift in Sources */, FD2286682C37DA3B00BC06F7 /* LibSession+Networking.swift in Sources */, + FD6B927A2E6F8B90004463B5 /* ServiceNetwork.swift in Sources */, FD2272A92C33E337004D8A6C /* ContentProxy.swift in Sources */, FDF848C829405C5B007DCAE5 /* ONSResolveRequest.swift in Sources */, FDF848CF29405C5B007DCAE5 /* SendMessageRequest.swift in Sources */, @@ -6394,6 +6406,7 @@ FD2272B52C33E337004D8A6C /* BatchResponse.swift in Sources */, FD3765E92ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift in Sources */, FD5E93D22C12B0580038C25A /* AppVersionResponse.swift in Sources */, + FD6B927C2E6F8BB2004463B5 /* Router.swift in Sources */, 941375BB2D5184C20058F244 /* HTTPHeader+SessionNetwork.swift in Sources */, FD2272AE2C33E337004D8A6C /* HTTPQueryParam.swift in Sources */, FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */, @@ -6492,7 +6505,6 @@ 7B0EFDEE274F598600FFAAE7 /* TimestampUtils.swift in Sources */, FD2272D02C34EBD0004D8A6C /* FileManager.swift in Sources */, FDE754CF2C9BAF37002A2623 /* DataSource.swift in Sources */, - FD10AF122AF85D11007709E5 /* Feature+ServiceNetwork.swift in Sources */, B8F5F58325EC94A6003BF8D4 /* Collection+Utilities.swift in Sources */, FD17D7A127F40D2500122BE0 /* Storage.swift in Sources */, FDE754DB2C9BAF8A002A2623 /* Crypto.swift in Sources */, diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 28dc54d998..0f1719acd3 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", "state" : { - "revision" : "4b8714a7fb84d42393314ce897127b3939885ec3", - "version" : "3.8.5" + "revision" : "a9ed4b6f9bdedce7d77046f43adfb8ce1fd54114", + "version" : "3.9.0" } }, { diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 280a56ab72..3e23b2f880 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -6,6 +6,7 @@ import GRDB import DifferenceKit import SessionUIKit import SessionMessagingKit +import SessionNetworkingKit import SessionUtilitiesKit import SignalUtilitiesKit diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index edede0e26a..33e2ab6ff9 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -6,6 +6,7 @@ import GRDB import DifferenceKit import SignalUtilitiesKit import SessionMessagingKit +import SessionNetworkingKit import SessionUtilitiesKit import StoreKit import SessionUIKit diff --git a/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist b/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist index 93f837c372..53b5af3e6a 100644 --- a/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist +++ b/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist @@ -6,7 +6,7 @@ License BSD 3-Clause License -Copyright (c) 2010-2024, Deusty, LLC +Copyright (c) 2010-2025, Deusty, LLC All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/Session/Settings/DeveloperSettings/DeveloperNetworkSettingsViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperNetworkSettingsViewModel.swift index 4576a9ee4a..015e8ec2d0 100644 --- a/Session/Settings/DeveloperSettings/DeveloperNetworkSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettings/DeveloperNetworkSettingsViewModel.swift @@ -71,6 +71,7 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState public enum TableItem: Hashable, Differentiable, CaseIterable { case environment + case router case pushNotificationService case forceOffline @@ -86,6 +87,7 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState public var differenceIdentifier: String { switch self { case .environment: return "environment" + case .router: return "router" case .pushNotificationService: return "pushNotificationService" case .forceOffline: return "forceOffline" @@ -104,6 +106,7 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState var result: [TableItem] = [] switch TableItem.environment { case .environment: result.append(.environment); fallthrough + case .router: result.append(.router); fallthrough case .pushNotificationService: result.append(.pushNotificationService); fallthrough case .forceOffline: result.append(.forceOffline); fallthrough @@ -122,6 +125,7 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState public struct State: Equatable, ObservableKeyProvider { struct NetworkState: Equatable, Hashable { let environment: ServiceNetwork + let router: Router let pushNotificationService: PushNotificationAPI.Service let forceOffline: Bool @@ -129,12 +133,14 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState public func with( environment: ServiceNetwork? = nil, + router: Router? = nil, pushNotificationService: PushNotificationAPI.Service? = nil, forceOffline: Bool? = nil, devnetConfig: ServiceNetwork.DevnetConfiguration? = nil ) -> NetworkState { return NetworkState( environment: (environment ?? self.environment), + router: (router ?? self.router), pushNotificationService: (pushNotificationService ?? self.pushNotificationService), forceOffline: (forceOffline ?? self.forceOffline), devnetConfig: (devnetConfig ?? self.devnetConfig) @@ -160,6 +166,7 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState static func initialState(using dependencies: Dependencies) -> State { let initialState: NetworkState = NetworkState( environment: dependencies[feature: .serviceNetwork], + router: dependencies[feature: .router], pushNotificationService: dependencies[feature: .pushNotificationService], forceOffline: dependencies[feature: .forceOffline], devnetConfig: dependencies[feature: .devnetConfig] @@ -182,10 +189,12 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState isEnabled: { guard state.initialState != state.pendingState else { return false } - return ( - state.pendingState.environment != .devnet || - state.pendingState.devnetConfig.isValid - ) + switch (state.pendingState.environment, state.pendingState.router) { + case (.devnet, _): return state.pendingState.devnetConfig.isValid + case (.testnet, .lokinet): return true + case (_, .lokinet): return false + default: return true + } }(), accessibility: Accessibility( identifier: "Set button", @@ -234,6 +243,21 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState viewModel?.showEnvironmentModal(pendingState: state.pendingState) } ), + SessionCell.Info( + id: .router, + title: "Router", + subtitle: """ + The routing method which should be used when making network requests. + + The Lokinet option only works on Testnet. + + Current: \(state.pendingState.router.title) + """, + trailingAccessory: .icon(.squarePen), + onTap: { [weak viewModel] in + viewModel?.showRoutingModal(pendingState: state.pendingState) + } + ), SessionCell.Info( id: .pushNotificationService, title: "Push Notification Service", @@ -351,6 +375,7 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState options: ServiceNetwork.allCases.map { network in ConfirmationModal.Info.Body.RadioOptionInfo( title: network.title, + descriptionText: network.subtitle.map { ThemedAttributedString(string: $0) }, enabled: true, selected: pendingState.environment == network ) @@ -390,6 +415,59 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState ) } + private func showRoutingModal(pendingState: State.NetworkState) { + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Router", + body: .radio( + explanation: ThemedAttributedString( + string: "The routing method which should be used when making network requests." + ), + warning: nil, + options: Router.allCases.map { router in + ConfirmationModal.Info.Body.RadioOptionInfo( + title: router.title, + descriptionText: router.subtitle.map { ThemedAttributedString(string: $0) }, + enabled: (router != .direct), + selected: pendingState.router == router + ) + } + ), + confirmTitle: "select".localized(), + cancelStyle: .alert_text, + onConfirm: { [dependencies] modal in + let selected: Router = { + switch modal.info.body { + case .radio(_, _, let options): + return options + .enumerated() + .first(where: { _, value in value.selected }) + .map { index, _ in + guard index < Router.allCases.count else { + return nil + } + + return Router.allCases[index] + } + .defaulting(to: .onionRequests) + + default: return .onionRequests + } + }() + + dependencies.notifyAsync( + priority: .immediate, + key: .updateScreen(DeveloperNetworkSettingsViewModel.self), + value: pendingState.with(router: selected) + ) + } + ) + ), + transitionType: .present + ) + } + private func showPushServiceModal(pendingState: State.NetworkState) { self.transitionToScreen( ConfirmationModal( @@ -705,6 +783,9 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState internalState.initialState.devnetConfig != internalState.pendingState.devnetConfig ) ) + let routerChanged: Bool = ( + internalState.initialState.router != internalState.pendingState.router + ) let pushServiceChanged: Bool = ( internalState.initialState.pushNotificationService != internalState.pendingState.pushNotificationService ) @@ -712,101 +793,96 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState /// Changing the network settings can result in data being cleared from the database so we should confirm that is desired before /// we make the changes guard hasConfirmed else { - switch (networkEnvironmentChanged, pushServiceChanged) { - case (false, false): - /// Most likely just the `forceOffline` (or some new) change - await self.saveChanges(hasConfirmed: true) - - case (false, true): - self.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Change Push Notification Service", - body: .attributedText( - ThemedAttributedString( - stringWithHTMLTags: """ - Are you sure you want to update the Push Notification Service to \(internalState.pendingState.pushNotificationService.title)? - - Warning: - This will unsubscribe from the current service and subscribe to the new service which may take a few minutes. - """, - font: ConfirmationModal.explanationFont - ), - scrollMode: .never - ), - confirmTitle: "confirm".localized(), - confirmStyle: .danger, - cancelStyle: .alert_text, - onConfirm: { [weak self] _ in - Task { [weak self] in - await self?.saveChanges(hasConfirmed: true) - } - } - ) - ), - transitionType: .present - ) - - case (true, false): - self.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Change Environment", - body: .attributedText( - ThemedAttributedString( - stringWithHTMLTags: """ - Are you sure you want to change the environment to \(internalState.pendingState.environment.title)? - - Warning: - This will result in all conversation and snode data being cleared and any pending network requests being cancelled. - """, - font: ConfirmationModal.explanationFont - ), - scrollMode: .never - ), - confirmTitle: "confirm".localized(), - confirmStyle: .danger, - cancelStyle: .alert_text, - onConfirm: { [weak self] _ in - Task { [weak self] in - await self?.saveChanges(hasConfirmed: true) - } - } - ) - ), - transitionType: .present - ) - - case (true, true): - self.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Change Environment", - body: .attributedText( - ThemedAttributedString( - stringWithHTMLTags: """ - Are you sure you want to change the environment to \(internalState.pendingState.environment.title) and the Push Notification Service to \(internalState.pendingState.pushNotificationService.title)? - - Warning: - This will result in all conversation and snode data being cleared and any pending network requests being cancelled. The device will unsubscribe from the current PN service and subscribe to the new service which may take a few minutes. - """, - font: ConfirmationModal.explanationFont - ), - scrollMode: .never - ), - confirmTitle: "confirm".localized(), - confirmStyle: .danger, - cancelStyle: .alert_text, - onConfirm: { [weak self] _ in - Task { [weak self] in - await self?.saveChanges(hasConfirmed: true) - } - } - ) - ), - transitionType: .present - ) + /// If we don't need confirmation then just go ahead (eg. `forceOffline` (or some new) change) + guard networkEnvironmentChanged || routerChanged || pushServiceChanged else { + return await self.saveChanges(hasConfirmed: true) + } + + let message: ThemedAttributedString = ThemedAttributedString(string: "Are you sure you want to update the network settings to:\n") + + let style: NSMutableParagraphStyle = NSMutableParagraphStyle() + style.alignment = .left + + /// Append the list of state changes + if networkEnvironmentChanged { + message.append( + ThemedAttributedString( + stringWithHTMLTags: """ + \nEnvironment: \(internalState.pendingState.environment.title) + """, + font: ConfirmationModal.explanationFont + ).addingAttribute(.paragraphStyle, value: style) + ) + } + + if routerChanged { + message.append( + ThemedAttributedString( + stringWithHTMLTags: """ + \nRouter: \(internalState.pendingState.router.title) + """, + font: ConfirmationModal.explanationFont + ).addingAttribute(.paragraphStyle, value: style) + ) + } + + if pushServiceChanged { + message.append( + ThemedAttributedString( + stringWithHTMLTags: """ + \nPN Service: \(internalState.pendingState.pushNotificationService.title) + """, + font: ConfirmationModal.explanationFont + ).addingAttribute(.paragraphStyle, value: style) + ) + } + + /// Add the warnings + message.append( + ThemedAttributedString( + stringWithHTMLTags: "\n\nWarning this will result in:", + font: ConfirmationModal.explanationFont + ) + .addingAttribute(.paragraphStyle, value: style) + .addingAttribute(.themeForegroundColor, value: ThemeValue.warning) + ) + + if networkEnvironmentChanged { + message.append(NSAttributedString( + string: "\n• All conversation and snode data being cleared and any pending network requests being cancelled.", + attributes: [NSAttributedString.Key.paragraphStyle: style] + )) + } + if routerChanged && !networkEnvironmentChanged { + message.append(NSAttributedString( + string: "\n• Any pending network requests being cancelled.", + attributes: [NSAttributedString.Key.paragraphStyle: style] + )) + } + if pushServiceChanged { + message.append(NSAttributedString( + string: "\n• Resubscribing for push notifications, which may take a few minutes.", + attributes: [NSAttributedString.Key.paragraphStyle: style] + )) } + + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Change Network Settings", + body: .attributedText(message, scrollMode: .never), + confirmTitle: "confirm".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text, + onConfirm: { [weak self] _ in + Task { [weak self] in + await self?.saveChanges(hasConfirmed: true) + } + } + ) + ), + transitionType: .present + ) return } @@ -836,6 +912,17 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState ) } + /// If the router changed then we need to recreate the `network` instance, but updating the environment does the same so + /// no need to do it again in that case + if routerChanged && !networkEnvironmentChanged { + let state: State.NetworkState = internalState.pendingState + + await DeveloperNetworkSettingsViewModel.updateRouter( + router: state.router, + using: dependencies + ) + } + /// Now that any environment changes have been made (which may result in rebuilding the network state, and likely clearing the /// database) we can trigger the push service change if pushServiceChanged && dependencies[defaults: .standard, key: .isUsingFullAPNs] { @@ -915,7 +1002,7 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState } catch { return Log.warn("[DevSettings] Environment change ignored due to error fetching identity data: \(error)") } - Log.info("[DevSettings] Swapping to \(String(describing: serviceNetwork)), clearing data") + Log.info("[DevSettings] Swapping environment to \(String(describing: serviceNetwork)), clearing data") /// Stop all pollers dependencies.remove(singleton: .currentUserPoller) @@ -1015,4 +1102,51 @@ class DeveloperNetworkSettingsViewModel: SessionTableViewModel, NavigatableState Log.info("[DevSettings] Completed swap to \(String(describing: serviceNetwork))") } + + internal static func updateRouter( + router: Router, + using dependencies: Dependencies + ) async { + /// Make sure we are actually changing the router before recreating the network + guard router != dependencies[feature: .router] else { return } + + Log.info("[DevSettings] Swapping router to \(String(describing: router))") + + /// Stop all pollers + dependencies.remove(singleton: .currentUserPoller) + dependencies.remove(singleton: .groupPollerManager) + dependencies.remove(singleton: .communityPollerManager) + + /// Reset the network (only if it's already been created - don't want to initialise the network if it hasn't already been started) + /// + /// **Note:** We need to set this to a `NoopNetwork` because a number of objects observe the `networkStatus` which + /// would result in automatic re-creation of the network with it's current config (since the `serviceNetwork` hasn't been updated + /// yet) + /// + /// **Note 2:** No need to clear the snode cache in this case as we aren't swapping the environment + if dependencies.has(singleton: .network) { + await dependencies[singleton: .network].suspendNetworkAccess() + await dependencies[singleton: .network].finishCurrentObservations() + } + + dependencies.set(singleton: .network, to: LibSession.NoopNetwork()) + + /// Update to the new `Router` + dependencies.set(feature: .router, to: router) + + /// Remove the temporary NoopNetwork and warm a new instance now that the `router` has been updated + dependencies.remove(singleton: .network) + dependencies.warm(singleton: .network) + + /// Restart all pollers + Task { @MainActor [dependencies] in + guard await dependencies[singleton: .onboarding].state.first() == .completed else { return } + + await dependencies[singleton: .currentUserPoller].startIfNeeded() + await dependencies[singleton: .groupPollerManager].startAllPollers() + await dependencies[singleton: .communityPollerManager].startAllPollers() + } + + Log.info("[DevSettings] Completed swap to \(String(describing: router))") + } } diff --git a/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel+Testing.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel+Testing.swift index e0c3b409c3..fc679296c1 100644 --- a/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel+Testing.swift +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel+Testing.swift @@ -4,6 +4,7 @@ import UIKit import SessionUtilitiesKit +import SessionNetworkingKit // MARK: - Automated Test Convenience @@ -29,7 +30,7 @@ extension DeveloperSettingsViewModel { /// **Note:** All values need to be provided as strings (eg. booleans) static func processUnitTestEnvVariablesIfNeeded(using dependencies: Dependencies) async { #if targetEnvironment(simulator) - enum EnvironmentVariable: String { + enum EnvironmentVariable: String, CaseIterable { /// Disables animations for the app (where possible) /// /// **Value:** `true`/`false` (default: `true`) @@ -45,6 +46,21 @@ extension DeveloperSettingsViewModel { /// **Value:** `true`/`false` (default: `true` in debug builds, `false` otherwise) case truncatePubkeysInLogs + /// Controls whether the app should trigger it's "Force Offline" behaviour (the network doesn't connect and all requests + /// fail after a 1 second delay with a serviceUnavailable error) + /// + /// **Value:** `true`/`false` (default: `false`) + case forceOffline + + /// Controls which routing method the app uses to send network requets + /// + /// **Value:** `"onionRequests"`/`"lokinet"`/`"direct"` (default: `"onionRequests"`) + /// + /// **Note:** When set to `lokinet` the `serviceNetwork` **MUST** be set to `testnet` will be used + /// if it's not then `onionRequests` will be used. Additionally `direct` is not currently supported, so + /// `onionRequests` will also be used in that case. + case router + /// Controls whether the app communicates with mainnet or testnet by default /// /// **Value:** `"mainnet"`/`"testnet"`/`"devnet"` (default: `"mainnet"`) @@ -81,12 +97,6 @@ extension DeveloperSettingsViewModel { /// **Note:** This will be ignored if `serviceNetwork` is not `devnet` case devnetOmqPort - /// Controls whether the app should trigger it's "Force Offline" behaviour (the network doesn't connect and all requests - /// fail after a 1 second delay with a serviceUnavailable error) - /// - /// **Value:** `true`/`false` (default: `false`) - case forceOffline - /// Controls whether the app should offer the debug durations for disappearing messages (eg. `10s`, `30s`, etc.) /// /// **Value:** `true`/`false` (default: `false`) @@ -108,7 +118,11 @@ extension DeveloperSettingsViewModel { } let allKeys: Set = Set(envVars.keys) - for (key, value) in envVars { + /// The order the the environment variables are applied in is important (configuring the network needs to happen in a certain + /// order to simplify the below logic) + for key in EnvironmentVariable.allCases { + guard let value: String = envVars[key] else { continue } + switch key { case .animationsEnabled: dependencies.set(feature: .animationsEnabled, to: (value == "true")) @@ -123,6 +137,34 @@ extension DeveloperSettingsViewModel { case .truncatePubkeysInLogs: dependencies.set(feature: .truncatePubkeysInLogs, to: (value == "true")) + case .forceOffline: + dependencies.set(feature: .forceOffline, to: (value == "true")) + + case .router: + let router: Router + + switch value { + case "onionRequests": router = .onionRequests + case "lokinet": + if envVars[.serviceNetwork] != "testnet" { + Log.warn("Router option '\(value)' can only be used on 'testnet', falling back to onion requests") + router = .onionRequests + } + else { + router = .lokinet + } + + case "direct": + router = .onionRequests + Log.warn("Invalid router option '\(value)' provided, falling back to onion requests") + + default: + Log.warn("Invalid router option '\(value)' provided, falling back to onion requests") + router = .onionRequests + } + + dependencies.set(feature: .router, to: router) + case .serviceNetwork: let (network, devnetConfig): (ServiceNetwork, ServiceNetwork.DevnetConfiguration?) = { switch value { @@ -203,13 +245,10 @@ extension DeveloperSettingsViewModel { devnetConfig: devnetConfig, using: dependencies ) - + /// These are handled in the `serviceNetwork` case case .devnetPubkey, .devnetIp, .devnetHttpPort, .devnetOmqPort: break - case .forceOffline: - dependencies.set(feature: .forceOffline, to: (value == "true")) - case .debugDisappearingMessageDurations: dependencies.set(feature: .debugDisappearingMessageDurations, to: (value == "true")) diff --git a/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift index 4393808f5d..f60151de1e 100644 --- a/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift @@ -527,6 +527,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Configure settings related to how and where network requests are sent. Service Network: \(dependencies[feature: .serviceNetwork].title) + Router: \(dependencies[feature: .router].title) PN Service: \(dependencies[feature: .pushNotificationService].title) """, trailingAccessory: .icon(.chevronRight), diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index c6e7987655..a069cd8213 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -7,6 +7,7 @@ import GRDB import DifferenceKit import SessionUIKit import SessionMessagingKit +import SessionNetworkingKit import SessionUtilitiesKit import SignalUtilitiesKit diff --git a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift index 06d2b8e3b9..ff46f4686f 100644 --- a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift +++ b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift @@ -59,122 +59,134 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { .upserted(db) } - /// Try to retrieve the default rooms 8 times - dependencies[singleton: .storage] - .readPublisher { [dependencies] db -> AuthenticationMethod in - try Authentication.with( - db, - server: OpenGroupAPI.defaultServer, - activeOnly: false, /// The record for the default rooms is inactive - using: dependencies - ) - } - .tryFlatMap { [dependencies] authMethod -> AnyPublisher<(ResponseInfoType, OpenGroupAPI.CapabilitiesAndRoomsResponse), Error> in - try OpenGroupAPI.preparedCapabilitiesAndRooms( - authMethod: authMethod, - using: dependencies - ).send(using: dependencies) + /// Don't bother trying to fetch if we don't have a network connection, just wait for one to be established + Task { + let networkStatus: NetworkStatus? = await dependencies[singleton: .network].networkStatus + .first() + + if networkStatus != .connected { + Log.info(.cat, "Waiting for network to connect before fetching.") + _ = await dependencies[singleton: .network].networkStatus.first(where: { $0 == .connected }) } - .subscribe(on: scheduler, using: dependencies) - .receive(on: scheduler, using: dependencies) - .retry(8, using: dependencies) - .sinkUntilComplete( - receiveCompletion: { result in - switch result { - case .finished: - Log.info(.cat, "Successfully retrieved default Community rooms") - success(job, false) - - case .failure(let error): - Log.error(.cat, "Failed to get default Community rooms due to error: \(error)") - failure(job, error, false) - } - }, - receiveValue: { info, response in - let defaultRooms: [OpenGroupManager.DefaultRoomInfo]? = dependencies[singleton: .storage].write { db -> [OpenGroupManager.DefaultRoomInfo] in - // Store the capabilities first - OpenGroupManager.handleCapabilities( - db, - capabilities: response.capabilities.data, - on: OpenGroupAPI.defaultServer - ) - - let existingImageIds: [String: String] = try OpenGroup - .filter(OpenGroup.Columns.server == OpenGroupAPI.defaultServer) - .filter(OpenGroup.Columns.imageId != nil) - .fetchAll(db) - .reduce(into: [:]) { result, next in result[next.id] = next.imageId } - let result: [OpenGroupManager.DefaultRoomInfo] = try response.rooms.data - .compactMap { room -> OpenGroupManager.DefaultRoomInfo? in - /// Try to insert an inactive version of the OpenGroup (use `insert` rather than - /// `save` as we want it to fail if the room already exists) - do { - return ( - room, - try OpenGroup( - server: OpenGroupAPI.defaultServer, - roomToken: room.token, - publicKey: OpenGroupAPI.defaultServerPublicKey, - isActive: false, - name: room.name, - roomDescription: room.roomDescription, - imageId: room.imageId, - userCount: room.activeUsers, - infoUpdates: room.infoUpdates + + dependencies[singleton: .storage] + .readPublisher { [dependencies] db -> AuthenticationMethod in + try Authentication.with( + db, + server: OpenGroupAPI.defaultServer, + activeOnly: false, /// The record for the default rooms is inactive + using: dependencies + ) + } + .tryFlatMap { [dependencies] authMethod -> AnyPublisher<(ResponseInfoType, OpenGroupAPI.CapabilitiesAndRoomsResponse), Error> in + try OpenGroupAPI.preparedCapabilitiesAndRooms( + authMethod: authMethod, + using: dependencies + ).send(using: dependencies) + } + .subscribe(on: scheduler, using: dependencies) + .receive(on: scheduler, using: dependencies) + .sinkUntilComplete( + receiveCompletion: { result in + switch result { + case .finished: + Log.info(.cat, "Successfully retrieved default Community rooms") + success(job, false) + + case .failure(let error): + /// We want to fail permanently here, otherwise we would just indefinitely retry (if the user opens the + /// "Join Community" screen that will kick off another job, otherwise this will automatically be rescheduled + /// on launch) + Log.error(.cat, "Failed to get default Community rooms due to error: \(error)") + failure(job, error, true) + } + }, + receiveValue: { info, response in + let defaultRooms: [OpenGroupManager.DefaultRoomInfo]? = dependencies[singleton: .storage].write { db -> [OpenGroupManager.DefaultRoomInfo] in + // Store the capabilities first + OpenGroupManager.handleCapabilities( + db, + capabilities: response.capabilities.data, + on: OpenGroupAPI.defaultServer + ) + + let existingImageIds: [String: String] = try OpenGroup + .filter(OpenGroup.Columns.server == OpenGroupAPI.defaultServer) + .filter(OpenGroup.Columns.imageId != nil) + .fetchAll(db) + .reduce(into: [:]) { result, next in result[next.id] = next.imageId } + let result: [OpenGroupManager.DefaultRoomInfo] = try response.rooms.data + .compactMap { room -> OpenGroupManager.DefaultRoomInfo? in + /// Try to insert an inactive version of the OpenGroup (use `insert` rather than + /// `save` as we want it to fail if the room already exists) + do { + return ( + room, + try OpenGroup( + server: OpenGroupAPI.defaultServer, + roomToken: room.token, + publicKey: OpenGroupAPI.defaultServerPublicKey, + isActive: false, + name: room.name, + roomDescription: room.roomDescription, + imageId: room.imageId, + userCount: room.activeUsers, + infoUpdates: room.infoUpdates + ) + .inserted(db) ) - .inserted(db) - ) + } + catch { + return try OpenGroup + .fetchOne( + db, + id: OpenGroup.idFor( + roomToken: room.token, + server: OpenGroupAPI.defaultServer + ) + ) + .map { (room, $0) } + } } - catch { - return try OpenGroup - .fetchOne( - db, - id: OpenGroup.idFor( + + /// Schedule the room image download (if it doesn't match out current one) + result.forEach { room, openGroup in + let openGroupId: String = OpenGroup.idFor(roomToken: room.token, server: OpenGroupAPI.defaultServer) + + guard + let imageId: String = room.imageId, + imageId != existingImageIds[openGroupId] || + openGroup.displayPictureOriginalUrl == nil + else { return } + + dependencies[singleton: .jobRunner].add( + db, + job: Job( + variant: .displayPictureDownload, + shouldBeUnique: true, + details: DisplayPictureDownloadJob.Details( + target: .community( + imageId: imageId, roomToken: room.token, server: OpenGroupAPI.defaultServer - ) + ), + timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) ) - .map { (room, $0) } - } + ), + canStartJob: true + ) } - - /// Schedule the room image download (if it doesn't match out current one) - result.forEach { room, openGroup in - let openGroupId: String = OpenGroup.idFor(roomToken: room.token, server: OpenGroupAPI.defaultServer) - guard - let imageId: String = room.imageId, - imageId != existingImageIds[openGroupId] || - openGroup.displayPictureOriginalUrl == nil - else { return } - - dependencies[singleton: .jobRunner].add( - db, - job: Job( - variant: .displayPictureDownload, - shouldBeUnique: true, - details: DisplayPictureDownloadJob.Details( - target: .community( - imageId: imageId, - roomToken: room.token, - server: OpenGroupAPI.defaultServer - ), - timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) - ) - ), - canStartJob: true - ) + return result } - return result - } - - /// Update the `openGroupManager` cache to have the default rooms - dependencies.mutate(cache: .openGroupManager) { cache in - cache.setDefaultRoomInfo(defaultRooms ?? []) + /// Update the `openGroupManager` cache to have the default rooms + dependencies.mutate(cache: .openGroupManager) { cache in + cache.setDefaultRoomInfo(defaultRooms ?? []) + } } - } - ) + ) + } } public static func run(using dependencies: Dependencies) { diff --git a/SessionNetworkingKit/Configuration/Router.swift b/SessionNetworkingKit/Configuration/Router.swift new file mode 100644 index 0000000000..96f1ee5c36 --- /dev/null +++ b/SessionNetworkingKit/Configuration/Router.swift @@ -0,0 +1,43 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SessionUtilitiesKit + +// MARK: - FeatureStorage + +public extension FeatureStorage { + static let router: FeatureConfig = Dependencies.create( + identifier: "router", + defaultOption: .onionRequests + ) +} + +// MARK: - Router + +public enum Router: Int, Sendable, FeatureOption, CaseIterable { + case onionRequests = 1 + case lokinet = 2 + case direct = 3 + + // MARK: - Feature Option + + public static var defaultOption: Router = .onionRequests + + public var title: String { + switch self { + case .onionRequests: return "Onion Requests" + case .lokinet: return "Lokinet" + case .direct: return "Direct" + } + } + + public var subtitle: String? { + switch self { + case .onionRequests: return "Requests will be encrypted in multiple layers and send via multiple hops in the network before going to their destination." + case .lokinet: return "Request will be sent via Lokinet." + case .direct: return "Requests will be sent directly to their destination (This option is not currently supported)." + } + } +} diff --git a/SessionUtilitiesKit/General/Feature+ServiceNetwork.swift b/SessionNetworkingKit/Configuration/ServiceNetwork.swift similarity index 98% rename from SessionUtilitiesKit/General/Feature+ServiceNetwork.swift rename to SessionNetworkingKit/Configuration/ServiceNetwork.swift index 44514bcfd9..389db7b3a9 100644 --- a/SessionUtilitiesKit/General/Feature+ServiceNetwork.swift +++ b/SessionNetworkingKit/Configuration/ServiceNetwork.swift @@ -3,6 +3,7 @@ // stringlint:disable import Foundation +import SessionUtilitiesKit // MARK: - FeatureStorage @@ -17,7 +18,7 @@ public extension FeatureStorage { ) } -// MARK: - ServiceNetwork Feature +// MARK: - ServiceNetwork public enum ServiceNetwork: Int, Sendable, FeatureOption, CaseIterable { case mainnet = 1 diff --git a/SessionNetworkingKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift index f6dc6c230a..233cf624a8 100644 --- a/SessionNetworkingKit/LibSession/LibSession+Networking.swift +++ b/SessionNetworkingKit/LibSession/LibSession+Networking.swift @@ -488,7 +488,7 @@ actor LibSessionNetwork: NetworkType { config.onionreq_single_path_mode = singlePathMode switch (dependencies[feature: .serviceNetwork], dependencies[feature: .devnetConfig], dependencies[feature: .devnetConfig].isValid) { - case (.mainnet, _, _): break + case (.mainnet, _, _): config.netid = SESSION_NETWORK_MAINNET case (.testnet, _, _), (_, _, false): config.netid = SESSION_NETWORK_TESTNET config.enforce_subnet_diversity = false /// On testnet we can't do this as nodes share IPs @@ -499,6 +499,12 @@ actor LibSessionNetwork: NetworkType { cDevnetNodes = [LibSession.Snode(devnetConfig).cSnode] } + switch dependencies[feature: .router] { + case .onionRequests: config.router = SESSION_NETWORK_ROUTER_ONION_REQUESTS + case .lokinet: config.router = SESSION_NETWORK_ROUTER_LOKINET + case .direct: config.router = SESSION_NETWORK_ROUTER_DIRECT + } + /// If it's not the main app then we want to run in "Single Path Mode" (no use creating extra paths in the extensions) if !dependencies[singleton: .appContext].isMainApp { config.onionreq_single_path_mode = true diff --git a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift index e60edeb422..9ffc05c148 100644 --- a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift +++ b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift @@ -485,6 +485,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { options: options.enumerated().map { otherIndex, otherInfo in Info.Body.RadioOptionInfo( title: otherInfo.title, + descriptionText: otherInfo.descriptionText, enabled: otherInfo.enabled, selected: (index == otherIndex), accessibility: otherInfo.accessibility @@ -495,6 +496,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { ) } radioButton.text = optionInfo.title + radioButton.descriptionText = optionInfo.descriptionText radioButton.accessibilityLabel = optionInfo.accessibility?.label radioButton.accessibilityIdentifier = optionInfo.accessibility?.identifier radioButton.update(isEnabled: optionInfo.enabled, isSelected: optionInfo.selected) @@ -962,17 +964,20 @@ public extension ConfirmationModal.Info { } public struct RadioOptionInfo: Equatable, Hashable { public let title: String + public let descriptionText: ThemedAttributedString? public let enabled: Bool public let selected: Bool public let accessibility: Accessibility? public init( title: String, + descriptionText: ThemedAttributedString? = nil, enabled: Bool, selected: Bool = false, accessibility: Accessibility? = nil ) { self.title = title + self.descriptionText = descriptionText self.enabled = enabled self.selected = selected self.accessibility = accessibility diff --git a/SessionUIKit/Components/RadioButton.swift b/SessionUIKit/Components/RadioButton.swift index 6eb78fdb80..56bca0cec2 100644 --- a/SessionUIKit/Components/RadioButton.swift +++ b/SessionUIKit/Components/RadioButton.swift @@ -36,6 +36,14 @@ public class RadioButton: UIView { set { titleLabel.text = newValue } } + public var descriptionText: ThemedAttributedString? { + get { descriptionLabel.attributedText.map { ThemedAttributedString(attributedString: $0) } } + set { + descriptionLabel.themeAttributedText = newValue + descriptionLabel.isHidden = (newValue == nil) + } + } + public private(set) var isEnabled: Bool = true public private(set) var isSelected: Bool = false private let titleTextColor: ThemeValue @@ -51,6 +59,16 @@ public class RadioButton: UIView { return result }() + private lazy var textStackView: UIStackView = { + let result: UIStackView = UIStackView() + result.translatesAutoresizingMaskIntoConstraints = false + result.isUserInteractionEnabled = false + result.axis = .vertical + result.distribution = .fill + + return result + }() + private lazy var titleLabel: UILabel = { let result: UILabel = UILabel() result.translatesAutoresizingMaskIntoConstraints = false @@ -62,6 +80,18 @@ public class RadioButton: UIView { return result }() + private lazy var descriptionLabel: UILabel = { + let result: UILabel = UILabel() + result.translatesAutoresizingMaskIntoConstraints = false + result.isUserInteractionEnabled = false + result.font = .systemFont(ofSize: Values.verySmallFontSize) + result.themeTextColor = titleTextColor + result.numberOfLines = 0 + result.isHidden = true + + return result + }() + private let selectionBorderView: UIView = { let result: UIView = UIView() result.translatesAutoresizingMaskIntoConstraints = false @@ -108,24 +138,21 @@ public class RadioButton: UIView { private func setupViewHierarchy(size: Size) { addSubview(selectionButton) - addSubview(titleLabel) + addSubview(textStackView) addSubview(selectionBorderView) addSubview(selectionView) - self.heightAnchor.constraint( - greaterThanOrEqualTo: titleLabel.heightAnchor, - constant: Values.mediumSpacing - ).isActive = true - self.heightAnchor.constraint( - greaterThanOrEqualTo: selectionBorderView.heightAnchor, - constant: Values.mediumSpacing - ).isActive = true + textStackView.addArrangedSubview(titleLabel) + textStackView.addArrangedSubview(descriptionLabel) + + set(.height, greaterThanOrEqualTo: .height, of: textStackView, withOffset: Values.mediumSpacing) + set(.height, greaterThanOrEqualTo: .height, of: selectionBorderView, withOffset: Values.mediumSpacing) selectionButton.pin(to: self) - titleLabel.center(.vertical, in: self) - titleLabel.pin(.leading, to: .leading, of: self) - titleLabel.pin(.trailing, to: .trailing, of: selectionBorderView, withInset: -Values.verySmallSpacing) + textStackView.center(.vertical, in: self) + textStackView.pin(.leading, to: .leading, of: self) + textStackView.pin(.trailing, to: .leading, of: selectionBorderView, withInset: -Values.verySmallSpacing) selectionBorderView.center(.vertical, in: self) selectionBorderView.pin(.trailing, to: .trailing, of: self) @@ -153,21 +180,25 @@ public class RadioButton: UIView { switch (self.isEnabled, self.isSelected) { case (true, true): titleLabel.themeTextColor = titleTextColor + descriptionLabel.themeTextColor = titleTextColor selectionBorderView.themeBorderColor = .radioButton_selectedBorder selectionView.themeBackgroundColor = .radioButton_selectedBackground case (true, false): titleLabel.themeTextColor = titleTextColor + descriptionLabel.themeTextColor = titleTextColor selectionBorderView.themeBorderColor = .radioButton_unselectedBorder selectionView.themeBackgroundColor = .radioButton_unselectedBackground case (false, true): titleLabel.themeTextColor = .disabled + descriptionLabel.themeTextColor = .disabled selectionBorderView.themeBorderColor = .radioButton_disabledBorder selectionView.themeBackgroundColor = .radioButton_disabledSelectedBackground case (false, false): titleLabel.themeTextColor = .disabled + descriptionLabel.themeTextColor = .disabled selectionBorderView.themeBorderColor = .radioButton_disabledBorder selectionView.themeBackgroundColor = .radioButton_disabledUnselectedBackground } diff --git a/SessionUtilitiesKit/General/Logging.swift b/SessionUtilitiesKit/General/Logging.swift index 67b529d8ec..c23eb69c65 100644 --- a/SessionUtilitiesKit/General/Logging.swift +++ b/SessionUtilitiesKit/General/Logging.swift @@ -43,7 +43,7 @@ public enum Log { level: Log.Level, categories: [Category], message: String, - file: StaticString, + file: String, function: StaticString, line: UInt ) @@ -71,6 +71,42 @@ public enum Log { case .default: return "default" } } + + var emoji: String { + switch self { + case .off, .default: return "" + case .verbose: return "💙" + case .debug: return "💚" + case .info: return "💛" + case .warn: return "🧡" + case .error: return "❤️" + case .critical: return "🔥" + } + } + + var ddLevel: DDLogLevel { + switch self { + case .off, .default: return .off + case .verbose: return .verbose + case .debug: return .warning + case .info: return .info + case .warn: return .warning + case .error: return .error + case .critical: return .error + } + } + + var ddFlag: DDLogFlag { + switch self { + case .off, .default: return .verbose + case .verbose: return .verbose + case .debug: return .debug + case .info: return .info + case .warn: return .warning + case .error: return .error + case .critical: return .error + } + } } public struct Group: Hashable { @@ -255,126 +291,126 @@ public enum Log { // FIXME: Would be nice to properly require a category for all logs public static func verbose( _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.verbose, [], msg, file: file, function: function, line: line) } public static func verbose( _ cat: Category , _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.verbose, [cat], msg, file: file, function: function, line: line) } public static func verbose( _ cats: [Category], _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.verbose, cats, msg, file: file, function: function, line: line) } public static func debug( _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.debug, [], msg, file: file, function: function, line: line) } public static func debug( _ cat: Category, _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.debug, [cat], msg, file: file, function: function, line: line) } public static func debug( _ cats: [Category], _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.debug, cats, msg, file: file, function: function, line: line) } public static func info( _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.info, [], msg, file: file, function: function, line: line) } public static func info( _ cat: Category, _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.info, [cat], msg, file: file, function: function, line: line) } public static func info( _ cats: [Category], _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.info, cats, msg, file: file, function: function, line: line) } public static func warn( _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.warn, [], msg, file: file, function: function, line: line) } public static func warn( _ cat: Category, _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.warn, [cat], msg, file: file, function: function, line: line) } public static func warn( _ cats: [Category], _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.warn, cats, msg, file: file, function: function, line: line) } public static func error( _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.error, [], msg, file: file, function: function, line: line) } public static func error( _ cat: Category, _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.error, [cat], msg, file: file, function: function, line: line) } public static func error( _ cats: [Category], _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.error, cats, msg, file: file, function: function, line: line) } public static func critical( _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.critical, [], msg, file: file, function: function, line: line) } public static func critical( _ cat: Category, _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.critical, [cat], msg, file: file, function: function, line: line) } public static func critical( _ cats: [Category], _ msg: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { custom(.critical, cats, msg, file: file, function: function, line: line) } @@ -382,7 +418,7 @@ public enum Log { public static func assert( _ condition: Bool, _ message: @autoclosure () -> String = String(), - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { @@ -397,7 +433,7 @@ public enum Log { } public static func assertOnMainThread( - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { @@ -412,7 +448,7 @@ public enum Log { } public static func assertNotOnMainThread( - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { @@ -430,7 +466,7 @@ public enum Log { _ level: Level, _ categories: [Category], _ message: String, - file: StaticString = #fileID, + file: String = #fileID, function: StaticString = #function, line: UInt = #line ) { @@ -459,7 +495,7 @@ public protocol LoggerType: Actor { _ level: Log.Level, _ categories: [Log.Category], _ message: String, - file: StaticString, + file: String, function: StaticString, line: UInt ) @@ -687,7 +723,7 @@ public actor Logger: LoggerType { _ level: Log.Level, _ categories: [Log.Category], _ message: String, - file: StaticString, + file: String, function: StaticString, line: UInt ) { @@ -753,16 +789,26 @@ public actor Logger: LoggerType { return updatedText } let ddLogMessage: String = "\(logPrefix) ".appending(cleanedMessage) - let consoleLogMessage: String = "\(logPrefix)[\(level)] ".appending(cleanedMessage) + let consoleLogMessage: String = "\(logPrefix)[\(level)|\(file):\(line)] ".appending(cleanedMessage) - switch level { - case .off, .default: return - case .verbose: DDLogVerbose("💙 \(ddLogMessage)", file: file, function: function, line: line) - case .debug: DDLogDebug("💚 \(ddLogMessage)", file: file, function: function, line: line) - case .info: DDLogInfo("💛 \(ddLogMessage)", file: file, function: function, line: line) - case .warn: DDLogWarn("🧡 \(ddLogMessage)", file: file, function: function, line: line) - case .error: DDLogError("❤️ \(ddLogMessage)", file: file, function: function, line: line) - case .critical: DDLogError("🔥 \(ddLogMessage)", file: file, function: function, line: line) + /// Log the message via `DDLog` if logging is on + if level != .off && level != .default { + DDLog.log( + asynchronous: asyncLoggingEnabled, + message: DDLogMessage( + format: "", + formatted: "\(level.emoji) \(ddLogMessage)", + level: level.ddLevel, + flag: level.ddFlag, + context: 0, + file: file, + function: String(describing: function), + line: line, + tag: nil, + options: [.dontCopyMessage], + timestamp: nil + ) + ) } let mainCategory: String = (categories.first?.rawValue ?? "General") diff --git a/SessionUtilitiesKit/LibSession/LibSession.swift b/SessionUtilitiesKit/LibSession/LibSession.swift index b0361ae41b..c0c696ab2c 100644 --- a/SessionUtilitiesKit/LibSession/LibSession.swift +++ b/SessionUtilitiesKit/LibSession/LibSession.swift @@ -72,44 +72,71 @@ extension LibSession { /// /// We want to simplify the message because our logging already includes category and timestamp information: /// `[+{lifetime}s] {message}` - let processedMessage: String = { - let trimmedMsg = msg.trimmingCharacters(in: .whitespacesAndNewlines) - - guard - let timestampRegex: NSRegularExpression = LibSession.timestampRegex, - let messageStartRegex: NSRegularExpression = LibSession.messageStartRegex - else { return trimmedMsg } - - let fullRange = NSRange(trimmedMsg.startIndex.. Date: Tue, 9 Sep 2025 14:44:18 +1000 Subject: [PATCH 40/59] Fixed an issue where settings could be set before the local config existed --- SessionMessagingKit/Database/Models/Interaction.swift | 5 ++++- SessionMessagingKit/Database/Models/SessionThread.swift | 4 +++- _SharedTestUtilities/MockLogger.swift | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index 1aad3bde79..4cbf754331 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -390,7 +390,10 @@ public struct Interaction: Codable, Identifiable, Equatable, Hashable, Fetchable switch ObservationContext.observingDb { case .none: Log.error("[Interaction] Could not process 'aroundInsert' due to missing observingDb.") case .some(let observingDb): - observingDb.dependencies.setAsync(.hasSavedMessage, true) + observingDb.afterCommit { [dependencies = observingDb.dependencies] in + dependencies.setAsync(.hasSavedMessage, true) + } + observingDb.addMessageEvent(id: id, threadId: threadId, type: .created) if self.expiresStartedAtMs != nil { diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 96ef1ef48e..c17726b975 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -151,7 +151,9 @@ public struct SessionThread: Codable, Identifiable, Equatable, Hashable, Fetchab case .some(let observingDb): /// Only set the `hasSavedThread` value if it's not the 'Note to Self' thread if id != observingDb.dependencies[cache: .general].sessionId.hexString { - observingDb.dependencies.setAsync(.hasSavedThread, true) + observingDb.afterCommit { [dependencies = observingDb.dependencies] in + dependencies.setAsync(.hasSavedThread, true) + } } observingDb.addConversationEvent(id: id, type: .created) diff --git a/_SharedTestUtilities/MockLogger.swift b/_SharedTestUtilities/MockLogger.swift index 744491bde1..28c343e415 100644 --- a/_SharedTestUtilities/MockLogger.swift +++ b/_SharedTestUtilities/MockLogger.swift @@ -29,7 +29,7 @@ public actor MockLogger: LoggerType { _ level: Log.Level, _ categories: [Log.Category], _ message: String, - file: StaticString, + file: String, function: StaticString, line: UInt ) { From 5eb99ed8cf5d8396a23c336fa41436be7cc7f580 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 9 Sep 2025 16:37:47 +1000 Subject: [PATCH 41/59] Updated MockNotificationsManager to use new Mockable convention (was hanging) --- Session.xcodeproj/project.pbxproj | 4 + .../Open Groups/OpenGroupManager.swift | 4 +- ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 1 + .../Open Groups/OpenGroupManagerSpec.swift | 31 ++--- .../MessageReceiverGroupsSpec.swift | 28 ++-- .../NotificationsManagerSpec.swift | 110 ++++++++-------- .../MockCommunityPollerCache.swift | 11 ++ .../MockNotificationsManager.swift | 59 +++++---- ...eadNotificationSettingsViewModelSpec.swift | 13 +- TestUtilities/MockFallbackRegistry.swift | 44 +++++++ TestUtilities/MockHandler.swift | 8 +- TestUtilities/Nimble/NimbleVerification.swift | 120 ++++++++++++++---- 12 files changed, 287 insertions(+), 146 deletions(-) create mode 100644 TestUtilities/MockFallbackRegistry.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index a21304d79e..10ce56ceaf 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -756,6 +756,7 @@ FD6B92722E6AB045004463B5 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = FD6B92712E6AB045004463B5 /* Quick */; }; FD6B927A2E6F8B90004463B5 /* ServiceNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD10AF112AF85D11007709E5 /* ServiceNetwork.swift */; }; FD6B927C2E6F8BB2004463B5 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B927B2E6F8BAC004463B5 /* Router.swift */; }; + FD6B927E2E6FEDFF004463B5 /* MockFallbackRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B927D2E6FEDFE004463B5 /* MockFallbackRegistry.swift */; }; FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */; }; FD6D9CF92CA152B300F706A8 /* Session+SNUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754BB2C9B9A8E002A2623 /* Session+SNUIKit.swift */; }; FD6DA9CF2D015B440092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9CE2D015B440092085A /* Lucide */; }; @@ -2097,6 +2098,7 @@ FD6B92632E696EDC004463B5 /* Mocked+SUK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mocked+SUK.swift"; sourceTree = ""; }; FD6B926B2E6A763D004463B5 /* Async+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Async+Utilities.swift"; sourceTree = ""; }; FD6B927B2E6F8BAC004463B5 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; + FD6B927D2E6FEDFE004463B5 /* MockFallbackRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFallbackRegistry.swift; sourceTree = ""; }; FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopSessionCallManager.swift; sourceTree = ""; }; FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _021_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = ""; }; @@ -4193,6 +4195,7 @@ FD1BDBAC2E653200008EF998 /* Mockable.swift */, FD1BDBE22E655BB4008EF998 /* Mocked.swift */, FD1BDBD82E653866008EF998 /* MockError.swift */, + FD6B927D2E6FEDFE004463B5 /* MockFallbackRegistry.swift */, FD1BDBD22E65365E008EF998 /* MockFunction.swift */, FD1BDBDA2E6538B0008EF998 /* MockFunctionBuilder.swift */, FD1BDBDE2E655734008EF998 /* MockFunctionHandler.swift */, @@ -7061,6 +7064,7 @@ files = ( FD1BDBE52E655DDF008EF998 /* NimbleVerification.swift in Sources */, FD1BDBE32E655BB6008EF998 /* Mocked.swift in Sources */, + FD6B927E2E6FEDFF004463B5 /* MockFallbackRegistry.swift in Sources */, FD1BDBDF2E655735008EF998 /* MockFunctionHandler.swift in Sources */, FD1BDBD92E653868008EF998 /* MockError.swift in Sources */, FD1BDBDB2E6538B4008EF998 /* MockFunctionBuilder.swift in Sources */, diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index c74b23ed61..a2bf724eb0 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -271,12 +271,12 @@ public final class OpenGroupManager { ) } .handleEvents( - receiveCompletion: { [dependencies] result in + receiveCompletion: { [communityPollerManager = dependencies[singleton: .communityPollerManager]] result in switch result { case .finished: /// (Re)start the poller if needed (want to force it to poll immediately in the next run loop to avoid /// a big delay before the next poll) - Task { [communityPollerManager = dependencies[singleton: .communityPollerManager]] in + Task { [communityPollerManager] in let poller = await communityPollerManager.getOrCreatePoller(for: server.lowercased()) await poller.stop() await poller.startIfNeeded() diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index 9119e7a537..9d76ef0afa 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -29,6 +29,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { ) @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( initialSetup: { network in + network.when { $0.networkStatus }.thenReturn(.singleValue(value: .connected)) network .when { $0.send( diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index c12fd3922e..d80fffe27e 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -128,6 +128,7 @@ class OpenGroupManagerSpec: AsyncSpec { ) @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( initialSetup: { network in + network.when { $0.networkStatus }.thenReturn(.singleValue(value: .connected)) network .when { $0.send( @@ -255,25 +256,25 @@ class OpenGroupManagerSpec: AsyncSpec { try await mockCrypto .when { $0.generate(.ciphertextWithXChaCha20(plaintext: .any, encKey: .any)) } .thenReturn(Data([1, 2, 3])) + + try await mockPoller.when { await $0.startIfNeeded() }.thenReturn(()) + try await mockPoller.when { await $0.stop() }.thenReturn(()) + + try await mockCommunityPollerManager.when { await $0.serversBeingPolled }.thenReturn([]) + try await mockCommunityPollerManager.when { await $0.startAllPollers() }.thenReturn(()) + try await mockCommunityPollerManager + .when { await $0.getOrCreatePoller(for: .any) } + .thenReturn(mockPoller) + try await mockCommunityPollerManager.when { await $0.stopAndRemovePoller(for: .any) }.thenReturn(()) + try await mockCommunityPollerManager.when { await $0.stopAndRemoveAllPollers() }.thenReturn(()) + try await mockCommunityPollerManager + .when { $0.syncState } + .thenReturn(CommunityPollerManagerSyncState()) } // MARK: - an OpenGroupManager describe("an OpenGroupManager") { beforeEach { - try await mockPoller.when { await $0.startIfNeeded() }.thenReturn(()) - try await mockPoller.when { await $0.stop() }.thenReturn(()) - - try await mockCommunityPollerManager.when { await $0.serversBeingPolled }.thenReturn([]) - try await mockCommunityPollerManager.when { await $0.startAllPollers() }.thenReturn(()) - try await mockCommunityPollerManager - .when { await $0.getOrCreatePoller(for: .any) } - .thenReturn(mockPoller) - try await mockCommunityPollerManager.when { await $0.stopAndRemovePoller(for: .any) }.thenReturn(()) - try await mockCommunityPollerManager.when { await $0.stopAndRemoveAllPollers() }.thenReturn(()) - try await mockCommunityPollerManager - .when { await $0.syncState } - .thenReturn(CommunityPollerManagerSyncState()) - _ = userGroupsInitResult } @@ -770,7 +771,7 @@ class OpenGroupManagerSpec: AsyncSpec { ) ) } - .wasCalled() + .wasCalled(timeout: .milliseconds(100)) await mockPoller.verify { await $0.startIfNeeded() }.wasCalled() } diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index 8adcb0506c..cf52de85b6 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -313,9 +313,9 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockNotificationsManager) - .to(call(.exactly(times: 1), matchingParameters: .all) { notificationsManager in - notificationsManager.addNotificationRequest( + await fixture.mockNotificationsManager + .verify { + $0.addNotificationRequest( content: NotificationContent( threadId: fixture.groupId.hexString, threadVariant: .group, @@ -334,7 +334,8 @@ class MessageReceiverGroupsSpec: AsyncSpec { ), extensionBaseUnreadCount: nil ) - }) + } + .wasCalled(exactly: 1) } } @@ -464,9 +465,9 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockNotificationsManager) - .to(call(.exactly(times: 1), matchingParameters: .all) { notificationsManager in - notificationsManager.addNotificationRequest( + await fixture.mockNotificationsManager + .verify { + $0.addNotificationRequest( content: NotificationContent( threadId: fixture.groupId.hexString, threadVariant: .group, @@ -492,7 +493,8 @@ class MessageReceiverGroupsSpec: AsyncSpec { ), extensionBaseUnreadCount: nil ) - }) + } + .wasCalled(exactly: 1) } // MARK: ------ and push notifications are disabled @@ -3241,9 +3243,7 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { var mockFileManager: MockFileManager { mock(for: .fileManager) { MockFileManager() } } var mockExtensionHelper: MockExtensionHelper { mock(for: .extensionHelper) { MockExtensionHelper() } } var mockGroupPollerManager: MockGroupPollerManager { mock(for: .groupPollerManager) } - var mockNotificationsManager: MockNotificationsManager { - mock(for: .notificationsManager) { MockNotificationsManager() } - } + var mockNotificationsManager: MockNotificationsManager { mock(for: .notificationsManager) } var mockGeneralCache: MockGeneralCache { mock(cache: .general) { MockGeneralCache() } } var mockLibSessionCache: MockLibSessionCache { mock(cache: .libSession) { MockLibSessionCache() } } var mockSnodeAPICache: MockSnodeAPICache { mock(cache: .snodeAPI) { MockSnodeAPICache() } } @@ -3528,7 +3528,7 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { await applyBaselineFileManager() await applyBaselineExtensionHelper() try await applyBaselineGroupPollerManager() - await applyBaselineNotificationsManager() + try await applyBaselineNotificationsManager() await applyBaselineGeneralCache() await applyBaselineLibSessionCache() await applyBaselineSnodeAPICache() @@ -3677,8 +3677,8 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { try await mockGroupPollerManager.when { await $0.stopAndRemoveAllPollers() }.thenReturn(()) } - private func applyBaselineNotificationsManager() async { - mockNotificationsManager.defaultInitialSetup() + private func applyBaselineNotificationsManager() async throws { + try await mockNotificationsManager.defaultInitialSetup() } private func applyBaselineGeneralCache() async { diff --git a/SessionMessagingKitTests/Sending & Receiving/Notifications/NotificationsManagerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Notifications/NotificationsManagerSpec.swift index bcb7522892..1ed9fd13e9 100644 --- a/SessionMessagingKitTests/Sending & Receiving/Notifications/NotificationsManagerSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/Notifications/NotificationsManagerSpec.swift @@ -3,13 +3,14 @@ import Foundation import SessionUIKit import SessionUtilitiesKit +import TestUtilities import Quick import Nimble @testable import SessionMessagingKit -class NotificationsManagerSpec: QuickSpec { +class NotificationsManagerSpec: AsyncSpec { override class func spec() { // MARK: Configuration @@ -24,9 +25,7 @@ class NotificationsManagerSpec: QuickSpec { helper.when { $0.hasDedupeRecordSinceLastCleared(threadId: .any) }.thenReturn(false) } ) - @TestState(singleton: .notificationsManager, in: dependencies) var mockNotificationsManager: MockNotificationsManager! = MockNotificationsManager( - initialSetup: { $0.defaultInitialSetup() } - ) + @TestState(singleton: .notificationsManager, in: dependencies) var mockNotificationsManager: MockNotificationsManager! = .create() @TestState var message: Message! = VisibleMessage( sender: "05\(TestConstants.publicKey.replacingOccurrences(of: "1", with: "2"))", sentTimestampMs: 1234567892, @@ -51,6 +50,8 @@ class NotificationsManagerSpec: QuickSpec { ) }.thenReturn(1234567800) dependencies.set(cache: .libSession, to: mockLibSessionCache) + + try await mockNotificationsManager.defaultInitialSetup() } // MARK: - a NotificationsManager - Ensure Should Show @@ -1283,9 +1284,12 @@ class NotificationsManagerSpec: QuickSpec { shouldShowForMessageRequest: { false } ) }.toNot(throwError()) - expect(mockNotificationsManager).to(call(.exactly(times: 1), matchingParameters: .all) { - $0.settings(threadId: "05\(TestConstants.publicKey)", threadVariant: .contact) - }) + + await mockNotificationsManager + .verify { + $0.settings(threadId: "05\(TestConstants.publicKey)", threadVariant: .contact) + } + .wasCalled(exactly: 1) } // MARK: -- checks whether it should show for messages requests if the message is a message request @@ -1339,27 +1343,29 @@ class NotificationsManagerSpec: QuickSpec { shouldShowForMessageRequest: { false } ) }.toNot(throwError()) - expect(mockNotificationsManager).to(call(.exactly(times: 1), matchingParameters: .all) { - $0.addNotificationRequest( - content: NotificationContent( - threadId: "05\(TestConstants.publicKey)", - threadVariant: .contact, - identifier: "05\(TestConstants.publicKey)-TestId", - category: .incomingMessage, - title: "0588...c65b", - body: "Test", - sound: .note, - applicationState: .background - ), - notificationSettings: Preferences.NotificationSettings( - previewType: .nameAndPreview, - sound: .defaultNotificationSound, - mentionsOnly: false, - mutedUntil: nil - ), - extensionBaseUnreadCount: 1 - ) - }) + await mockNotificationsManager + .verify { + $0.addNotificationRequest( + content: NotificationContent( + threadId: "05\(TestConstants.publicKey)", + threadVariant: .contact, + identifier: "05\(TestConstants.publicKey)-TestId", + category: .incomingMessage, + title: "0588...c65b", + body: "Test", + sound: .note, + applicationState: .background + ), + notificationSettings: Preferences.NotificationSettings( + previewType: .nameAndPreview, + sound: .defaultNotificationSound, + mentionsOnly: false, + mutedUntil: nil + ), + extensionBaseUnreadCount: 1 + ) + } + .wasCalled(exactly: 1) } // MARK: -- uses a random identifier for reaction notifications @@ -1395,29 +1401,31 @@ class NotificationsManagerSpec: QuickSpec { shouldShowForMessageRequest: { false } ) }.toNot(throwError()) - expect(mockNotificationsManager).to(call(.exactly(times: 1), matchingParameters: .all) { - $0.addNotificationRequest( - content: NotificationContent( - threadId: "05\(TestConstants.publicKey)", - threadVariant: .contact, - identifier: "00000000-0000-0000-0000-000000000001", - category: .incomingMessage, - title: "0588...c65b", - body: "emojiReactsNotification" - .put(key: "emoji", value: "A") - .localized(), - sound: .note, - applicationState: .background - ), - notificationSettings: Preferences.NotificationSettings( - previewType: .nameAndPreview, - sound: .defaultNotificationSound, - mentionsOnly: false, - mutedUntil: nil - ), - extensionBaseUnreadCount: 1 - ) - }) + await mockNotificationsManager + .verify { + $0.addNotificationRequest( + content: NotificationContent( + threadId: "05\(TestConstants.publicKey)", + threadVariant: .contact, + identifier: "00000000-0000-0000-0000-000000000001", + category: .incomingMessage, + title: "0588...c65b", + body: "emojiReactsNotification" + .put(key: "emoji", value: "A") + .localized(), + sound: .note, + applicationState: .background + ), + notificationSettings: Preferences.NotificationSettings( + previewType: .nameAndPreview, + sound: .defaultNotificationSound, + mentionsOnly: false, + mutedUntil: nil + ), + extensionBaseUnreadCount: 1 + ) + } + .wasCalled(exactly: 1) } } } diff --git a/SessionMessagingKitTests/_TestUtilities/MockCommunityPollerCache.swift b/SessionMessagingKitTests/_TestUtilities/MockCommunityPollerCache.swift index 7d2f2b05a2..1b8c7398ff 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockCommunityPollerCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockCommunityPollerCache.swift @@ -10,6 +10,12 @@ class MockCommunityPollerManager: CommunityPollerManagerType, Mockable { required init(handler: MockHandler) { self.handler = handler + + /// Register `any PollerType` with the `MockFallbackRegistry` so we don't need to explicitly mock `getOrCreatePoller` + MockFallbackRegistry.register( + for: (any PollerType).self, + provider: { MockPoller(handler: .invalid()) } + ) } required init(handlerForBuilder: any MockFunctionHandler) { @@ -27,3 +33,8 @@ class MockCommunityPollerManager: CommunityPollerManagerType, Mockable { func stopAndRemovePoller(for server: String) async { handler.mockNoReturn(args: [server]) } func stopAndRemoveAllPollers() async { handler.mockNoReturn() } } + +extension CommunityPoller.Info: @retroactive Mocked { + public static let any: CommunityPoller.Info = CommunityPoller.Info(server: .any, pollFailureCount: .any) + public static let mock: CommunityPoller.Info = CommunityPoller.Info(server: .mock, pollFailureCount: .mock) +} diff --git a/SessionMessagingKitTests/_TestUtilities/MockNotificationsManager.swift b/SessionMessagingKitTests/_TestUtilities/MockNotificationsManager.swift index 427bf01912..6e57935cbd 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockNotificationsManager.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockNotificationsManager.swift @@ -5,28 +5,35 @@ import Combine import GRDB import SessionMessagingKit import SessionUtilitiesKit +import TestUtilities -public class MockNotificationsManager: Mock, NotificationsManagerType { - public required init(using dependencies: Dependencies) { - super.init() - - mockNoReturn(untrackedArgs: [dependencies]) +class MockNotificationsManager: NotificationsManagerType, Mockable { + let handler: MockHandler + let dependencies: Dependencies = TestDependencies.any + + required init(handler: MockHandler) { + self.handler = handler } - internal required init(functionHandler: MockFunctionHandler_Old? = nil, initialSetup: ((Mock) -> ())? = nil) { - super.init(functionHandler: functionHandler, initialSetup: initialSetup) + required init(handlerForBuilder: any MockFunctionHandler) { + self.handler = MockHandler(forwardingHandler: handlerForBuilder) + } + + public required init(using dependencies: Dependencies) { + handler = MockHandler(dummyProvider: { _ in MockNotificationsManager(handler: .invalid()) }) + handler.mockNoReturn() } public func setDelegate(_ delegate: (any UNUserNotificationCenterDelegate)?) { - mockNoReturn(args: [delegate]) + handler.mockNoReturn(args: [delegate]) } public func registerSystemNotificationSettings() -> AnyPublisher { - return mock() + return handler.mock() } public func settings(threadId: String?, threadVariant: SessionThread.Variant) -> Preferences.NotificationSettings { - return mock(args: [threadId, threadVariant]) + return handler.mock(args: [threadId, threadVariant]) } public func updateSettings( @@ -35,18 +42,18 @@ public class MockNotificationsManager: Mock, Notificat mentionsOnly: Bool, mutedUntil: TimeInterval? ) { - return mock(args: [threadId, threadVariant, mentionsOnly, mutedUntil]) + return handler.mock(args: [threadId, threadVariant, mentionsOnly, mutedUntil]) } public func notificationUserInfo( threadId: String, threadVariant: SessionThread.Variant ) -> [String: AnyHashable] { - return mock(args: [threadId, threadVariant]) + return handler.mock(args: [threadId, threadVariant]) } public func notificationShouldPlaySound(applicationState: UIApplication.State) -> Bool { - return mock(args: [applicationState]) + return handler.mock(args: [applicationState]) } public func notifyForFailedSend( @@ -54,11 +61,11 @@ public class MockNotificationsManager: Mock, Notificat threadVariant: SessionThread.Variant, applicationState: UIApplication.State ) { - mockNoReturn(args: [threadId, threadVariant, applicationState]) + handler.mockNoReturn(args: [threadId, threadVariant, applicationState]) } public func scheduleSessionNetworkPageLocalNotifcation(force: Bool) { - mockNoReturn(args: [force]) + handler.mockNoReturn(args: [force]) } public func addNotificationRequest( @@ -66,29 +73,29 @@ public class MockNotificationsManager: Mock, Notificat notificationSettings: Preferences.NotificationSettings, extensionBaseUnreadCount: Int? ) { - mockNoReturn(args: [content, notificationSettings, extensionBaseUnreadCount]) + handler.mockNoReturn(args: [content, notificationSettings, extensionBaseUnreadCount]) } public func cancelNotifications(identifiers: [String]) { - mockNoReturn(args: [identifiers]) + handler.mockNoReturn(args: [identifiers]) } public func clearAllNotifications() { - mockNoReturn() + handler.mockNoReturn() } } // MARK: - Convenience -extension Mock where T == NotificationsManagerType { - func defaultInitialSetup() { - self +extension MockNotificationsManager { + func defaultInitialSetup() async throws { + try await self .when { $0.notificationUserInfo(threadId: .any, threadVariant: .any) } .thenReturn([:]) - self + try await self .when { $0.notificationShouldPlaySound(applicationState: .any) } .thenReturn(false) - self + try await self .when { $0.addNotificationRequest( content: .any, @@ -97,10 +104,10 @@ extension Mock where T == NotificationsManagerType { ) } .thenReturn(()) - self + try await self .when { $0.cancelNotifications(identifiers: .any) } .thenReturn(()) - self + try await self .when { $0.settings(threadId: .any, threadVariant: .any) } .thenReturn( Preferences.NotificationSettings( @@ -110,7 +117,7 @@ extension Mock where T == NotificationsManagerType { mutedUntil: nil ) ) - self + try await self .when { $0.updateSettings( threadId: .any, diff --git a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift index 417bf4376a..f8f7408dbc 100644 --- a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift @@ -33,9 +33,7 @@ class ThreadNotificationSettingsViewModelSpec: AsyncSpec { .thenReturn(nil) } ) - @TestState(singleton: .notificationsManager, in: dependencies) var mockNotificationsManager: MockNotificationsManager! = MockNotificationsManager( - initialSetup: { $0.defaultInitialSetup() } - ) + @TestState(singleton: .notificationsManager, in: dependencies) var mockNotificationsManager: MockNotificationsManager! = .create() @TestState var viewModel: ThreadNotificationSettingsViewModel! @TestState var cancellables: [AnyCancellable]! @@ -50,6 +48,8 @@ class ThreadNotificationSettingsViewModelSpec: AsyncSpec { creationDateTimestamp: 0 ).insert(db) } + try await mockNotificationsManager.defaultInitialSetup() + viewModel = await ThreadNotificationSettingsViewModel( threadId: "TestId", threadVariant: .contact, @@ -424,15 +424,16 @@ class ThreadNotificationSettingsViewModelSpec: AsyncSpec { it("saves the updated settings") { await MainActor.run { [footerButtonInfo] in footerButtonInfo?.onTap() } - await expect(mockNotificationsManager) - .toEventually(call(.exactly(times: 1), matchingParameters: .all) { + await mockNotificationsManager + .verify { $0.updateSettings( threadId: "TestId", threadVariant: .contact, mentionsOnly: false, mutedUntil: Date.distantFuture.timeIntervalSince1970 ) - }) + } + .wasCalled(exactly: 1, timeout: .milliseconds(100)) } } } diff --git a/TestUtilities/MockFallbackRegistry.swift b/TestUtilities/MockFallbackRegistry.swift new file mode 100644 index 0000000000..28f7950c87 --- /dev/null +++ b/TestUtilities/MockFallbackRegistry.swift @@ -0,0 +1,44 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public final class MockFallbackRegistry { + internal static let shared: MockFallbackRegistry = MockFallbackRegistry() + private var fallbacks: [ObjectIdentifier: () -> Any] = [:] + private let lock: NSLock = NSLock() + + private init() {} + + // MARK: - Public Functions + + public static func register(for type: T.Type, provider: @escaping () -> T) { + shared.registerFallback(for: type, provider: provider) + } + + // MARK: - Internal Functions + + internal func registerFallback(for type: T.Type, provider: @escaping () -> T) { + lock.lock() + defer { lock.unlock() } + + let typeId: ObjectIdentifier = ObjectIdentifier(T.self) + fallbacks[typeId] = provider + } + + internal func makeFallback(for type: T.Type) -> T? { + lock.lock() + defer { lock.unlock() } + + let typeId: ObjectIdentifier = ObjectIdentifier(T.self) + + if let provider = fallbacks[typeId], let value: T = provider() as? T { + return value + } + + if let mockedType = T.self as? any Mocked.Type, let value = mockedType.mock as? T { + return value + } + + return nil + } +} diff --git a/TestUtilities/MockHandler.swift b/TestUtilities/MockHandler.swift index 7393b81ebf..28f1a44a70 100644 --- a/TestUtilities/MockHandler.swift +++ b/TestUtilities/MockHandler.swift @@ -289,11 +289,11 @@ public extension MockHandler { return () as! Output } - guard let mockedType = Output.self as? Mocked.Type else { - fatalError("FATAL: The return type '\(Output.self)' of the non-throwing function '\(funcName)' does not conform to 'Mocked'. This conformance is required to provide a fallback value when a test fails due to a missing stub.") + if let fallbackValue: Output = MockFallbackRegistry.shared.makeFallback(for: Output.self) { + return fallbackValue } - - return mockedType.mock as! Output + + fatalError("FATAL: The return type '\(Output.self)' of the non-throwing function '\(funcName)' does not conform to 'Mocked' and has no custom fallback registered. The framework cannot produce a default value.") } } } diff --git a/TestUtilities/Nimble/NimbleVerification.swift b/TestUtilities/Nimble/NimbleVerification.swift index 7080c061cc..35dd58619e 100644 --- a/TestUtilities/Nimble/NimbleVerification.swift +++ b/TestUtilities/Nimble/NimbleVerification.swift @@ -5,56 +5,107 @@ import Foundation internal import Nimble -public struct NimbleVerification { - public let matchingCalls: [RecordedCall]? - public let allCallsForFunction: [RecordedCall]? +public struct NimbleVerification { + fileprivate struct VerificationData { + fileprivate let mock: M + fileprivate let callBlock: (M.MockedType) async throws -> R + } + + fileprivate let data: VerificationData - public func wasCalled(exactly times: Int, fileID: String = #fileID, file: String = #filePath, line: UInt = #line) { - expect(fileID: fileID, file: file, line: line, self).to(beCalled(exactly: times)) + public func wasCalled( + exactly times: Int, + timeout: DispatchTimeInterval = .seconds(0), + fileID: String = #fileID, + file: String = #filePath, + line: UInt = #line + ) async { + if timeout == .seconds(0) { + await expect(fileID: fileID, file: file, line: line, self.data).to(beCalled(exactly: times)) + } + else { + await expect(fileID: fileID, file: file, line: line, self.data) + .toEventually(beCalled(exactly: times), timeout: timeout.nimbleInterval) + } } - public func wasCalled(atLeast times: Int = 1, fileID: String = #fileID, file: String = #filePath, line: UInt = #line) { - let actualCount: Int = (matchingCalls?.count ?? 0) - let description: String = "Expected call to happen at least \(times) time(s), but was called \(actualCount) times(s)." - - expect(fileID: fileID, file: file, line: line, actualCount) - .to(beGreaterThanOrEqualTo(times), description: description) + public func wasCalled( + atLeast times: Int = 1, + timeout: DispatchTimeInterval = .seconds(0), + fileID: String = #fileID, + file: String = #filePath, + line: UInt = #line + ) async { + if timeout == .seconds(0) { + await expect(fileID: fileID, file: file, line: line, self.data).to(beCalled(atLeast: times)) + } + else { + await expect(fileID: fileID, file: file, line: line, self.data) + .toEventually(beCalled(atLeast: times), timeout: timeout.nimbleInterval) + } } - public func wasNotCalled(fileID: String = #fileID, file: String = #filePath, line: UInt = #line) { - expect(fileID: fileID, file: file, line: line, self).to(beCalled(exactly: 0)) + public func wasNotCalled( + timeout: DispatchTimeInterval = .seconds(0), + fileID: String = #fileID, + file: String = #filePath, + line: UInt = #line + ) async { + if timeout == .seconds(0) { + await expect(fileID: fileID, file: file, line: line, self.data).to(beCalled(exactly: 0)) + } + else { + await expect(fileID: fileID, file: file, line: line, self.data) + .toEventually(beCalled(exactly: 0), timeout: timeout.nimbleInterval) + } } } public extension Mockable { - func verify(_ callBlock: @escaping (MockedType) async throws -> R) async -> NimbleVerification { - let matching: [RecordedCall] = (await handler.recordedCalls(for: callBlock) ?? []) - let all: [RecordedCall] = (await handler.allRecordedCalls(for: callBlock) ?? []) - + func verify(_ callBlock: @escaping (MockedType) async throws -> R) async -> NimbleVerification { return NimbleVerification( - matchingCalls: matching, - allCallsForFunction: all + data: NimbleVerification.VerificationData(mock: self, callBlock: callBlock) ) } } -internal func beCalled(exactly times: Int) -> Matcher { - return Matcher { actualExpression in - let message: ExpectationMessage = ExpectationMessage.expectedTo("be called exactly \(times) time(s)") +private func beCalled( + exactly exactTimes: Int? = nil, + atLeast atLeastTimes: Int? = nil +) -> AsyncMatcher.VerificationData> { + return AsyncMatcher { actualExpression in + let message: ExpectationMessage = (atLeastTimes != nil ? + ExpectationMessage.expectedTo("be called at least \(atLeastTimes ?? 1) time(s)") : + ExpectationMessage.expectedTo("be called exactly \(exactTimes ?? 1) time(s)") + ) - guard let verification = try actualExpression.evaluate() else { + guard let info = try await actualExpression.evaluate() else { return MatcherResult(status: .fail, message: message.appendedBeNilHint()) } - let actualCount: Int = (verification.matchingCalls?.count ?? 0) + let matchingCalls: [RecordedCall] = (await info.mock.handler.recordedCalls(for: info.callBlock) ?? []) - if actualCount == times { - return MatcherResult(status: .matches, message: message) + switch (exactTimes, atLeastTimes) { + case (.some(let times), _): + if matchingCalls.count == times { + return MatcherResult(status: .matches, message: message) + } + + case (_, .some(let times)): + if matchingCalls.count >= times { + return MatcherResult(status: .matches, message: message) + } + + case (.none, .none): + if matchingCalls.count >= 1 { + return MatcherResult(status: .matches, message: message) + } } var details: String = "" + let maybeAllCalls: [RecordedCall]? = await info.mock.handler.allRecordedCalls(for: info.callBlock) - if let allCalls: [RecordedCall] = verification.allCallsForFunction, !allCalls.isEmpty { + if let allCalls: [RecordedCall] = maybeAllCalls, !allCalls.isEmpty { let callDescriptions: String = allCalls .map { call in let args: String = call.args.map { summary(for: $0) }.joined(separator: ", ") @@ -71,8 +122,21 @@ internal func beCalled(exactly times: Int) -> Matcher { return MatcherResult( status: .fail, message: message - .appended(message: ", got \(actualCount) matching call(s).") + .appended(message: ", got \(matchingCalls.count) matching call(s).") .appended(details: details) ) } } + +private extension DispatchTimeInterval { + var nimbleInterval: NimbleTimeInterval { + switch self { + case .seconds(let value): return .seconds(value) + case .milliseconds(let value): return .milliseconds(value) + case .microseconds(let value): return .microseconds(value) + case .nanoseconds(let value): return .nanoseconds(value) + case .never: return .seconds(0) + @unknown default: return .seconds(0) + } + } +} From 85184ba81c82c4fbee1d4570c38b9811ffffaa87 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 10 Sep 2025 12:53:18 +1000 Subject: [PATCH 42/59] Fixed a bunch of broke unit tests (still some left) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Replaced most `data.write` usages with `dependencies[singleton: .fileManager].write` for unit test mocking • Fixed a warning that would appear when running migration tests due to invalid mock image data trying to be loaded • Fixed Onboarding unit tests • Fixed LibSession-related tests --- Session/Onboarding/Onboarding.swift | 16 +- .../_036_GroupsRebuildChanges.swift | 8 +- .../Database/Models/Attachment.swift | 2 +- .../Database/Models/LinkPreview.swift | 6 +- .../Jobs/AttachmentDownloadJob.swift | 6 +- .../LibSession+SessionMessagingKit.swift | 12 +- .../Open Groups/OpenGroupAPI.swift | 2 + .../Types/Request+OpenGroupAPI.swift | 2 + SessionMessagingKit/Utilities/AppSetup.swift | 2 +- .../Utilities/AttachmentManager.swift | 8 +- .../Utilities/DisplayPictureManager.swift | 8 +- .../Jobs/DisplayPictureDownloadJobSpec.swift | 4 +- ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 26 +-- .../LibSession/LibSessionGroupInfoSpec.swift | 22 +- .../LibSessionGroupMembersSpec.swift | 8 +- .../LibSession/LibSessionSpec.swift | 64 +++--- .../Utilities/ExtensionHelperSpec.swift | 5 +- .../_TestUtilities/MockLibSessionCache.swift | 11 +- SessionNetworkingKit/SnodeAPI/SnodeAPI.swift | 11 - .../_TestUtilities/MockNetwork.swift | 30 +++ .../ShareNavController.swift | 7 +- SessionTests/Database/DatabaseSpec.swift | 3 + SessionTests/Onboarding/OnboardingSpec.swift | 206 ++++++++++-------- SessionUtilitiesKit/Media/DataSource.swift | 6 +- SessionUtilitiesKit/Types/FileManager.swift | 15 ++ .../Image Editing/ImageEditorModel.swift | 2 +- TestUtilities/Mocked.swift | 5 + _SharedTestUtilities/MockFileManager.swift | 8 + 28 files changed, 307 insertions(+), 198 deletions(-) diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index ea6d77a213..f6cd644458 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -298,13 +298,11 @@ extension Onboarding { } catch { Log.warn(.onboarding, "Failed to retrieve existing profile information due to error: \(error).") + + /// Always emit a value if we got a response (doesn't matter it it was a successful response or not, we just want to + /// finish loading) + await self?.displayNameStream.send(self?.displayNameStream.currentValue) } - - guard let self = self else { return } - - /// Always emit a value if we got a response (doesn't matter it it was a successful response or not, we just want to - /// finish loading) - await displayNameStream.send(displayNameStream.currentValue ?? "") } } @@ -313,6 +311,8 @@ extension Onboarding { } func setDisplayName(_ displayName: String) async { + retrieveDisplayNameTask?.cancel() + await displayNameStream.send(displayName) } @@ -371,8 +371,8 @@ extension Onboarding { ) /// Load the initial `libSession` state (won't have been created on launch due to lack of ed25519 key) - dependencies.mutate(cache: .libSession) { cache in - cache.loadState(db) + try dependencies.mutate(cache: .libSession) { cache in + try cache.loadState(db, userEd25519SecretKey: ed25519KeyPair.secretKey) /// If we have a `userProfileConfigMessage` then we should try to handle it here as if we don't then /// we won't even process it (because the hash may be deduped via another process) diff --git a/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift b/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift index 6bd3de624c..da393dfe2e 100644 --- a/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift @@ -186,9 +186,13 @@ enum _036_GroupsRebuildChanges: Migration { .path // Save the decrypted display picture to disk - try? imageData.write(to: URL(fileURLWithPath: filePath), options: [.atomic]) + try? dependencies[singleton: .fileManager].write( + data: imageData, + to: URL(fileURLWithPath: filePath), + options: .atomic + ) - guard UIImage(contentsOfFile: filePath) != nil else { + guard dependencies[singleton: .fileManager].imageContents(atPath: filePath) != nil else { Log.error("[GroupsRebuildChanges] Failed to save Community imageData for \(threadId)") return } diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index 9419f7b1d9..6de1a3da7e 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -685,7 +685,7 @@ extension Attachment { let path: String = try? dependencies[singleton: .attachmentManager].path(for: downloadUrl) else { return false } - try data.write(to: URL(fileURLWithPath: path)) + try dependencies[singleton: .fileManager].write(data: data, to: URL(fileURLWithPath: path)) return true } diff --git a/SessionMessagingKit/Database/Models/LinkPreview.swift b/SessionMessagingKit/Database/Models/LinkPreview.swift index 0c8cada73f..3d17b35b45 100644 --- a/SessionMessagingKit/Database/Models/LinkPreview.swift +++ b/SessionMessagingKit/Database/Models/LinkPreview.swift @@ -137,7 +137,11 @@ public extension LinkPreview { guard let mimeType: String = type.preferredMIMEType else { return nil } let filePath = dependencies[singleton: .fileManager].temporaryFilePath(fileExtension: fileExtension) - try imageData.write(to: NSURL.fileURL(withPath: filePath), options: .atomicWrite) + try dependencies[singleton: .fileManager].write( + data: imageData, + to: URL(fileURLWithPath: filePath), + options: .atomic + ) let dataSource: DataSourcePath = DataSourcePath( filePath: filePath, sourceFilename: nil, diff --git a/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift b/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift index b52dbe95d5..36419bfef8 100644 --- a/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift +++ b/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift @@ -144,7 +144,11 @@ public enum AttachmentDownloadJob: JobExecutor { .receive(on: scheduler, using: dependencies) .tryMap { attachment, temporaryFileUrl, data -> Attachment in // Store the encrypted data temporarily - try data.write(to: temporaryFileUrl, options: .atomic) + try dependencies[singleton: .fileManager].write( + data: data, + to: temporaryFileUrl, + options: .atomic + ) // Decrypt the data let plaintext: Data = try { diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 04babf5755..4d1317abbc 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -206,7 +206,7 @@ public extension LibSession { // MARK: - State Management - public func loadState(_ db: ObservingDatabase) { + public func loadState(_ db: ObservingDatabase, userEd25519SecretKey: [UInt8]) throws { // Ensure we have the ed25519 key and that we haven't already loaded the state before // we continue guard configStore.isEmpty else { @@ -270,11 +270,11 @@ public extension LibSession { } /// Now that we have fully populated and sorted `configsToLoad` we should load each into memory - configsToLoad.forEach { sessionId, variant, dump in - configStore[sessionId, variant] = try? loadState( + try configsToLoad.forEach { sessionId, variant, dump in + configStore[sessionId, variant] = try loadState( for: variant, sessionId: sessionId, - userEd25519SecretKey: dependencies[cache: .general].ed25519SecretKey, + userEd25519SecretKey: userEd25519SecretKey, groupEd25519SecretKey: groupsByKey[sessionId.hexString]? .groupIdentityPrivateKey .map { Array($0) }, @@ -950,7 +950,7 @@ public protocol LibSessionCacheType: LibSessionImmutableCacheType, MutableCacheT // MARK: - State Management - func loadState(_ db: ObservingDatabase) + func loadState(_ db: ObservingDatabase, userEd25519SecretKey: [UInt8]) throws func loadDefaultStateFor( variant: ConfigDump.Variant, sessionId: SessionId, @@ -1208,7 +1208,7 @@ private final class NoopLibSessionCache: LibSessionCacheType, NoopDependency { // MARK: - State Management - func loadState(_ db: ObservingDatabase) {} + func loadState(_ db: ObservingDatabase, userEd25519SecretKey: [UInt8]) throws {} func loadDefaultStateFor( variant: ConfigDump.Variant, sessionId: SessionId, diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 10771b5dc5..8a6a011b7b 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -810,6 +810,7 @@ public enum OpenGroupAPI { fileName: fileName ), body: data, + category: .upload, requestTimeout: Network.fileUploadTimeout ), responseType: FileUploadResponse.self, @@ -847,6 +848,7 @@ public enum OpenGroupAPI { return try Network.PreparedRequest( request: Request( endpoint: .roomFileIndividual(roomToken, fileId), + category: .download, authMethod: authMethod, requestTimeout: Network.fileDownloadTimeout ), diff --git a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift index e096467c5e..8c290dffb7 100644 --- a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift @@ -14,6 +14,7 @@ public extension Request where Endpoint == OpenGroupAPI.Endpoint { queryParameters: [HTTPQueryParam: String] = [:], headers: [HTTPHeader: String] = [:], body: T? = nil, + category: Network.RequestCategory = .standard, authMethod: AuthenticationMethod, requestTimeout: TimeInterval = Network.defaultTimeout, overallTimeout: TimeInterval? = nil @@ -32,6 +33,7 @@ public extension Request where Endpoint == OpenGroupAPI.Endpoint { x25519PublicKey: publicKey ), body: body, + category: category, requestTimeout: requestTimeout, overallTimeout: overallTimeout ) diff --git a/SessionMessagingKit/Utilities/AppSetup.swift b/SessionMessagingKit/Utilities/AppSetup.swift index 3a97f2fd02..af7ce2b400 100644 --- a/SessionMessagingKit/Utilities/AppSetup.swift +++ b/SessionMessagingKit/Utilities/AppSetup.swift @@ -76,7 +76,7 @@ public enum AppSetup { userSessionId: userSessionId, using: dependencies ) - cache.loadState(db) + try? cache.loadState(db, userEd25519SecretKey: ed25519KeyPair.secretKey) dependencies.set(cache: .libSession, to: cache) return ( diff --git a/SessionMessagingKit/Utilities/AttachmentManager.swift b/SessionMessagingKit/Utilities/AttachmentManager.swift index 394cb1fef0..814040323f 100644 --- a/SessionMessagingKit/Utilities/AttachmentManager.swift +++ b/SessionMessagingKit/Utilities/AttachmentManager.swift @@ -171,13 +171,17 @@ public final class AttachmentManager: Sendable, ThumbnailManager { public func existingThumbnailImage(url: URL, size: ImageDataManager.ThumbnailSize) -> UIImage? { guard let thumbnailUrl: URL = try? thumbnailUrl(for: url, size: size) else { return nil } - return UIImage(contentsOfFile: thumbnailUrl.path) + return dependencies[singleton: .fileManager].imageContents(atPath: thumbnailUrl.path) } public func saveThumbnail(data: Data, size: ImageDataManager.ThumbnailSize, url: URL) { guard let thumbnailUrl: URL = try? thumbnailUrl(for: url, size: size) else { return } - try? data.write(to: thumbnailUrl) + try? dependencies[singleton: .fileManager].write( + data: data, + to: thumbnailUrl, + options: .atomic + ) } // MARK: - Validity diff --git a/SessionMessagingKit/Utilities/DisplayPictureManager.swift b/SessionMessagingKit/Utilities/DisplayPictureManager.swift index fec7bb37ac..f9a874d763 100644 --- a/SessionMessagingKit/Utilities/DisplayPictureManager.swift +++ b/SessionMessagingKit/Utilities/DisplayPictureManager.swift @@ -261,7 +261,13 @@ public class DisplayPictureManager { let temporaryFilePath: String = dependencies[singleton: .fileManager].temporaryFilePath(fileExtension: fileExtension) // Write the avatar to disk - do { try finalImageData.write(to: URL(fileURLWithPath: temporaryFilePath), options: [.atomic]) } + do { + try dependencies[singleton: .fileManager].write( + data: finalImageData, + to: URL(fileURLWithPath: temporaryFilePath), + options: .atomic + ) + } catch { Log.error(.displayPictureManager, "Updating service with profile failed.") throw DisplayPictureError.writeFailed diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index 2ad09df9f5..695fbd45fd 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -516,7 +516,7 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { endpoint: expectedRequest.endpoint, destination: expectedRequest.destination, body: expectedRequest.body, - category: .upload, + category: .download, requestTimeout: expectedRequest.requestTimeout, overallTimeout: expectedRequest.overallTimeout ) @@ -581,7 +581,7 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { endpoint: expectedRequest.endpoint, destination: expectedRequest.destination, body: expectedRequest.body, - category: .upload, + category: .download, requestTimeout: expectedRequest.requestTimeout, overallTimeout: expectedRequest.overallTimeout ) diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index 9d76ef0afa..dba901e704 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -311,8 +311,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { }) } - // MARK: -- will retry 8 times before it fails - it("will retry 8 times before it fails") { + // MARK: -- permanently fails if it gets an error + it("permanently fails if it gets an error") { mockNetwork .when { $0.send( @@ -339,17 +339,17 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { ) expect(error).to(matchError(NetworkError.parsingFailed)) - expect(mockNetwork) // First attempt + 8 retries - .to(call(.exactly(times: 9)) { network in - network.send( - endpoint: MockEndpoint.any, - destination: .any, - body: .any, - category: .any, - requestTimeout: .any, - overallTimeout: .any - ) - }) + expect(permanentFailure).to(beTrue()) + expect(mockNetwork).to(call(.exactly(times: 1)) { network in + network.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + }) } // MARK: -- stores the updated capabilities diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift index d3dd1d9faa..866fed58db 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift @@ -58,6 +58,10 @@ class LibSessionGroupInfoSpec: AsyncSpec { @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + try await mockStorage.perform(migrations: SNMessagingKit.migrations) try await mockStorage.writeAsync { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) @@ -76,10 +80,6 @@ class LibSessionGroupInfoSpec: AsyncSpec { ) } - /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead - mockGeneralCache.defaultInitialSetup() - dependencies.set(cache: .general, to: mockGeneralCache) - var conf: UnsafeMutablePointer! var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) _ = user_groups_init(&conf, &secretKey, nil, 0, nil) @@ -229,9 +229,9 @@ class LibSessionGroupInfoSpec: AsyncSpec { // MARK: ---- updates the formation timestamp if it is later than the current value it("updates the formation timestamp if it is later than the current value") { - // Note: the 'formationTimestamp' stores the "joinedAt" date so we on'y update it if it's later - // than the current value (as we don't want to replace the record of when the current user joined - // the group with when the group was originally created) + /// **Note:** the `formationTimestamp` stores the "joinedAt" date so we only update it if it's later than + /// the current value (as we don't want to replace the record of when the current user joined the group with + /// when the group was originally created) mockStorage.write { db in try ClosedGroup.updateAll(db, ClosedGroup.Columns.formationTimestamp.set(to: 50000)) } createGroupOutput.groupState[.groupInfo]?.conf.map { groups_info_set_created($0, 54321) } let originalGroup: ClosedGroup? = mockStorage.read { db in @@ -843,6 +843,14 @@ class LibSessionGroupInfoSpec: AsyncSpec { // MARK: ---- deletes from the server after deleting messages before a given timestamp it("deletes from the server after deleting messages before a given timestamp") { + mockLibSessionCache + .when { $0.authData(groupSessionId: .any) } + .thenReturn( + GroupAuthData( + groupIdentityPrivateKey: Data(createGroupOutput.identityKeyPair.secretKey), + authData: nil + ) + ) mockStorage.write { db in try SessionThread.upsert( db, diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift index 33a5b727a9..cbcc225d19 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift @@ -57,6 +57,10 @@ class LibSessionGroupMembersSpec: AsyncSpec { @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() beforeEach { + /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + mockGeneralCache.defaultInitialSetup() + dependencies.set(cache: .general, to: mockGeneralCache) + try await mockStorage.perform(migrations: SNMessagingKit.migrations) try await mockStorage.writeAsync { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) @@ -75,10 +79,6 @@ class LibSessionGroupMembersSpec: AsyncSpec { ) } - /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead - mockGeneralCache.defaultInitialSetup() - dependencies.set(cache: .general, to: mockGeneralCache) - var conf: UnsafeMutablePointer! var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) _ = user_groups_init(&conf, &secretKey, nil, 0, nil) diff --git a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift index 16fdbc883b..25a984512e 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift @@ -30,42 +30,10 @@ class LibSessionSpec: AsyncSpec { @TestState var userGroupsConfig: LibSession.Config! beforeEach { - try await mockStorage.perform(migrations: SNMessagingKit.migrations) - try await mockStorage.writeAsync { db in - try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) - try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) - try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) - try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) - - createGroupOutput = try LibSession.createGroup( - db, - name: "TestGroup", - description: nil, - displayPictureUrl: nil, - displayPictureEncryptionKey: nil, - members: [], - using: dependencies - ) - } - /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) - var conf: UnsafeMutablePointer! - var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) - _ = user_groups_init(&conf, &secretKey, nil, 0, nil) - - mockLibSessionCache.defaultInitialSetup( - configs: [ - .userGroups: .userGroups(conf), - .groupInfo: createGroupOutput.groupState[.groupInfo], - .groupMembers: createGroupOutput.groupState[.groupMembers], - .groupKeys: createGroupOutput.groupState[.groupKeys] - ] - ) - dependencies.set(cache: .libSession, to: mockLibSessionCache) - try await mockCrypto .when { $0.generate(.ed25519KeyPair()) } .thenReturn( @@ -93,6 +61,38 @@ class LibSessionSpec: AsyncSpec { .thenReturn( Authentication.Signature.standard(signature: Array("TestSignature".data(using: .utf8)!)) ) + + try await mockStorage.perform(migrations: SNMessagingKit.migrations) + try await mockStorage.writeAsync { db in + try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) + try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) + try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) + try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) + + createGroupOutput = try LibSession.createGroup( + db, + name: "TestGroup", + description: nil, + displayPictureUrl: nil, + displayPictureEncryptionKey: nil, + members: [], + using: dependencies + ) + } + + var conf: UnsafeMutablePointer! + var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) + _ = user_groups_init(&conf, &secretKey, nil, 0, nil) + + mockLibSessionCache.defaultInitialSetup( + configs: [ + .userGroups: .userGroups(conf), + .groupInfo: createGroupOutput.groupState[.groupInfo], + .groupMembers: createGroupOutput.groupState[.groupMembers], + .groupKeys: createGroupOutput.groupState[.groupKeys] + ] + ) + dependencies.set(cache: .libSession, to: mockLibSessionCache) } // MARK: - LibSession diff --git a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift index ab3f16f420..4c8988b746 100644 --- a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift +++ b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift @@ -1453,10 +1453,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- does nothing if it fails to replicate it("does nothing if it fails to replicate") { try await mockCrypto - .when { $0.generate(.ciphertextWithXChaCha20(plaintext: Data([2, 3, 4]), encKey: .any)) } - .thenThrow(TestError.mock) - try await mockCrypto - .when { $0.generate(.ciphertextWithXChaCha20(plaintext: Data([5, 6, 7]), encKey: .any)) } + .when { $0.generate(.ciphertextWithXChaCha20(plaintext: .any, encKey: .any)) } .thenThrow(TestError.mock) try? extensionHelper.replicate( diff --git a/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift b/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift index e7da3d370f..0fd0c99049 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift @@ -15,8 +15,8 @@ class MockLibSessionCache: Mock, LibSessionCacheType { // MARK: - State Management - func loadState(_ db: ObservingDatabase) { - mockNoReturn(untrackedArgs: [db]) + func loadState(_ db: ObservingDatabase, userEd25519SecretKey: [UInt8]) throws { + try mockThrowingNoReturn(args: [userEd25519SecretKey], untrackedArgs: [db]) } func loadDefaultStateFor( @@ -466,7 +466,12 @@ extension Mock where T == LibSessionCacheType { self .when { $0.authData(groupSessionId: .any) } .thenReturn(GroupAuthData( - groupIdentityPrivateKey: Data([1, 2, 3]), + groupIdentityPrivateKey: Data([ + 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8 + ]), authData: nil )) } diff --git a/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift index e44e17ccb0..aba2b75416 100644 --- a/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift +++ b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift @@ -583,11 +583,6 @@ public final class SnodeAPI { snode: snode, body: [:] ), -// request: Request, Endpoint>( -// endpoint: .getInfo, -// snode: snode, -// body: [:] -// ), responseType: GetNetworkTimestampResponse.self, using: dependencies ) @@ -607,18 +602,12 @@ public final class SnodeAPI { request: Request, responseType: R.Type, requireAllBatchResponses: Bool = true, -// retryCount: Int = 0, -// requestTimeout: TimeInterval = Network.defaultTimeout, -// requestAndPathBuildTimeout: TimeInterval? = nil, using dependencies: Dependencies ) throws -> Network.PreparedRequest { return try Network.PreparedRequest( request: request, responseType: responseType, requireAllBatchResponses: requireAllBatchResponses, -// retryCount: retryCount, -// requestTimeout: requestTimeout, -// requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) .handleEvents( diff --git a/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift index 1ed9d66ff9..93434fa620 100644 --- a/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift +++ b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift @@ -143,6 +143,36 @@ extension MockNetwork { static func errorResponse() -> AnyPublisher<(ResponseInfoType, Data?), Error> { return Fail(error: TestError.mock).eraseToAnyPublisher() } + + static func response(info: MockResponseInfo = .mock, with value: T) -> (ResponseInfoType, Data?) { + return (info, try? JSONEncoder().with(outputFormatting: .sortedKeys).encode(value)) + } + + static func response(info: MockResponseInfo = .mock, type: T.Type) -> (ResponseInfoType, Data?) { + return response(info: info, with: T.mock) + } + + static func response(info: MockResponseInfo = .mock, type: Array.Type) -> (ResponseInfoType, Data?) { + return response(info: info, with: [T.mock]) + } + + static func batchResponseData( + info: MockResponseInfo = .mock, + with value: [(endpoint: E, data: Data)] + ) -> (ResponseInfoType, Data?) { + let data: Data = "[\(value.map { String(data: $0.data, encoding: .utf8)! }.joined(separator: ","))]" + .data(using: .utf8)! + + return (info, data) + } + + static func response(info: MockResponseInfo = .mock, data: Data) -> (ResponseInfoType, Data?) { + return (info, data) + } + + static func nullResponse(info: MockResponseInfo = .mock) -> (ResponseInfoType, Data?) { + return (info, nil) + } } // MARK: - MockResponseInfo diff --git a/SessionShareExtension/ShareNavController.swift b/SessionShareExtension/ShareNavController.swift index 92359cc47c..a63ccbffb6 100644 --- a/SessionShareExtension/ShareNavController.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -541,8 +541,11 @@ final class ShareNavController: UINavigationController { if let data = image.pngData() { let tempFilePath: String = dependencies[singleton: .fileManager].temporaryFilePath(fileExtension: "png") // stringlint:ignore do { - let url = NSURL.fileURL(withPath: tempFilePath) - try data.write(to: url) + let url: URL = URL(fileURLWithPath: tempFilePath) + try dependencies[singleton: .fileManager].write( + data: data, + to: URL(fileURLWithPath: tempFilePath) + ) resolver( Result.success( diff --git a/SessionTests/Database/DatabaseSpec.swift b/SessionTests/Database/DatabaseSpec.swift index 304eadfbbe..e773f667a6 100644 --- a/SessionTests/Database/DatabaseSpec.swift +++ b/SessionTests/Database/DatabaseSpec.swift @@ -26,6 +26,9 @@ class DatabaseSpec: AsyncSpec { customWriter: try! DatabaseQueue(), using: dependencies ) + @TestState(singleton: .fileManager, in: dependencies) var mockFileManager: MockFileManager! = MockFileManager( + initialSetup: { $0.defaultInitialSetup() } + ) @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() @TestState var libSessionCache: LibSession.Cache! = LibSession.Cache( userSessionId: SessionId(.standard, hex: TestConstants.publicKey), diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index aa0f921f7f..2e70018fdb 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -39,73 +39,7 @@ class OnboardingSpec: AsyncSpec { defaults.when { $0.set(false, forKey: .any) }.thenReturn(()) } ) - @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( - initialSetup: { network in - network.when { try await $0.getSwarm(for: .any) }.thenReturn([ - LibSession.Snode( - ed25519PubkeyHex: "1234", - ip: "1.2.3.4", - httpsPort: 1233, - quicPort: 1234, - version: "2.11.0", - swarmId: 1 - ) - ]) - - let cache: LibSession.Cache = LibSession.Cache( - userSessionId: SessionId(.standard, hex: TestConstants.publicKey), - using: dependencies - ) - cache.loadDefaultStateFor( - variant: .userProfile, - sessionId: cache.userSessionId, - userEd25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)), - groupEd25519SecretKey: nil - ) - try? cache.updateProfile(displayName: "TestPolledName") - let pendingPushes: LibSession.PendingPushes? = try? cache.pendingPushes( - swarmPublicKey: cache.userSessionId.hexString - ) - - network - .when { - $0.send( - endpoint: MockEndpoint.mock, - destination: .any, - body: .any, - category: .any, - requestTimeout: .any, - overallTimeout: .any - ) - } - .thenReturn(MockNetwork.batchResponseData( - with: [ - ( - SnodeAPI.Endpoint.getMessages, - GetMessagesResponse( - messages: (pendingPushes? - .pushData - .first { $0.variant == .userProfile }? - .data - .enumerated() - .map { index, data in - GetMessagesResponse.RawMessage( - base64EncodedDataString: data.base64EncodedString(), - expirationMs: nil, - hash: "\(index)", - timestampMs: 1234567890 - ) - } ?? []), - more: false, - hardForkVersion: [2, 2], - timeOffset: 0 - - ).batchSubResponse() - ) - ] - )) - } - ) + @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork() @TestState(singleton: .extensionHelper, in: dependencies) var mockExtensionHelper: MockExtensionHelper! = MockExtensionHelper( initialSetup: { helper in helper @@ -177,6 +111,71 @@ class OnboardingSpec: AsyncSpec { try await mockCrypto .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) + + mockNetwork.when { try await $0.getSwarm(for: .any) }.thenReturn([ + LibSession.Snode( + ed25519PubkeyHex: "1234", + ip: "1.2.3.4", + httpsPort: 1233, + quicPort: 1234, + version: "2.11.0", + swarmId: 1 + ) + ]) + + let pendingPushes: LibSession.PendingPushes? = { + let cache: LibSession.Cache = LibSession.Cache( + userSessionId: SessionId(.standard, hex: TestConstants.publicKey), + using: dependencies + ) + cache.loadDefaultStateFor( + variant: .userProfile, + sessionId: cache.userSessionId, + userEd25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)), + groupEd25519SecretKey: nil + ) + try? cache.updateProfile(displayName: "TestPolledName") + + return try? cache.pendingPushes(swarmPublicKey: cache.userSessionId.hexString) + }() + + mockNetwork + .when { + try await $0.send( + endpoint: MockEndpoint.mock, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } + .thenReturn(MockNetwork.batchResponseData( + with: [ + ( + SnodeAPI.Endpoint.getMessages, + GetMessagesResponse( + messages: (pendingPushes? + .pushData + .first { $0.variant == .userProfile }? + .data + .enumerated() + .map { index, data in + GetMessagesResponse.RawMessage( + base64EncodedDataString: data.base64EncodedString(), + expirationMs: nil, + hash: "\(index)", + timestampMs: 1234567890 + ) + } ?? []), + more: false, + hardForkVersion: [2, 2], + timeOffset: 0 + + ).batchSubResponse() + ) + ] + )) } // MARK: - an Onboarding Cache - Initialization @@ -192,6 +191,7 @@ class OnboardingSpec: AsyncSpec { flow: .restore, using: dependencies ) + try await manager.loadInitialState() } // MARK: -- stores the initialFlow @@ -222,11 +222,16 @@ class OnboardingSpec: AsyncSpec { // MARK: ---- generates new key pairs it("generates new key pairs") { - await expect { await manager.ed25519KeyPair.publicKey.toHexString() }.to(equal("010203")) - await expect { await manager.ed25519KeyPair.secretKey.toHexString() }.to(equal("040506")) - await expect { await manager.x25519KeyPair.publicKey.toHexString() }.to(equal("030201")) - await expect { await manager.x25519KeyPair.secretKey.toHexString() }.to(equal("060504")) - await expect { await manager.userSessionId }.to(equal(SessionId(.standard, hex: "030201"))) + await expect { await manager.ed25519KeyPair.publicKey.toHexString() } + .toEventually(equal("010203")) + await expect { await manager.ed25519KeyPair.secretKey.toHexString() } + .toEventually(equal("040506")) + await expect { await manager.x25519KeyPair.publicKey.toHexString() } + .toEventually(equal("030201")) + await expect { await manager.x25519KeyPair.secretKey.toHexString() } + .toEventually(equal("060504")) + await expect { await manager.userSessionId } + .toEventually(equal(SessionId(.standard, hex: "030201"))) } } @@ -297,9 +302,10 @@ class OnboardingSpec: AsyncSpec { .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [9, 8, 7])) try await mockCrypto .when { - $0.generate(.ed25519KeyPair( - seed: [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8] - )) } + $0.generate(.ed25519KeyPair(seed: [ + 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ])) } .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [4, 5, 6])) try await mockCrypto .when { $0.generate(.x25519(ed25519Pubkey: [1, 2, 3])) } @@ -331,6 +337,7 @@ class OnboardingSpec: AsyncSpec { flow: .restore, using: dependencies ) + try await manager.loadInitialState() await expect{ await manager.state.first() }.to(equal(.noUserInvalidSeedGeneration)) } @@ -398,9 +405,10 @@ class OnboardingSpec: AsyncSpec { .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [9, 8, 7])) try await mockCrypto .when { - $0.generate(.ed25519KeyPair( - seed: [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8] - )) } + $0.generate(.ed25519KeyPair(seed: [ + 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ])) } .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [4, 5, 6])) try await mockCrypto .when { $0.generate(.x25519(ed25519Pubkey: [1, 2, 3])) } @@ -415,7 +423,7 @@ class OnboardingSpec: AsyncSpec { // MARK: -------- has an empty display name it("has an empty display name") { - await expect { await manager.displayName.first() }.to(equal("")) + await expect { await manager.displayName.first() }.to(beNil()) } } } @@ -460,32 +468,34 @@ class OnboardingSpec: AsyncSpec { flow: .register, using: dependencies ) - try? await manager.setSeedData(Data(hex: TestConstants.edKeySeed).prefix(upTo: 16)) + try await manager.loadInitialState() + try await manager.setSeedData(Data(hex: TestConstants.edKeySeed).prefix(upTo: 16)) } // MARK: -- throws if the seed is the wrong length it("throws if the seed is the wrong length") { - expect { try await manager.setSeedData(Data([1, 2, 3])) } - .to(throwError(CryptoError.invalidSeed)) + await expect { try await manager.setSeedData(Data([1, 2, 3])) } + .toEventually(throwError(CryptoError.invalidSeed)) } // MARK: -- stores the seed it("stores the seed") { - await expect { await manager.seed }.to(equal(Data(hex: TestConstants.edKeySeed).prefix(upTo: 16))) + await expect { await manager.seed } + .toEventually(equal(Data(hex: TestConstants.edKeySeed).prefix(upTo: 16))) } // MARK: -- stores the generated identity it("stores the generated identity") { await expect { await manager.ed25519KeyPair.publicKey.toHexString() } - .to(equal(TestConstants.edPublicKey)) + .toEventually(equal(TestConstants.edPublicKey)) await expect { await manager.ed25519KeyPair.secretKey.toHexString() } - .to(equal(TestConstants.edSecretKey)) + .toEventually(equal(TestConstants.edSecretKey)) await expect { await manager.x25519KeyPair.publicKey.toHexString() } - .to(equal(TestConstants.publicKey)) + .toEventually(equal(TestConstants.publicKey)) await expect { await manager.x25519KeyPair.secretKey.toHexString() } - .to(equal(TestConstants.privateKey)) + .toEventually(equal(TestConstants.privateKey)) await expect { await manager.userSessionId } - .to(equal(SessionId(.standard, hex: TestConstants.publicKey))) + .toEventually(equal(SessionId(.standard, hex: TestConstants.publicKey))) } // MARK: -- polls for the userProfile config @@ -494,7 +504,7 @@ class OnboardingSpec: AsyncSpec { await expect(mockNetwork) .toEventually(call(.exactly(times: 1), matchingParameters: .atLeast(3)) { - $0.send( + try await $0.send( endpoint: MockEndpoint.mock, destination: Network.Destination.snode( LibSession.Snode( @@ -528,30 +538,34 @@ class OnboardingSpec: AsyncSpec { flow: .register, using: dependencies ) + try await manager.loadInitialState() } // MARK: -- stores the useAPNs setting it("stores the useAPNs setting") { - await expect { await manager.useAPNS }.to(beFalse()) + await expect { await manager.useAPNS }.toEventually(beFalse()) await manager.setUseAPNS(true) - await expect { await manager.useAPNS }.to(beTrue()) + await expect { await manager.useAPNS }.toEventually(beTrue()) } // MARK: -- stores the display name it("stores the display name") { - await expect { await manager.displayName.first() }.to(equal("")) + await expect { await manager.displayName.first() }.toEventually(equal("")) await manager.setDisplayName("TestName") - await expect { await manager.displayName.first() }.to(equal("TestName")) + await expect { await manager.displayName.first() }.toEventually(equal("TestName")) } } // MARK: - an Onboarding Cache - Complete Registration describe("an Onboarding Cache when completing registration") { justBeforeEach { + mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) + manager = Onboarding.Manager( flow: .register, using: dependencies ) + try await manager.loadInitialState() await manager.setDisplayName("TestCompleteName") await manager.completeRegistration() } @@ -695,6 +709,7 @@ class OnboardingSpec: AsyncSpec { flow: .restore, using: dependencies ) + try await manager.loadInitialState() await manager.setDisplayName("TestCompleteName") await manager.completeRegistration() @@ -725,11 +740,12 @@ class OnboardingSpec: AsyncSpec { flow: .register, using: dependencies ) - await expect { await manager.state.first() }.toNot(equal(.completed)) + try await manager.loadInitialState() + await expect { await manager.state.first() }.toEventuallyNot(equal(.completed)) await manager.setDisplayName("TestCompleteName") await manager.completeRegistration() - await expect { await manager.state.first() }.to(equal(.completed)) + await expect { await manager.state.first() }.toEventually(equal(.completed)) } } } diff --git a/SessionUtilitiesKit/Media/DataSource.swift b/SessionUtilitiesKit/Media/DataSource.swift index 84fcddb595..ee09320fc4 100644 --- a/SessionUtilitiesKit/Media/DataSource.swift +++ b/SessionUtilitiesKit/Media/DataSource.swift @@ -160,7 +160,11 @@ public class DataSourceValue: DataSource { } public func write(to path: String) throws { - try data.write(to: URL(fileURLWithPath: path), options: .atomic) + try dependencies[singleton: .fileManager].write( + data: data, + to: URL(fileURLWithPath: path), + options: .atomic + ) } public static func == (lhs: DataSourceValue, rhs: DataSourceValue) -> Bool { diff --git a/SessionUtilitiesKit/Types/FileManager.swift b/SessionUtilitiesKit/Types/FileManager.swift index 6c49165f86..e6fe46c5f2 100644 --- a/SessionUtilitiesKit/Types/FileManager.swift +++ b/SessionUtilitiesKit/Types/FileManager.swift @@ -3,6 +3,7 @@ // stringlint:disable import Foundation +import UIKit.UIImage // MARK: - Singleton @@ -29,6 +30,7 @@ public protocol FileManagerType { func protectFileOrFolder(at path: String, fileProtectionType: FileProtectionType) throws func fileSize(of path: String) -> UInt64? func temporaryFilePath(fileExtension: String?) -> String + func write(data: Data, to url: URL, options: Data.WritingOptions) throws func write(data: Data, toTemporaryFileWithExtension fileExtension: String?) throws -> String? // MARK: - Forwarded NSFileManager @@ -46,6 +48,7 @@ public protocol FileManagerType { func fileExists(atPath: String) -> Bool func fileExists(atPath: String, isDirectory: UnsafeMutablePointer?) -> Bool func contents(atPath: String) -> Data? + func imageContents(atPath: String) -> UIImage? func contentsOfDirectory(at url: URL) throws -> [URL] func contentsOfDirectory(atPath path: String) throws -> [String] func isDirectoryEmpty(at url: URL) -> Bool @@ -75,6 +78,10 @@ public extension FileManagerType { try protectFileOrFolder(at: path, fileProtectionType: .completeUntilFirstUserAuthentication) } + func write(data: Data, to url: URL) throws { + try write(data: data, to: url, options: []) + } + func enumerator(at url: URL, includingPropertiesForKeys keys: [URLResourceKey]?) -> FileManager.DirectoryEnumerator? { return enumerator(at: url, includingPropertiesForKeys: keys, options: [], errorHandler: nil) } @@ -257,6 +264,10 @@ public class SessionFileManager: FileManagerType { .path } + public func write(data: Data, to url: URL, options: Data.WritingOptions) throws { + try data.write(to: url, options: options) + } + public func write(data: Data, toTemporaryFileWithExtension fileExtension: String?) throws -> String? { let tempFilePath: String = temporaryFilePath(fileExtension: fileExtension) @@ -301,6 +312,10 @@ public class SessionFileManager: FileManagerType { return fileManager.contents(atPath: atPath) } + public func imageContents(atPath: String) -> UIImage? { + return UIImage(contentsOfFile: atPath) + } + public func contentsOfDirectory(at url: URL) throws -> [URL] { return try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift index 1e6e22dfc1..8fc20cfba9 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift @@ -283,7 +283,7 @@ public class ImageEditorModel { unitCropRect: CGRect, using dependencies: Dependencies ) -> UIImage? { - guard let srcImage = UIImage(contentsOfFile: imagePath) else { + guard let srcImage = dependencies[singleton: .fileManager].imageContents(atPath: imagePath) else { Log.error("[ImageEditorModel] Could not load image") return nil } diff --git a/TestUtilities/Mocked.swift b/TestUtilities/Mocked.swift index d29fa1487b..e34e59e844 100644 --- a/TestUtilities/Mocked.swift +++ b/TestUtilities/Mocked.swift @@ -168,3 +168,8 @@ extension FileProtectionType: Mocked { public static let any: FileProtectionType = .complete public static let mock: FileProtectionType = .complete } + +extension Data.WritingOptions: Mocked { + public static let any: Data.WritingOptions = .fileProtectionMask + public static let mock: Data.WritingOptions = .atomic +} diff --git a/_SharedTestUtilities/MockFileManager.swift b/_SharedTestUtilities/MockFileManager.swift index 473fad7b93..3105256dc2 100644 --- a/_SharedTestUtilities/MockFileManager.swift +++ b/_SharedTestUtilities/MockFileManager.swift @@ -1,6 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation +import UIKit.UIImage import SessionUtilitiesKit class MockFileManager: Mock, FileManagerType { @@ -28,6 +29,10 @@ class MockFileManager: Mock, FileManagerType { return mock(args: [fileExtension]) } + func write(data: Data, to url: URL, options: Data.WritingOptions) throws { + try mockThrowingNoReturn(args: [data, url, options]) + } + func write(data: Data, toTemporaryFileWithExtension fileExtension: String?) throws -> String? { return try mockThrowing(args: [data, fileExtension]) } @@ -55,6 +60,7 @@ class MockFileManager: Mock, FileManagerType { } func contents(atPath: String) -> Data? { return mock(args: [atPath]) } + func imageContents(atPath: String) -> UIImage? { return mock(args: [atPath]) } func contentsOfDirectory(at url: URL) throws -> [URL] { return try mockThrowing(args: [url]) } func contentsOfDirectory(atPath path: String) throws -> [String] { return try mockThrowing(args: [path]) } func isDirectoryEmpty(at url: URL) -> Bool { return mock(args: [url]) } @@ -110,6 +116,7 @@ extension Mock where T == FileManagerType { self.when { $0.appSharedDataDirectoryPath }.thenReturn("/test") self.when { try $0.ensureDirectoryExists(at: .any, fileProtectionType: .any) }.thenReturn(()) self.when { try $0.protectFileOrFolder(at: .any, fileProtectionType: .any) }.thenReturn(()) + self.when { try $0.write(data: .any, to: .any, options: .any) }.thenReturn(()) self.when { $0.fileExists(atPath: .any) }.thenReturn(false) self.when { $0.fileExists(atPath: .any, isDirectory: .any) }.thenReturn(false) self.when { $0.temporaryFilePath(fileExtension: .any) }.thenReturn("tmpFile") @@ -134,6 +141,7 @@ extension Mock where T == FileManagerType { }.thenReturn(nil) self.when { try $0.removeItem(atPath: .any) }.thenReturn(()) self.when { $0.contents(atPath: .any) }.thenReturn(Data([1, 2, 3])) + self.when { $0.imageContents(atPath: .any) }.thenReturn(UIImage(data: TestConstants.validImageData)) self.when { try $0.contentsOfDirectory(at: .any) }.thenReturn([]) self.when { try $0.contentsOfDirectory(atPath: .any) }.thenReturn([]) self.when { From 2ad95f467f4d04cb77dcaa56ff93266906203b73 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 10 Sep 2025 13:27:23 +1000 Subject: [PATCH 43/59] Updated MockGeneralCache and MockUserDefaults to use new Mockable --- .../Crypto/CryptoSMKSpec.swift | 10 +-- .../Jobs/DisplayPictureDownloadJobSpec.swift | 4 +- ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 22 +++-- .../LibSession/LibSessionGroupInfoSpec.swift | 4 +- .../LibSessionGroupMembersSpec.swift | 4 +- .../LibSession/LibSessionSpec.swift | 6 +- .../Crypto/CryptoOpenGroupAPISpec.swift | 10 +-- .../Open Groups/OpenGroupAPISpec.swift | 22 ++--- .../Open Groups/OpenGroupManagerSpec.swift | 82 +++++++++---------- .../MessageReceiverGroupsSpec.swift | 41 ++++++---- .../MessageSenderGroupsSpec.swift | 31 ++++--- .../MessageSenderSpec.swift | 4 +- .../Pollers/CommunityPollerManagerSpec.swift | 24 ++++-- .../Utilities/ExtensionHelperSpec.swift | 4 +- .../ThreadSettingsViewModelSpec.swift | 4 +- SessionTests/Database/DatabaseSpec.swift | 4 +- SessionTests/Onboarding/OnboardingSpec.swift | 52 ++++++------ TestUtilities/Mocked.swift | 2 + _SharedTestUtilities/MockGeneralCache.swift | 53 +++++++----- _SharedTestUtilities/MockUserDefaults.swift | 64 ++++++++++----- _SharedTestUtilities/TestDependencies.swift | 16 ---- 21 files changed, 245 insertions(+), 218 deletions(-) diff --git a/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift b/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift index 4376e41fca..3fb61c83df 100644 --- a/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift +++ b/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift @@ -8,17 +8,17 @@ import Nimble @testable import SessionMessagingKit -class CryptoSMKSpec: QuickSpec { +class CryptoSMKSpec: AsyncSpec { override class func spec() { // MARK: Configuration @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .crypto, in: dependencies) var crypto: Crypto! = Crypto(using: dependencies) - @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + @TestState var mockGeneralCache: MockGeneralCache! = .create() beforeEach { /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead - mockGeneralCache.defaultInitialSetup() + try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) } @@ -85,7 +85,7 @@ class CryptoSMKSpec: QuickSpec { // MARK: ---- throws an error if there is no ed25519 keyPair it("throws an error if there is no ed25519 keyPair") { - mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) + try await mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { try crypto.tryGenerate( @@ -120,7 +120,7 @@ class CryptoSMKSpec: QuickSpec { // MARK: ---- throws an error if there is no ed25519 keyPair it("throws an error if there is no ed25519 keyPair") { - mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) + try await mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { try crypto.tryGenerate( diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index 695fbd45fd..e277c2d386 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -67,11 +67,11 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { .thenReturn(nil) } ) - @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + @TestState var mockGeneralCache: MockGeneralCache! = .create() beforeEach { /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead - mockGeneralCache.defaultInitialSetup() + try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) mockLibSessionCache.defaultInitialSetup() diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index dba901e704..0fca648ddf 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -22,11 +22,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState(defaults: .appGroup, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults( - initialSetup: { defaults in - defaults.when { $0.bool(forKey: .any) }.thenReturn(true) - } - ) + @TestState var mockUserDefaults: MockUserDefaults! = .create() @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( initialSetup: { network in network.when { $0.networkStatus }.thenReturn(.singleValue(value: .connected)) @@ -82,7 +78,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { } ) @TestState var mockOGMCache: MockOGMCache! = MockOGMCache() - @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + @TestState var mockGeneralCache: MockGeneralCache! = .create() @TestState var job: Job! = Job(variant: .retrieveDefaultOpenGroupRooms) @TestState var error: Error? = nil @TestState var permanentFailure: Bool! = false @@ -90,7 +86,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { beforeEach { /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead - mockGeneralCache.defaultInitialSetup() + try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) mockOGMCache.when { $0.setDefaultRoomInfo(.any) }.thenReturn(()) @@ -103,13 +99,19 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) } + + try await mockUserDefaults.defaultInitialSetup() + try await mockUserDefaults.when { $0.bool(forKey: .any) }.thenReturn(true) + dependencies.set(defaults: .appGroup, to: mockUserDefaults) } // MARK: - a RetrieveDefaultOpenGroupRoomsJob describe("a RetrieveDefaultOpenGroupRoomsJob") { // MARK: -- defers the job if the main app is not running it("defers the job if the main app is not running") { - mockUserDefaults.when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) }.thenReturn(false) + try await mockUserDefaults + .when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) } + .thenReturn(false) RetrieveDefaultOpenGroupRoomsJob.run( job, @@ -125,7 +127,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { // MARK: -- does not defer the job when the main app is running it("does not defer the job when the main app is running") { - mockUserDefaults.when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) }.thenReturn(true) + try await mockUserDefaults + .when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) } + .thenReturn(true) RetrieveDefaultOpenGroupRoomsJob.run( job, diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift index 866fed58db..056f1c74e0 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift @@ -20,7 +20,7 @@ class LibSessionGroupInfoSpec: AsyncSpec { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) dependencies.forceSynchronous = true } - @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + @TestState var mockGeneralCache: MockGeneralCache! = .create() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), using: dependencies @@ -59,7 +59,7 @@ class LibSessionGroupInfoSpec: AsyncSpec { beforeEach { /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead - mockGeneralCache.defaultInitialSetup() + try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) try await mockStorage.perform(migrations: SNMessagingKit.migrations) diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift index cbcc225d19..fae995d1f9 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift @@ -19,7 +19,7 @@ class LibSessionGroupMembersSpec: AsyncSpec { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) dependencies.forceSynchronous = true } - @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + @TestState var mockGeneralCache: MockGeneralCache! = .create() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), using: dependencies @@ -58,7 +58,7 @@ class LibSessionGroupMembersSpec: AsyncSpec { beforeEach { /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead - mockGeneralCache.defaultInitialSetup() + try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) try await mockStorage.perform(migrations: SNMessagingKit.migrations) diff --git a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift index 25a984512e..a608dd8147 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift @@ -19,7 +19,7 @@ class LibSessionSpec: AsyncSpec { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) dependencies.forceSynchronous = true } - @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + @TestState var mockGeneralCache: MockGeneralCache! = .create() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), using: dependencies @@ -31,7 +31,7 @@ class LibSessionSpec: AsyncSpec { beforeEach { /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead - mockGeneralCache.defaultInitialSetup() + try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) try await mockCrypto @@ -320,7 +320,7 @@ class LibSessionSpec: AsyncSpec { // MARK: ---- throws when there is no user ed25519 keyPair it("throws when there is no user ed25519 keyPair") { var resultError: Error? = nil - mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) + try await mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) mockStorage.write { db in do { _ = try LibSession.createGroup( diff --git a/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift index 8169b6aebb..4e38d6797b 100644 --- a/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift @@ -8,17 +8,17 @@ import Nimble @testable import SessionMessagingKit -class CryptoOpenGroupAPISpec: QuickSpec { +class CryptoOpenGroupAPISpec: AsyncSpec { override class func spec() { // MARK: Configuration @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .crypto, in: dependencies) var crypto: Crypto! = Crypto(using: dependencies) - @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + @TestState var mockGeneralCache: MockGeneralCache! = .create() beforeEach { /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead - mockGeneralCache.defaultInitialSetup() + try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) } @@ -238,7 +238,7 @@ class CryptoOpenGroupAPISpec: QuickSpec { // MARK: ---- throws an error if there is no ed25519 keyPair it("throws an error if there is no ed25519 keyPair") { - mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) + try await mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { try crypto.tryGenerate( @@ -293,7 +293,7 @@ class CryptoOpenGroupAPISpec: QuickSpec { // MARK: ---- throws an error if there is no ed25519 keyPair it("throws an error if there is no ed25519 keyPair") { - mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) + try await mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { try crypto.tryGenerate( diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index 674c98d937..502024d185 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -21,14 +21,14 @@ class OpenGroupAPISpec: AsyncSpec { } @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork() @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() - @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + @TestState var mockGeneralCache: MockGeneralCache! = .create() @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() @TestState var disposables: [AnyCancellable]! = [] @TestState var error: Error? beforeEach { /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead - mockGeneralCache.defaultInitialSetup() + try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) mockLibSessionCache.defaultInitialSetup() @@ -996,7 +996,7 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ------ fails to sign if there is no ed25519SecretKey it("fails to sign if there is no ed25519SecretKey") { - mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) + try await mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { preparedRequest = try OpenGroupAPI.preparedSend( @@ -1023,7 +1023,7 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ------ fails to sign if there is ed25519Seed it("fails to sign if there is ed25519Seed") { - mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) + try await mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) expect { preparedRequest = try OpenGroupAPI.preparedSend( @@ -1110,7 +1110,7 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ------ fails to sign if there is no ed25519SecretKey it("fails to sign if there is no ed25519SecretKey") { - mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) + try await mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { preparedRequest = try OpenGroupAPI.preparedSend( @@ -1137,7 +1137,7 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ------ fails to sign if there is no ed25519Seed it("fails to sign if there is no ed25519Seed") { - mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) + try await mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) expect { preparedRequest = try OpenGroupAPI.preparedSend( @@ -1281,7 +1281,7 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ------ fails to sign if there is no ed25519SecretKey it("fails to sign if there is no ed25519SecretKey") { - mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) + try await mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { preparedRequest = try OpenGroupAPI.preparedMessageUpdate( @@ -1307,7 +1307,7 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ------ fails to sign if there is no ed25519Seed it("fails to sign if there is no ed25519Seed") { - mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) + try await mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) expect { preparedRequest = try OpenGroupAPI.preparedMessageUpdate( @@ -1391,7 +1391,7 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ------ fails to sign if there is no ed25519SecretKey it("fails to sign if there is no ed25519SecretKey") { - mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) + try await mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { preparedRequest = try OpenGroupAPI.preparedMessageUpdate( @@ -1417,7 +1417,7 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ------ fails to sign if there is no ed25519Seed it("fails to sign if there is no ed25519Seed") { - mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) + try await mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) expect { preparedRequest = try OpenGroupAPI.preparedMessageUpdate( @@ -2128,7 +2128,7 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ---- fails when there is no ed25519SecretKey it("fails when there is no ed25519SecretKey") { - mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) + try await mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { preparedRequest = try OpenGroupAPI.preparedRooms( diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index d80fffe27e..f9fd9fea61 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -145,18 +145,9 @@ class OpenGroupManagerSpec: AsyncSpec { } ) @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() - @TestState(defaults: .standard, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults( - initialSetup: { defaults in - defaults.when { $0.integer(forKey: .any) }.thenReturn(0) - defaults.when { $0.set(Int.any, forKey: .any) }.thenReturn(()) - } - ) - @TestState(defaults: .appGroup, in: dependencies) var mockAppGroupDefaults: MockUserDefaults! = MockUserDefaults( - initialSetup: { defaults in - defaults.when { $0.bool(forKey: .any) }.thenReturn(false) - } - ) - @TestState var mockGeneralCache: MockGeneralCache! = MockGeneralCache() + @TestState var mockUserDefaults: MockUserDefaults! = .create() + @TestState var mockAppGroupDefaults: MockUserDefaults! = .create() + @TestState var mockGeneralCache: MockGeneralCache! = .create() @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() @TestState var mockOGMCache: MockOGMCache! = MockOGMCache() @TestState var mockPoller: MockPoller! = .create() @@ -192,7 +183,7 @@ class OpenGroupManagerSpec: AsyncSpec { beforeEach { /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead - mockGeneralCache.defaultInitialSetup() + try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) mockLibSessionCache.defaultInitialSetup() @@ -270,6 +261,14 @@ class OpenGroupManagerSpec: AsyncSpec { try await mockCommunityPollerManager .when { $0.syncState } .thenReturn(CommunityPollerManagerSyncState()) + + try await mockUserDefaults.defaultInitialSetup() + try await mockUserDefaults.when { $0.integer(forKey: .any) }.thenReturn(0) + dependencies.set(defaults: .standard, to: mockUserDefaults) + + try await mockAppGroupDefaults.defaultInitialSetup() + try await mockAppGroupDefaults.when { $0.bool(forKey: .any) }.thenReturn(false) + dependencies.set(defaults: .appGroup, to: mockAppGroupDefaults) } // MARK: - an OpenGroupManager @@ -282,10 +281,8 @@ class OpenGroupManagerSpec: AsyncSpec { context("cache data") { // MARK: ---- defaults the time since last open to zero it("defaults the time since last open to zero") { - mockUserDefaults - .when { (defaults: inout any UserDefaultsType) -> Any? in - defaults.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) - } + try await mockUserDefaults + .when { $0.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) } .thenReturn(nil) expect(cache.getLastSuccessfulCommunityPollTimestamp()).to(equal(0)) @@ -293,10 +290,8 @@ class OpenGroupManagerSpec: AsyncSpec { // MARK: ---- returns the time since the last poll it("returns the time since the last poll") { - mockUserDefaults - .when { (defaults: inout any UserDefaultsType) -> Any? in - defaults.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) - } + try await mockUserDefaults + .when { $0.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) } .thenReturn(Date(timeIntervalSince1970: 1234567880)) dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) @@ -306,20 +301,16 @@ class OpenGroupManagerSpec: AsyncSpec { // MARK: ---- caches the time since the last poll in memory it("caches the time since the last poll in memory") { - mockUserDefaults - .when { (defaults: inout any UserDefaultsType) -> Any? in - defaults.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) - } + try await mockUserDefaults + .when { $0.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) } .thenReturn(Date(timeIntervalSince1970: 1234567770)) dependencies.dateNow = Date(timeIntervalSince1970: 1234567780) expect(cache.getLastSuccessfulCommunityPollTimestamp()) .to(equal(1234567770)) - mockUserDefaults - .when { (defaults: inout any UserDefaultsType) -> Any? in - defaults.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) - } + try await mockUserDefaults + .when { $0.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) } .thenReturn(Date(timeIntervalSince1970: 1234567890)) // Cached value shouldn't have been updated @@ -331,13 +322,14 @@ class OpenGroupManagerSpec: AsyncSpec { it("updates the time since the last poll in user defaults") { cache.setLastSuccessfulCommunityPollTimestamp(12345) - expect(mockUserDefaults) - .to(call(matchingParameters: .all) { + await mockUserDefaults + .verify { $0.set( Date(timeIntervalSince1970: 12345), forKey: UserDefaults.DateKey.lastOpen.rawValue ) - }) + } + .wasCalled(exactly: 1) } } @@ -416,8 +408,10 @@ class OpenGroupManagerSpec: AsyncSpec { context("when there is a thread for the room and the cache has a poller") { beforeEach { try await mockCommunityPollerManager - .when { await $0.serversBeingPolled } - .thenReturn(["http://127.0.0.1"]) + .when { await $0.syncState } + .thenReturn(CommunityPollerManagerSyncState( + serversBeingPolled: ["http://127.0.0.1"] + )) } // MARK: ------ for the no-scheme variant @@ -698,10 +692,8 @@ class OpenGroupManagerSpec: AsyncSpec { } .thenReturn(Network.BatchResponse.mockCapabilitiesAndRoomResponse) - mockUserDefaults - .when { (defaults: inout any UserDefaultsType) -> Any? in - defaults.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) - } + try await mockUserDefaults + .when { $0.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) } .thenReturn(Date(timeIntervalSince1970: 1234567890)) } @@ -848,10 +840,8 @@ class OpenGroupManagerSpec: AsyncSpec { } .thenReturn(MockNetwork.response(data: Data())) - mockUserDefaults - .when { (defaults: inout any UserDefaultsType) -> Any? in - defaults.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) - } + try await mockUserDefaults + .when { $0.object(forKey: UserDefaults.DateKey.lastOpen.rawValue) } .thenReturn(Date(timeIntervalSince1970: 1234567890)) } @@ -2539,7 +2529,7 @@ class OpenGroupManagerSpec: AsyncSpec { // MARK: ------ generates and unblinded key if the key belongs to the current user it("generates and unblinded key if the key belongs to the current user") { - mockGeneralCache.when { $0.ed25519Seed }.thenReturn([4, 5, 6]) + try await mockGeneralCache.when { $0.ed25519Seed }.thenReturn([4, 5, 6]) mockStorage.read { db in openGroupManager.isUserModeratorOrAdmin( db, @@ -2561,7 +2551,7 @@ class OpenGroupManagerSpec: AsyncSpec { context("when accessing the default rooms publisher") { // MARK: ---- starts a job to retrieve the default rooms if we have none it("starts a job to retrieve the default rooms if we have none") { - mockAppGroupDefaults + try await mockAppGroupDefaults .when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) } .thenReturn(true) mockStorage.write { db in @@ -2607,7 +2597,9 @@ class OpenGroupManagerSpec: AsyncSpec { // MARK: ---- does not start a job to retrieve the default rooms if we already have rooms it("does not start a job to retrieve the default rooms if we already have rooms") { - mockAppGroupDefaults.when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) }.thenReturn(true) + try await mockAppGroupDefaults + .when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) } + .thenReturn(true) cache.setDefaultRoomInfo([(room: OpenGroupAPI.Room.mock, openGroup: OpenGroup.mock)]) cache.defaultRoomsPublisher.sinkUntilComplete() diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index cf52de85b6..a7bf974c2c 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -297,7 +297,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ------ sends a local notification about the group invite it("sends a local notification about the group invite") { - fixture.mockUserDefaults + try await fixture.mockUserDefaults .when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) } .thenReturn(true) @@ -500,10 +500,10 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ------ and push notifications are disabled context("and push notifications are disabled") { beforeEach { - fixture.mockUserDefaults + try await fixture.mockUserDefaults .when { $0.string(forKey: UserDefaults.StringKey.deviceToken.rawValue) } .thenReturn(nil) - fixture.mockUserDefaults + try await fixture.mockUserDefaults .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } .thenReturn(false) } @@ -511,7 +511,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: -------- does not subscribe for push notifications it("does not subscribe for push notifications") { // Need to set `isUsingFullAPNs` to true to generate the `expectedRequest` - fixture.mockUserDefaults + try await fixture.mockUserDefaults .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } .thenReturn(true) fixture.mockStorage.write { db in @@ -538,7 +538,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { try ClosedGroup.filter(id: fixture.groupId.hexString).deleteAll(db) try SessionThread.filter(id: fixture.groupId.hexString).deleteAll(db) }! - fixture.mockUserDefaults + try await fixture.mockUserDefaults .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } .thenReturn(false) @@ -600,10 +600,10 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ------ and push notifications are enabled context("and push notifications are enabled") { beforeEach { - fixture.mockUserDefaults + try await fixture.mockUserDefaults .when { $0.string(forKey: UserDefaults.StringKey.deviceToken.rawValue) } .thenReturn(Data([5, 4, 3, 2, 1]).toHexString()) - fixture.mockUserDefaults + try await fixture.mockUserDefaults .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } .thenReturn(true) } @@ -2817,10 +2817,10 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ------ unsubscribes from push notifications it("unsubscribes from push notifications") { - fixture.mockUserDefaults + try await fixture.mockUserDefaults .when { $0.string(forKey: UserDefaults.StringKey.deviceToken.rawValue) } .thenReturn(Data([5, 4, 3, 2, 1]).toHexString()) - fixture.mockUserDefaults + try await fixture.mockUserDefaults .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } .thenReturn(true) @@ -3237,14 +3237,14 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { var mockNetwork: MockNetwork { mock(for: .network) { MockNetwork() } } var mockJobRunner: MockJobRunner { mock(for: .jobRunner) { MockJobRunner() } } var mockAppContext: MockAppContext { mock(for: .appContext) } - var mockUserDefaults: MockUserDefaults { mock(for: .standard) { MockUserDefaults() } } + var mockUserDefaults: MockUserDefaults { mock(defaults: .standard) } var mockCrypto: MockCrypto { mock(for: .crypto) } var mockKeychain: MockKeychain { mock(for: .keychain) { MockKeychain() } } var mockFileManager: MockFileManager { mock(for: .fileManager) { MockFileManager() } } var mockExtensionHelper: MockExtensionHelper { mock(for: .extensionHelper) { MockExtensionHelper() } } var mockGroupPollerManager: MockGroupPollerManager { mock(for: .groupPollerManager) } var mockNotificationsManager: MockNotificationsManager { mock(for: .notificationsManager) } - var mockGeneralCache: MockGeneralCache { mock(cache: .general) { MockGeneralCache() } } + var mockGeneralCache: MockGeneralCache { mock(cache: .general) } var mockLibSessionCache: MockLibSessionCache { mock(cache: .libSession) { MockLibSessionCache() } } var mockSnodeAPICache: MockSnodeAPICache { mock(cache: .snodeAPI) { MockSnodeAPICache() } } let mockPoller: MockPoller = .create() @@ -3522,14 +3522,14 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { await applyBaselineNetwork() await applyBaselineJobRunner() try await applyBaselineAppContext() - await applyBaselineUserDefaults() + try await applyBaselineUserDefaults() try await applyBaselineCrypto() await applyBaselineKeychain() await applyBaselineFileManager() await applyBaselineExtensionHelper() try await applyBaselineGroupPollerManager() try await applyBaselineNotificationsManager() - await applyBaselineGeneralCache() + try await applyBaselineGeneralCache() await applyBaselineLibSessionCache() await applyBaselineSnodeAPICache() try await applyBaselinePoller() @@ -3604,8 +3604,9 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { try await mockAppContext.when { $0.isMainApp }.thenReturn(false) } - private func applyBaselineUserDefaults() async { - mockUserDefaults.when { $0.string(forKey: .any) }.thenReturn(nil) + private func applyBaselineUserDefaults() async throws { + try await mockUserDefaults.defaultInitialSetup() + try await mockUserDefaults.when { $0.string(forKey: .any) }.thenReturn(nil) } private func applyBaselineCrypto() async throws { @@ -3681,9 +3682,13 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { try await mockNotificationsManager.defaultInitialSetup() } - private func applyBaselineGeneralCache() async { - mockGeneralCache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) + private func applyBaselineGeneralCache() async throws { + try await mockGeneralCache + .when { $0.sessionId } + .thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) + try await mockGeneralCache + .when { $0.ed25519SecretKey } + .thenReturn(Array(Data(hex: TestConstants.edSecretKey))) } private func applyBaselineLibSessionCache() async { diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift index afe0dfd5ec..687ffc13c4 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift @@ -32,12 +32,7 @@ class MessageSenderGroupsSpec: AsyncSpec { customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState(defaults: .standard, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults( - initialSetup: { userDefaults in - userDefaults.when { $0.string(forKey: .any) }.thenReturn(nil) - userDefaults.when { $0.set(String.any, forKey: .any) }.thenReturn(()) - } - ) + @TestState var mockUserDefaults: MockUserDefaults! = .create() @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( initialSetup: { jobRunner in jobRunner @@ -80,7 +75,7 @@ class MessageSenderGroupsSpec: AsyncSpec { .thenReturn(Data((0.. Any { 0 } + extension Int: Mocked { public static let any: Int = (Int.max - 123) public static let mock: Int = 0 diff --git a/_SharedTestUtilities/MockGeneralCache.swift b/_SharedTestUtilities/MockGeneralCache.swift index 11dc5867f7..16fb1517bb 100644 --- a/_SharedTestUtilities/MockGeneralCache.swift +++ b/_SharedTestUtilities/MockGeneralCache.swift @@ -2,52 +2,63 @@ import UIKit import SessionUtilitiesKit +import TestUtilities -class MockGeneralCache: Mock, GeneralCacheType { +class MockGeneralCache: GeneralCacheType, Mockable { + public var handler: MockHandler + + required init(handler: MockHandler) { + self.handler = handler + } + + required init(handlerForBuilder: any MockFunctionHandler) { + self.handler = MockHandler(forwardingHandler: handlerForBuilder) + } + var userExists: Bool { - get { return mock() } - set { mockNoReturn(args: [newValue]) } + get { return handler.mock() } + set { handler.mockNoReturn(args: [newValue]) } } var sessionId: SessionId { - get { return mock() } - set { mockNoReturn(args: [newValue]) } + get { return handler.mock() } + set { handler.mockNoReturn(args: [newValue]) } } var ed25519Seed: [UInt8] { - get { return mock() } - set { mockNoReturn(args: [newValue]) } + get { return handler.mock() } + set { handler.mockNoReturn(args: [newValue]) } } var ed25519SecretKey: [UInt8] { - get { return mock() } - set { mockNoReturn(args: [newValue]) } + get { return handler.mock() } + set { handler.mockNoReturn(args: [newValue]) } } var recentReactionTimestamps: [Int64] { - get { return (mock() ?? []) } - set { mockNoReturn(args: [newValue]) } + get { return (handler.mock() ?? []) } + set { handler.mockNoReturn(args: [newValue]) } } var contextualActionLookupMap: [Int: [String: [Int: Any]]] { - get { return (mock() ?? [:]) } - set { mockNoReturn(args: [newValue]) } + get { return (handler.mock() ?? [:]) } + set { handler.mockNoReturn(args: [newValue]) } } func setSecretKey(ed25519SecretKey: [UInt8]) { - mockNoReturn(args: [ed25519SecretKey]) + handler.mockNoReturn(args: [ed25519SecretKey]) } } // MARK: - Convenience -extension Mock where T == GeneralCacheType { - func defaultInitialSetup() { - self.when { $0.userExists }.thenReturn(true) - self.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) - self.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) - self.when { $0.setSecretKey(ed25519SecretKey: .any) }.thenReturn(()) - self +extension MockGeneralCache { + func defaultInitialSetup() async throws { + try await self.when { $0.userExists }.thenReturn(true) + try await self.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) + try await self.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) + try await self.when { $0.setSecretKey(ed25519SecretKey: .any) }.thenReturn(()) + try await self .when { $0.ed25519Seed } .thenReturn(Array(Array(Data(hex: TestConstants.edSecretKey)).prefix(upTo: 32))) } diff --git a/_SharedTestUtilities/MockUserDefaults.swift b/_SharedTestUtilities/MockUserDefaults.swift index 28ead9e1eb..145864b9ff 100644 --- a/_SharedTestUtilities/MockUserDefaults.swift +++ b/_SharedTestUtilities/MockUserDefaults.swift @@ -2,32 +2,54 @@ import Foundation import SessionUtilitiesKit +import TestUtilities -class MockUserDefaults: Mock, UserDefaultsType { - var allKeys: [String] { mock() } +class MockUserDefaults: UserDefaultsType, Mockable { + public var handler: MockHandler - func object(forKey defaultName: String) -> Any? { return mock(args: [defaultName]) } - func string(forKey defaultName: String) -> String? { return mock(args: [defaultName]) } - func array(forKey defaultName: String) -> [Any]? { return mock(args: [defaultName]) } - func dictionary(forKey defaultName: String) -> [String: Any]? { return mock(args: [defaultName]) } - func data(forKey defaultName: String) -> Data? { return mock(args: [defaultName]) } - func stringArray(forKey defaultName: String) -> [String]? { return mock(args: [defaultName]) } - func integer(forKey defaultName: String) -> Int { return (mock(args: [defaultName]) ?? 0) } - func float(forKey defaultName: String) -> Float { return (mock(args: [defaultName]) ?? 0) } - func double(forKey defaultName: String) -> Double { return (mock(args: [defaultName]) ?? 0) } - func bool(forKey defaultName: String) -> Bool { return (mock(args: [defaultName]) ?? false) } - func url(forKey defaultName: String) -> URL? { return mock(args: [defaultName]) } + required init(handler: MockHandler) { + self.handler = handler + } + + required init(handlerForBuilder: any MockFunctionHandler) { + self.handler = MockHandler(forwardingHandler: handlerForBuilder) + } + + var allKeys: [String] { handler.mock() } + + func object(forKey defaultName: String) -> Any? { return handler.mock(args: [defaultName]) } + func string(forKey defaultName: String) -> String? { return handler.mock(args: [defaultName]) } + func array(forKey defaultName: String) -> [Any]? { return handler.mock(args: [defaultName]) } + func dictionary(forKey defaultName: String) -> [String: Any]? { return handler.mock(args: [defaultName]) } + func data(forKey defaultName: String) -> Data? { return handler.mock(args: [defaultName]) } + func stringArray(forKey defaultName: String) -> [String]? { return handler.mock(args: [defaultName]) } + func integer(forKey defaultName: String) -> Int { return (handler.mock(args: [defaultName]) ?? 0) } + func float(forKey defaultName: String) -> Float { return (handler.mock(args: [defaultName]) ?? 0) } + func double(forKey defaultName: String) -> Double { return (handler.mock(args: [defaultName]) ?? 0) } + func bool(forKey defaultName: String) -> Bool { return (handler.mock(args: [defaultName]) ?? false) } + func url(forKey defaultName: String) -> URL? { return handler.mock(args: [defaultName]) } - func set(_ value: Any?, forKey defaultName: String) { mockNoReturn(args: [value, defaultName]) } - func set(_ value: Int, forKey defaultName: String) { mockNoReturn(args: [value, defaultName]) } - func set(_ value: Float, forKey defaultName: String) { mockNoReturn(args: [value, defaultName]) } - func set(_ value: Double, forKey defaultName: String) { mockNoReturn(args: [value, defaultName]) } - func set(_ value: Bool, forKey defaultName: String) { mockNoReturn(args: [value, defaultName]) } - func set(_ url: URL?, forKey defaultName: String) { mockNoReturn(args: [url, defaultName]) } + func set(_ value: Any?, forKey defaultName: String) { handler.mockNoReturn(args: [value, defaultName]) } + func set(_ value: Int, forKey defaultName: String) { handler.mockNoReturn(args: [value, defaultName]) } + func set(_ value: Float, forKey defaultName: String) { handler.mockNoReturn(args: [value, defaultName]) } + func set(_ value: Double, forKey defaultName: String) { handler.mockNoReturn(args: [value, defaultName]) } + func set(_ value: Bool, forKey defaultName: String) { handler.mockNoReturn(args: [value, defaultName]) } + func set(_ url: URL?, forKey defaultName: String) { handler.mockNoReturn(args: [url, defaultName]) } func removeObject(forKey defaultName: String) { - mockNoReturn(args: [defaultName]) + handler.mockNoReturn(args: [defaultName]) } - func removeAll() { mockNoReturn() } + func removeAll() { handler.mockNoReturn() } +} + +extension MockUserDefaults { + func defaultInitialSetup() async throws { + try await self.when { $0.set(anyAny(), forKey: .any) }.thenReturn(()) + try await self.when { $0.set(Int.any, forKey: .any) }.thenReturn(()) + try await self.when { $0.set(Float.any, forKey: .any) }.thenReturn(()) + try await self.when { $0.set(Double.any, forKey: .any) }.thenReturn(()) + try await self.when { $0.set(Bool.any, forKey: .any) }.thenReturn(()) + try await self.when { $0.set(URL.any, forKey: .any) }.thenReturn(()) + } } diff --git a/_SharedTestUtilities/TestDependencies.swift b/_SharedTestUtilities/TestDependencies.swift index 8dd9ef8965..7d073da0ea 100644 --- a/_SharedTestUtilities/TestDependencies.swift +++ b/_SharedTestUtilities/TestDependencies.swift @@ -273,20 +273,4 @@ internal extension TestState { return value }()) } - - init( - wrappedValue: @escaping @autoclosure () -> T?, - defaults: UserDefaultsConfig, - in dependenciesRetriever: @escaping @autoclosure () -> TestDependencies? - ) where T: UserDefaultsType { - self.init(wrappedValue: { - let dependencies: TestDependencies? = dependenciesRetriever() - let value: T? = wrappedValue() - (value as? DependenciesSettable)?.setDependencies(dependencies) - dependencies?[defaults: defaults] = value - (value as? (any InitialSetupable))?.performInitialSetup() - - return value - }()) - } } From 04fb9d471e9c2e3e7360f5370742c4379eebc6c3 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 10 Sep 2025 16:48:29 +1000 Subject: [PATCH 44/59] Fixed a few more tests and updated more code to the new mocking system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added a mechanism to ignore the type comparison when comparing against `any` values (for things like `MockEndpoint.any`) • Fixed an issue where `any` values weren't being compared recursively (so it would fail to match if an argument of an argument was a wildcard) • Updated MockNetwork to use Mockable • Updated MockExtensionHelper to be Mockable --- .../Models/MessageDeduplicationSpec.swift | 266 ++++++++++-------- .../Jobs/DisplayPictureDownloadJobSpec.swift | 51 ++-- ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 123 ++++---- .../LibSession/LibSessionGroupInfoSpec.swift | 63 +++-- .../LibSessionGroupMembersSpec.swift | 32 +-- .../Open Groups/OpenGroupAPISpec.swift | 19 +- .../Open Groups/OpenGroupManagerSpec.swift | 70 ++--- .../MessageReceiverGroupsSpec.swift | 151 +++++----- .../MessageSenderGroupsSpec.swift | 121 ++++---- .../NotificationsManagerSpec.swift | 9 +- .../Pollers/CommunityPollerManagerSpec.swift | 12 +- .../Utilities/ExtensionHelperSpec.swift | 30 +- .../_TestUtilities/MockExtensionHelper.swift | 55 ++-- .../Types/PreparedRequestSendingSpec.swift | 10 +- .../_TestUtilities/MockNetwork.swift | 43 +-- SessionTests/Onboarding/OnboardingSpec.swift | 89 +++--- TestUtilities/ArgumentDescribing.swift | 80 +++++- TestUtilities/MockFunction.swift | 131 +++++++-- TestUtilities/MockHandler.swift | 13 + TestUtilities/Mocked.swift | 6 + TestUtilities/Nimble/NimbleVerification.swift | 14 +- _SharedTestUtilities/MockUserDefaults.swift | 28 +- 22 files changed, 854 insertions(+), 562 deletions(-) diff --git a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift index 050f072c6d..1857d208a6 100644 --- a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift +++ b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift @@ -4,6 +4,7 @@ import Foundation import GRDB import SessionNetworkingKit import SessionUtilitiesKit +import TestUtilities import Quick import Nimble @@ -20,23 +21,7 @@ class MessageDeduplicationSpec: AsyncSpec { customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState(singleton: .extensionHelper, in: dependencies) var mockExtensionHelper: MockExtensionHelper! = MockExtensionHelper( - initialSetup: { helper in - helper.when { $0.deleteCache() }.thenReturn(()) - helper - .when { $0.dedupeRecordExists(threadId: .any, uniqueIdentifier: .any) } - .thenReturn(false) - helper - .when { try $0.createDedupeRecord(threadId: .any, uniqueIdentifier: .any) } - .thenReturn(()) - helper - .when { try $0.removeDedupeRecord(threadId: .any, uniqueIdentifier: .any) } - .thenReturn(()) - helper - .when { try $0.upsertLastClearedRecord(threadId: .any) } - .thenReturn(()) - } - ) + @TestState var mockExtensionHelper: MockExtensionHelper! = .create() @TestState var mockMessage: Message! = { let result: ReadReceipt = ReadReceipt(timestamps: [1]) result.sentTimestampMs = 12345678901234 @@ -46,6 +31,21 @@ class MessageDeduplicationSpec: AsyncSpec { beforeEach { try await mockStorage.perform(migrations: SNMessagingKit.migrations) + + try await mockExtensionHelper.when { $0.deleteCache() }.thenReturn(()) + try await mockExtensionHelper + .when { $0.dedupeRecordExists(threadId: .any, uniqueIdentifier: .any) } + .thenReturn(false) + try await mockExtensionHelper + .when { try $0.createDedupeRecord(threadId: .any, uniqueIdentifier: .any) } + .thenReturn(()) + try await mockExtensionHelper + .when { try $0.removeDedupeRecord(threadId: .any, uniqueIdentifier: .any) } + .thenReturn(()) + try await mockExtensionHelper + .when { try $0.upsertLastClearedRecord(threadId: .any) } + .thenReturn(()) + dependencies.set(singleton: .extensionHelper, to: mockExtensionHelper) } // MARK: - MessageDeduplication - Inserting @@ -77,9 +77,9 @@ class MessageDeduplicationSpec: AsyncSpec { expect(records?.first?.uniqueIdentifier).to(equal("testId")) expect(records?.first?.expirationTimestampSeconds).to(equal(expectedTimestamp)) expect(records?.first?.shouldDeleteWhenDeletingThread).to(beFalse()) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") - }) + await mockExtensionHelper + .verify { try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") } + .wasCalled(exactly: 1) } // MARK: ---- checks that it is not a duplicate record @@ -99,9 +99,9 @@ class MessageDeduplicationSpec: AsyncSpec { }.toNot(throwError()) } - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - $0.dedupeRecordExists(threadId: "testThreadId", uniqueIdentifier: "testId") - }) + await mockExtensionHelper + .verify { $0.dedupeRecordExists(threadId: "testThreadId", uniqueIdentifier: "testId") } + .wasCalled(exactly: 1) } // MARK: ---- creates a legacy record if needed @@ -122,9 +122,9 @@ class MessageDeduplicationSpec: AsyncSpec { }.toNot(throwError()) } - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - $0.dedupeRecordExists(threadId: "testThreadId", uniqueIdentifier: "testLegacyId") - }) + await mockExtensionHelper + .verify { $0.dedupeRecordExists(threadId: "testThreadId", uniqueIdentifier: "testLegacyId") } + .wasCalled(exactly: 1) } // MARK: ---- sets the shouldDeleteWhenDeletingThread flag correctly @@ -312,15 +312,17 @@ class MessageDeduplicationSpec: AsyncSpec { ) }.toNot(throwError()) } - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") - }) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.createDedupeRecord( - threadId: "testThreadId", - uniqueIdentifier: "LegacyRecord-1-12345678901234" - ) - }) + await mockExtensionHelper + .verify { try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") } + .wasCalled(exactly: 1) + await mockExtensionHelper + .verify { + try $0.createDedupeRecord( + threadId: "testThreadId", + uniqueIdentifier: "LegacyRecord-1-12345678901234" + ) + } + .wasCalled(exactly: 1) } // MARK: ---- does not create records for config ProcessedMessages @@ -346,14 +348,14 @@ class MessageDeduplicationSpec: AsyncSpec { let records: [MessageDeduplication]? = mockStorage .read { db in try MessageDeduplication.fetchAll(db) } expect(records).to(beEmpty()) - expect(mockExtensionHelper).toNot(call { - try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") - }) + await mockExtensionHelper + .verify { try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") } + .wasNotCalled() } // MARK: ---- throws if the message is a duplicate it("throws if the message is a duplicate") { - mockExtensionHelper + try await mockExtensionHelper .when { $0.dedupeRecordExists(threadId: .any, uniqueIdentifier: .any) } .thenReturn(true) @@ -376,7 +378,7 @@ class MessageDeduplicationSpec: AsyncSpec { // MARK: ---- throws if the message is a legacy duplicate it("throws if the message is a legacy duplicate") { - mockExtensionHelper + try await mockExtensionHelper .when { $0.dedupeRecordExists( threadId: "testThreadId", @@ -384,7 +386,7 @@ class MessageDeduplicationSpec: AsyncSpec { ) } .thenReturn(false) - mockExtensionHelper + try await mockExtensionHelper .when { $0.dedupeRecordExists( threadId: "testThreadId", @@ -412,7 +414,7 @@ class MessageDeduplicationSpec: AsyncSpec { // MARK: ---- throws if it fails to create the dedupe file it("throws if it fails to create the dedupe file") { - mockExtensionHelper + try await mockExtensionHelper .when { try $0.createDedupeRecord(threadId: .any, uniqueIdentifier: .any) } .thenThrow(TestError.mock) @@ -435,7 +437,7 @@ class MessageDeduplicationSpec: AsyncSpec { // MARK: ---- throws if it fails to create the legacy dedupe file it("throws if it fails to create the legacy dedupe file") { - mockExtensionHelper + try await mockExtensionHelper .when { try $0.createDedupeRecord( threadId: "testThreadId", @@ -491,9 +493,11 @@ class MessageDeduplicationSpec: AsyncSpec { expect(records?.first?.uniqueIdentifier).to(equal("12345-preOffer")) expect(records?.first?.expirationTimestampSeconds).to(equal(1234567891)) expect(records?.first?.shouldDeleteWhenDeletingThread).to(beFalse()) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "12345-preOffer") - }) + await mockExtensionHelper + .verify { + try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "12345-preOffer") + } + .wasCalled(exactly: 1) } // MARK: ---- inserts a generic record correctly @@ -523,9 +527,11 @@ class MessageDeduplicationSpec: AsyncSpec { expect(records?.first?.uniqueIdentifier).to(equal("12345")) expect(records?.first?.expirationTimestampSeconds).to(equal(1234567891)) expect(records?.first?.shouldDeleteWhenDeletingThread).to(beFalse()) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "12345") - }) + await mockExtensionHelper + .verify { + try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "12345") + } + .wasCalled(exactly: 1) } // MARK: ---- does nothing if no call message is provided @@ -546,9 +552,9 @@ class MessageDeduplicationSpec: AsyncSpec { let records: [MessageDeduplication]? = mockStorage .read { db in try MessageDeduplication.fetchAll(db) } expect(records?.count).to(equal(0)) - expect(mockExtensionHelper).toNot(call { - try $0.createDedupeRecord(threadId: .any, uniqueIdentifier: .any) - }) + await mockExtensionHelper + .verify { try $0.createDedupeRecord(threadId: .any, uniqueIdentifier: .any) } + .wasNotCalled() } } } @@ -581,9 +587,11 @@ class MessageDeduplicationSpec: AsyncSpec { await expect(mockStorage .read { db in try MessageDeduplication.fetchAll(db) }) .toEventually(beEmpty()) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.removeDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") - }) + await mockExtensionHelper + .verify { + try $0.removeDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") + } + .wasCalled(exactly: 1) } // MARK: ---- upserts the last cleared record @@ -610,9 +618,9 @@ class MessageDeduplicationSpec: AsyncSpec { await expect(mockStorage .read { db in try MessageDeduplication.fetchAll(db) }) .toEventually(beEmpty()) - await expect(mockExtensionHelper).toEventually(call(.exactly(times: 1), matchingParameters: .all) { - try $0.upsertLastClearedRecord(threadId: "testThreadId") - }) + await mockExtensionHelper + .verify { try $0.upsertLastClearedRecord(threadId: "testThreadId") } + .wasCalled(exactly: 1) } // MARK: ---- deletes multiple records @@ -645,12 +653,12 @@ class MessageDeduplicationSpec: AsyncSpec { await expect(mockStorage .read { db in try MessageDeduplication.fetchAll(db) }) .toEventually(beEmpty()) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.removeDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") - }) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.removeDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId2") - }) + await mockExtensionHelper + .verify { try $0.removeDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") } + .wasCalled(exactly: 1) + await mockExtensionHelper + .verify { try $0.removeDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId2") } + .wasCalled(exactly: 1) } // MARK: ---- leaves unrelated records @@ -687,9 +695,11 @@ class MessageDeduplicationSpec: AsyncSpec { expect((records?.map { $0.threadId }).map { Set($0) }).to(equal(["testThreadId2"])) expect((records?.map { $0.uniqueIdentifier }).map { Set($0) }) .to(equal(["testId2"])) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.removeDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") - }) + await mockExtensionHelper + .verify { + try $0.removeDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") + } + .wasCalled(exactly: 1) } // MARK: ---- leaves records which should not be deleted alongside the thread @@ -718,9 +728,9 @@ class MessageDeduplicationSpec: AsyncSpec { expect((records?.map { $0.threadId }).map { Set($0) }).to(equal(["testThreadId"])) expect((records?.map { $0.uniqueIdentifier }).map { Set($0) }) .to(equal(["testId"])) - expect(mockExtensionHelper).toNot(call { - try $0.removeDedupeRecord(threadId: .any, uniqueIdentifier: .any) - }) + await mockExtensionHelper + .verify { try $0.removeDedupeRecord(threadId: .any, uniqueIdentifier: .any) } + .wasNotCalled() } // MARK: ---- resets the expiration timestamp when failing to delete the file @@ -733,7 +743,7 @@ class MessageDeduplicationSpec: AsyncSpec { shouldDeleteWhenDeletingThread: true ).insert(db) } - mockExtensionHelper + try await mockExtensionHelper .when { try $0.removeDedupeRecord(threadId: .any, uniqueIdentifier: .any) } .thenThrow(TestError.mock) @@ -756,9 +766,11 @@ class MessageDeduplicationSpec: AsyncSpec { .to(equal(["testId"])) expect((records?.map { $0.expirationTimestampSeconds }).map { Set($0) }) .to(equal([0])) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.removeDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") - }) + await mockExtensionHelper + .verify { + try $0.removeDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") + } + .wasCalled(exactly: 1) } } } @@ -776,9 +788,9 @@ class MessageDeduplicationSpec: AsyncSpec { using: dependencies ) }.toNot(throwError()) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") - }) + await mockExtensionHelper + .verify { try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") } + .wasCalled(exactly: 1) } // MARK: ---- creates both the main file and a legacy file when needed @@ -791,15 +803,17 @@ class MessageDeduplicationSpec: AsyncSpec { using: dependencies ) }.toNot(throwError()) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") - }) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.createDedupeRecord( - threadId: "testThreadId", - uniqueIdentifier: "testLegacyId" - ) - }) + await mockExtensionHelper + .verify { try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") } + .wasCalled(exactly: 1) + await mockExtensionHelper + .verify { + try $0.createDedupeRecord( + threadId: "testThreadId", + uniqueIdentifier: "testLegacyId" + ) + } + .wasCalled(exactly: 1) } // MARK: ---- creates a file from a ProcessedMessage @@ -822,14 +836,14 @@ class MessageDeduplicationSpec: AsyncSpec { using: dependencies ) }.toNot(throwError()) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") - }) + await mockExtensionHelper + .verify { try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") } + .wasCalled(exactly: 1) } // MARK: ---- throws when it fails to create the file it("throws when it fails to create the file") { - mockExtensionHelper + try await mockExtensionHelper .when { try $0.createDedupeRecord(threadId: .any, uniqueIdentifier: .any) } .thenThrow(TestError.mock) @@ -844,7 +858,7 @@ class MessageDeduplicationSpec: AsyncSpec { // MARK: ---- throws when it fails to create the legacy file it("throws when it fails to create the legacy file") { - mockExtensionHelper + try await mockExtensionHelper .when { try $0.createDedupeRecord( threadId: "testThreadId", @@ -852,7 +866,7 @@ class MessageDeduplicationSpec: AsyncSpec { ) } .thenReturn(()) - mockExtensionHelper + try await mockExtensionHelper .when { try $0.createDedupeRecord( threadId: "testThreadId", @@ -869,15 +883,17 @@ class MessageDeduplicationSpec: AsyncSpec { using: dependencies ) }.to(throwError(TestError.mock)) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") - }) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.createDedupeRecord( - threadId: "testThreadId", - uniqueIdentifier: "testLegacyId" - ) - }) + await mockExtensionHelper + .verify { try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "testId") } + .wasCalled(exactly: 1) + await mockExtensionHelper + .verify { + try $0.createDedupeRecord( + threadId: "testThreadId", + uniqueIdentifier: "testLegacyId" + ) + } + .wasCalled(exactly: 1) } } @@ -898,9 +914,11 @@ class MessageDeduplicationSpec: AsyncSpec { ) }.toNot(throwError()) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "12345-preOffer") - }) + await mockExtensionHelper + .verify { + try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "12345-preOffer") + } + .wasCalled(exactly: 1) } // MARK: ---- creates a generic file correctly @@ -918,9 +936,9 @@ class MessageDeduplicationSpec: AsyncSpec { ) }.toNot(throwError()) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "12345") - }) + await mockExtensionHelper + .verify { try $0.createDedupeRecord(threadId: "testThreadId", uniqueIdentifier: "12345") } + .wasCalled(exactly: 1) } // MARK: ---- creates a files for the correct call message kinds @@ -928,8 +946,8 @@ class MessageDeduplicationSpec: AsyncSpec { var resultIdentifiers: [String] = [] var resultKinds: [CallMessage.Kind] = [] - CallMessage.Kind.allCases.forEach { kind in - mockExtensionHelper + for kind in CallMessage.Kind.allCases { + try await mockExtensionHelper .when { try $0.createDedupeRecord(threadId: .any, uniqueIdentifier: .any) } .then { args in guard let identifier: String = args[test: 1] as? String else { return } @@ -962,7 +980,7 @@ class MessageDeduplicationSpec: AsyncSpec { var resultIdentifiers: [String] = [] var resultStates: [CallMessage.MessageInfo.State] = [] - CallMessage.MessageInfo.State.allCases.forEach { state in + for state in CallMessage.MessageInfo.State.allCases { let message: CallMessage = CallMessage( uuid: "12345", kind: .answer, @@ -970,7 +988,7 @@ class MessageDeduplicationSpec: AsyncSpec { sentTimestampMs: 1234567890 ) message.state = state - mockExtensionHelper + try await mockExtensionHelper .when { try $0.createDedupeRecord(threadId: .any, uniqueIdentifier: .any) } .then { args in guard let identifier: String = args[test: 1] as? String else { return } @@ -1003,9 +1021,9 @@ class MessageDeduplicationSpec: AsyncSpec { ) }.toNot(throwError()) - expect(mockExtensionHelper).toNot(call { - try $0.createDedupeRecord(threadId: .any, uniqueIdentifier: .any) - }) + await mockExtensionHelper + .verify { try $0.createDedupeRecord(threadId: .any, uniqueIdentifier: .any) } + .wasNotCalled() } } } @@ -1061,7 +1079,7 @@ class MessageDeduplicationSpec: AsyncSpec { // MARK: ---- throws when the message is a duplicate it("throws when the message is a duplicate") { - mockExtensionHelper + try await mockExtensionHelper .when { $0.dedupeRecordExists(threadId: .any, uniqueIdentifier: .any) } .thenReturn(true) @@ -1076,7 +1094,7 @@ class MessageDeduplicationSpec: AsyncSpec { // MARK: ---- throws when the message is a legacy duplicate it("throws when the message is a legacy duplicate") { - mockExtensionHelper + try await mockExtensionHelper .when { $0.dedupeRecordExists( threadId: "testThreadId", @@ -1084,7 +1102,7 @@ class MessageDeduplicationSpec: AsyncSpec { ) } .thenReturn(false) - mockExtensionHelper + try await mockExtensionHelper .when { $0.dedupeRecordExists( threadId: "testThreadId", @@ -1101,12 +1119,12 @@ class MessageDeduplicationSpec: AsyncSpec { using: dependencies ) }.to(throwError(MessageReceiverError.duplicateMessage)) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - $0.dedupeRecordExists(threadId: "testThreadId", uniqueIdentifier: "testId") - }) - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - $0.dedupeRecordExists(threadId: "testThreadId", uniqueIdentifier: "testLegacyId") - }) + await mockExtensionHelper + .verify { $0.dedupeRecordExists(threadId: "testThreadId", uniqueIdentifier: "testId") } + .wasCalled(exactly: 1) + await mockExtensionHelper + .verify { $0.dedupeRecordExists(threadId: "testThreadId", uniqueIdentifier: "testLegacyId") } + .wasCalled(exactly: 1) } } @@ -1141,7 +1159,7 @@ class MessageDeduplicationSpec: AsyncSpec { // MARK: ---- throws when the call message is a duplicate it("throws when the call message is a duplicate") { - mockExtensionHelper + try await mockExtensionHelper .when { $0.dedupeRecordExists(threadId: .any, uniqueIdentifier: .any) } .thenReturn(true) diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index e277c2d386..545aa4839f 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -40,22 +40,7 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { "673120e153a5cb6b869380744d493068ebc418266d6596d728cfc60b30662a089376" + "f2761e3bb6ee837a26b24b5" ) - @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( - initialSetup: { network in - network - .when { - $0.send( - endpoint: MockEndpoint.any, - destination: .any, - body: .any, - category: .any, - requestTimeout: .any, - overallTimeout: .any - ) - } - .thenReturn(MockNetwork.response(data: encryptedData)) - } - ) + @TestState var mockNetwork: MockNetwork! = .create() @TestState(singleton: .fileManager, in: dependencies) var mockFileManager: MockFileManager! = MockFileManager( initialSetup: { $0.defaultInitialSetup() } ) @@ -104,6 +89,20 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { try await mockCrypto .when { $0.generate(.signatureBlind15(message: .any, serverPublicKey: .any, ed25519SecretKey: .any)) } .thenReturn("TestSogsSignature".bytes) + + try await mockNetwork + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } + .thenReturn(MockNetwork.response(data: encryptedData)) + dependencies.set(singleton: .network, to: mockNetwork) } // MARK: - a DisplayPictureDownloadJob @@ -510,9 +509,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { using: dependencies ) - expect(mockNetwork) - .to(call(.exactly(times: 1), matchingParameters: .all) { network in - network.send( + await mockNetwork + .verify { + $0.send( endpoint: expectedRequest.endpoint, destination: expectedRequest.destination, body: expectedRequest.body, @@ -520,7 +519,8 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { requestTimeout: expectedRequest.requestTimeout, overallTimeout: expectedRequest.overallTimeout ) - }) + } + .wasCalled(exactly: 1) } // MARK: -- generates a SOGS download request correctly @@ -575,9 +575,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { using: dependencies ) - expect(mockNetwork) - .to(call(.exactly(times: 1), matchingParameters: .all) { network in - network.send( + await mockNetwork + .verify { + $0.send( endpoint: expectedRequest.endpoint, destination: expectedRequest.destination, body: expectedRequest.body, @@ -585,7 +585,8 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { requestTimeout: expectedRequest.requestTimeout, overallTimeout: expectedRequest.overallTimeout ) - }) + } + .wasCalled(exactly: 1) } // MARK: -- checking if a downloaded display picture is valid @@ -1092,7 +1093,7 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { ) // SOGS doesn't encrypt it's images so replace the encrypted mock response - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index 0fca648ddf..b2d7aba9d2 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -2,6 +2,7 @@ import Foundation import GRDB +import TestUtilities import Quick import Nimble @@ -23,47 +24,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { using: dependencies ) @TestState var mockUserDefaults: MockUserDefaults! = .create() - @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( - initialSetup: { network in - network.when { $0.networkStatus }.thenReturn(.singleValue(value: .connected)) - network - .when { - $0.send( - endpoint: MockEndpoint.any, - destination: .any, - body: .any, - category: .any, - requestTimeout: .any, - overallTimeout: .any - ) - } - .thenReturn( - MockNetwork.batchResponseData( - with: [ - ( - OpenGroupAPI.Endpoint.capabilities, - OpenGroupAPI.Capabilities(capabilities: [.blind, .reactions]).batchSubResponse() - ), - ( - OpenGroupAPI.Endpoint.rooms, - [ - OpenGroupAPI.Room.mock.with( - token: "testRoom", - name: "TestRoomName" - ), - OpenGroupAPI.Room.mock.with( - token: "testRoom2", - name: "TestRoomName2", - infoUpdates: 12, - imageId: "12" - ) - ].batchSubResponse() - ) - ] - ) - ) - } - ) + @TestState var mockNetwork: MockNetwork! = .create() @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( initialSetup: { jobRunner in jobRunner @@ -103,6 +64,45 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { try await mockUserDefaults.defaultInitialSetup() try await mockUserDefaults.when { $0.bool(forKey: .any) }.thenReturn(true) dependencies.set(defaults: .appGroup, to: mockUserDefaults) + + try await mockNetwork.when { $0.networkStatus }.thenReturn(.singleValue(value: .connected)) + try await mockNetwork + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } + .thenReturn( + MockNetwork.batchResponseData( + with: [ + ( + OpenGroupAPI.Endpoint.capabilities, + OpenGroupAPI.Capabilities(capabilities: [.blind, .reactions]).batchSubResponse() + ), + ( + OpenGroupAPI.Endpoint.rooms, + [ + OpenGroupAPI.Room.mock.with( + token: "testRoom", + name: "TestRoomName" + ), + OpenGroupAPI.Room.mock.with( + token: "testRoom2", + name: "TestRoomName2", + infoUpdates: 12, + imageId: "12" + ) + ].batchSubResponse() + ) + ] + ) + ) + dependencies.set(singleton: .network, to: mockNetwork) } // MARK: - a RetrieveDefaultOpenGroupRoomsJob @@ -188,7 +188,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { // MARK: -- creates an inactive entry in the database if one does not exist it("creates an inactive entry in the database if one does not exist") { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -221,7 +221,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { // MARK: -- does not create a new entry if one already exists it("does not create a new entry if one already exists") { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -302,9 +302,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { using: dependencies ) - expect(mockNetwork) - .to(call { network in - network.send( + await mockNetwork + .verify { + $0.send( endpoint: OpenGroupAPI.Endpoint.sequence, destination: expectedRequest.destination, body: expectedRequest.body, @@ -312,12 +312,13 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { requestTimeout: expectedRequest.requestTimeout, overallTimeout: expectedRequest.overallTimeout ) - }) + } + .wasCalled(exactly: 1) } // MARK: -- permanently fails if it gets an error it("permanently fails if it gets an error") { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -344,16 +345,18 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { expect(error).to(matchError(NetworkError.parsingFailed)) expect(permanentFailure).to(beTrue()) - expect(mockNetwork).to(call(.exactly(times: 1)) { network in - network.send( - endpoint: MockEndpoint.any, - destination: .any, - body: .any, - category: .any, - requestTimeout: .any, - overallTimeout: .any - ) - }) + await mockNetwork + .verify { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } + .wasCalled(exactly: 1) } // MARK: -- stores the updated capabilities @@ -415,7 +418,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { ) .insert(db) } - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -552,7 +555,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { // MARK: -- does not schedule a display picture download if there is no imageId it("does not schedule a display picture download if there is no imageId") { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift index 056f1c74e0..c9a022cf9f 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift @@ -5,6 +5,7 @@ import GRDB import SessionUtil import SessionUtilitiesKit import SessionNetworkingKit +import TestUtilities import Quick import Nimble @@ -25,22 +26,7 @@ class LibSessionGroupInfoSpec: AsyncSpec { customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( - initialSetup: { network in - network - .when { - $0.send( - endpoint: MockEndpoint.any, - destination: .any, - body: .any, - category: .any, - requestTimeout: .any, - overallTimeout: .any - ) - } - .thenReturn(MockNetwork.response(data: Data([1, 2, 3]))) - } - ) + @TestState var mockNetwork: MockNetwork! = .create() @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( initialSetup: { jobRunner in jobRunner @@ -94,6 +80,20 @@ class LibSessionGroupInfoSpec: AsyncSpec { ) mockLibSessionCache.when { $0.configNeedsDump(.any) }.thenReturn(true) dependencies.set(cache: .libSession, to: mockLibSessionCache) + + try await mockNetwork + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } + .thenReturn(MockNetwork.response(data: Data([1, 2, 3]))) + dependencies.set(singleton: .network, to: mockNetwork) } // MARK: - LibSessionGroupInfo @@ -898,9 +898,9 @@ class LibSessionGroupInfoSpec: AsyncSpec { ) } - expect(mockNetwork) - .to(call(.exactly(times: 1), matchingParameters: .all) { network in - network.send( + await mockNetwork + .verify { + $0.send( endpoint: SnodeAPI.Endpoint.deleteMessages, destination: .randomSnode(swarmPublicKey: createGroupOutput.groupSessionId.hexString), body: try! JSONEncoder(using: dependencies).encode( @@ -917,7 +917,8 @@ class LibSessionGroupInfoSpec: AsyncSpec { requestTimeout: Network.defaultTimeout, overallTimeout: nil ) - }) + } + .wasCalled(exactly: 1) } // MARK: ---- does not delete from the server if there is no server hash @@ -974,16 +975,18 @@ class LibSessionGroupInfoSpec: AsyncSpec { } expect(result?.count).to(equal(1)) expect(result?.map { $0.variant }).to(equal([.standardIncomingDeleted])) - expect(mockNetwork).toNot(call { network in - network.send( - endpoint: MockEndpoint.any, - destination: .any, - body: .any, - category: .any, - requestTimeout: .any, - overallTimeout: .any - ) - }) + await mockNetwork + .verify { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } + .wasNotCalled() } } } diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift index fae995d1f9..e79931e31f 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift @@ -4,6 +4,7 @@ import Foundation import GRDB import SessionUtil import SessionUtilitiesKit +import TestUtilities import Quick import Nimble @@ -24,22 +25,7 @@ class LibSessionGroupMembersSpec: AsyncSpec { customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( - initialSetup: { network in - network - .when { - $0.send( - endpoint: MockEndpoint.any, - destination: .any, - body: .any, - category: .any, - requestTimeout: .any, - overallTimeout: .any - ) - } - .thenReturn(MockNetwork.response(data: Data([1, 2, 3]))) - } - ) + @TestState var mockNetwork: MockNetwork! = .create() @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( initialSetup: { jobRunner in jobRunner @@ -92,6 +78,20 @@ class LibSessionGroupMembersSpec: AsyncSpec { ] ) dependencies.set(cache: .libSession, to: mockLibSessionCache) + + try await mockNetwork + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } + .thenReturn(MockNetwork.response(data: Data([1, 2, 3]))) + dependencies.set(singleton: .network, to: mockNetwork) } // MARK: - LibSessionGroupMembers diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index 502024d185..d8288cb12e 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -5,6 +5,7 @@ import Combine import GRDB import SessionNetworkingKit import SessionUtilitiesKit +import TestUtilities import Quick import Nimble @@ -19,7 +20,7 @@ class OpenGroupAPISpec: AsyncSpec { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) dependencies.forceSynchronous = true } - @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork() + @TestState var mockNetwork: MockNetwork! = .create() @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() @TestState var mockGeneralCache: MockGeneralCache! = .create() @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() @@ -74,6 +75,8 @@ class OpenGroupAPISpec: AsyncSpec { try await mockCrypto .when { $0.generate(.x25519(ed25519Seckey: .any)) } .thenReturn(Array(Data(hex: TestConstants.privateKey))) + + dependencies.set(singleton: .network, to: mockNetwork) } // MARK: - an OpenGroupAPI @@ -632,7 +635,7 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ---- processes a valid response correctly it("processes a valid response correctly") { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -677,7 +680,7 @@ class OpenGroupAPISpec: AsyncSpec { context("and given an invalid response") { // MARK: ------ errors when not given a room response it("errors when not given a room response") { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -720,7 +723,7 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ------ errors when not given a capabilities response it("errors when not given a capabilities response") { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -798,7 +801,7 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ---- processes a valid response correctly it("processes a valid response correctly") { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -842,7 +845,7 @@ class OpenGroupAPISpec: AsyncSpec { context("and given an invalid response") { // MARK: ------ errors when not given a room response it("errors when not given a room response") { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -892,7 +895,7 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ------ errors when not given a capabilities response it("errors when not given a capabilities response") { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -2296,7 +2299,7 @@ class OpenGroupAPISpec: AsyncSpec { @TestState var preparedRequest: Network.PreparedRequest<[OpenGroupAPI.Room]>? beforeEach { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index f9fd9fea61..86cde3afe8 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -126,24 +126,7 @@ class OpenGroupManagerSpec: AsyncSpec { .thenReturn([:]) } ) - @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( - initialSetup: { network in - network.when { $0.networkStatus }.thenReturn(.singleValue(value: .connected)) - network - .when { - $0.send( - endpoint: MockEndpoint.any, - destination: .any, - body: .any, - category: .any, - requestTimeout: .any, - overallTimeout: .any - ) - } - .thenReturn(MockNetwork.errorResponse()) - network.when { $0.syncState }.thenReturn(NetworkSyncState(isSuspended: false)) - } - ) + @TestState var mockNetwork: MockNetwork! = .create() @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() @TestState var mockUserDefaults: MockUserDefaults! = .create() @TestState var mockAppGroupDefaults: MockUserDefaults! = .create() @@ -269,6 +252,22 @@ class OpenGroupManagerSpec: AsyncSpec { try await mockAppGroupDefaults.defaultInitialSetup() try await mockAppGroupDefaults.when { $0.bool(forKey: .any) }.thenReturn(false) dependencies.set(defaults: .appGroup, to: mockAppGroupDefaults) + + try await mockNetwork.when { $0.networkStatus }.thenReturn(.singleValue(value: .connected)) + try await mockNetwork + .when { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } + .thenReturn(MockNetwork.errorResponse()) + try await mockNetwork.when { $0.syncState }.thenReturn(NetworkSyncState(isSuspended: false)) + dependencies.set(singleton: .network, to: mockNetwork) } // MARK: - an OpenGroupManager @@ -679,7 +678,7 @@ class OpenGroupManagerSpec: AsyncSpec { try OpenGroup.deleteAll(db) } - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -827,7 +826,7 @@ class OpenGroupManagerSpec: AsyncSpec { // MARK: ---- with an invalid response context("with an invalid response") { beforeEach { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -2582,9 +2581,9 @@ class OpenGroupManagerSpec: AsyncSpec { } cache.defaultRoomsPublisher.sinkUntilComplete() - expect(mockNetwork) - .to(call { network in - network.send( + await mockNetwork + .verify { + $0.send( endpoint: OpenGroupAPI.Endpoint.sequence, destination: expectedRequest.destination, body: expectedRequest.body, @@ -2592,7 +2591,8 @@ class OpenGroupManagerSpec: AsyncSpec { requestTimeout: expectedRequest.requestTimeout, overallTimeout: expectedRequest.overallTimeout ) - }) + } + .wasCalled(exactly: 1) } // MARK: ---- does not start a job to retrieve the default rooms if we already have rooms @@ -2603,16 +2603,18 @@ class OpenGroupManagerSpec: AsyncSpec { cache.setDefaultRoomInfo([(room: OpenGroupAPI.Room.mock, openGroup: OpenGroup.mock)]) cache.defaultRoomsPublisher.sinkUntilComplete() - expect(mockNetwork).toNot(call { - $0.send( - endpoint: MockEndpoint.any, - destination: .any, - body: .any, - category: .any, - requestTimeout: .any, - overallTimeout: .any - ) - }) + await mockNetwork + .verify { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } + .wasNotCalled() } } } diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index a7bf974c2c..7a8097315b 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -554,46 +554,48 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockNetwork).toNot(call { network in - network.send( - endpoint: PushNotificationAPI.Endpoint.subscribe, - destination: .server( - method: .post, - server: PushNotificationAPI.server, - queryParameters: [:], - headers: [:], - x25519PublicKey: PushNotificationAPI.serverPublicKey - ), - body: try! JSONEncoder(using: fixture.dependencies).encode( - PushNotificationAPI.SubscribeRequest( - subscriptions: [ - PushNotificationAPI.SubscribeRequest.Subscription( - namespaces: [ - .groupMessages, - .configGroupKeys, - .configGroupInfo, - .configGroupMembers, - .revokedRetrievableGroupMessages - ], - includeMessageData: true, - serviceInfo: PushNotificationAPI.ServiceInfo( - token: Data([5, 4, 3, 2, 1]).toHexString() - ), - notificationsEncryptionKey: Data([1, 2, 3]), - authMethod: try! Authentication.with( - swarmPublicKey: fixture.groupId.hexString, - using: fixture.dependencies - ), - timestamp: 1234567890 - ) - ] - ) - ), - category: .standard, - requestTimeout: Network.defaultTimeout, - overallTimeout: nil - ) - }) + await fixture.mockNetwork + .verify { + $0.send( + endpoint: PushNotificationAPI.Endpoint.subscribe, + destination: .server( + method: .post, + server: PushNotificationAPI.server, + queryParameters: [:], + headers: [:], + x25519PublicKey: PushNotificationAPI.serverPublicKey + ), + body: try! JSONEncoder(using: fixture.dependencies).encode( + PushNotificationAPI.SubscribeRequest( + subscriptions: [ + PushNotificationAPI.SubscribeRequest.Subscription( + namespaces: [ + .groupMessages, + .configGroupKeys, + .configGroupInfo, + .configGroupMembers, + .revokedRetrievableGroupMessages + ], + includeMessageData: true, + serviceInfo: PushNotificationAPI.ServiceInfo( + token: Data([5, 4, 3, 2, 1]).toHexString() + ), + notificationsEncryptionKey: Data([1, 2, 3]), + authMethod: try! Authentication.with( + swarmPublicKey: fixture.groupId.hexString, + using: fixture.dependencies + ), + timestamp: 1234567890 + ) + ] + ) + ), + category: .standard, + requestTimeout: Network.defaultTimeout, + overallTimeout: nil + ) + } + .wasNotCalled() } } @@ -647,9 +649,9 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockNetwork) - .to(call(.exactly(times: 1), matchingParameters: .all) { network in - network.send( + await fixture.mockNetwork + .verify { + $0.send( endpoint: PushNotificationAPI.Endpoint.subscribe, destination: .server( method: .post, @@ -687,7 +689,8 @@ class MessageReceiverGroupsSpec: AsyncSpec { requestTimeout: Network.defaultTimeout, overallTimeout: nil ) - }) + } + .wasCalled(exactly: 1) } } } @@ -2568,9 +2571,9 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockNetwork) - .to(call(.exactly(times: 1), matchingParameters: .all) { network in - network.send( + await fixture.mockNetwork + .verify { + $0.send( endpoint: SnodeAPI.Endpoint.deleteMessages, destination: preparedRequest.destination, body: preparedRequest.body, @@ -2578,7 +2581,8 @@ class MessageReceiverGroupsSpec: AsyncSpec { requestTimeout: preparedRequest.requestTimeout, overallTimeout: preparedRequest.overallTimeout ) - }) + } + .wasCalled(exactly: 1) } } @@ -2606,16 +2610,18 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockNetwork).toNot(call { network in - network.send( - endpoint: MockEndpoint.any, - destination: .any, - body: .any, - category: .any, - requestTimeout: .any, - overallTimeout: .any - ) - }) + await fixture.mockNetwork + .verify { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } + .wasNotCalled() } } } @@ -2833,9 +2839,9 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockNetwork) - .to(call(.exactly(times: 1), matchingParameters: .all) { network in - network.send( + await fixture.mockNetwork + .verify { + $0.send( endpoint: PushNotificationAPI.Endpoint.unsubscribe, destination: .server( method: .post, @@ -2864,7 +2870,8 @@ class MessageReceiverGroupsSpec: AsyncSpec { requestTimeout: Network.defaultTimeout, overallTimeout: nil ) - }) + } + .wasCalled(exactly: 1) } // MARK: ---- and the group is an invitation @@ -3234,14 +3241,14 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { ) } } - var mockNetwork: MockNetwork { mock(for: .network) { MockNetwork() } } + var mockNetwork: MockNetwork { mock(for: .network) } var mockJobRunner: MockJobRunner { mock(for: .jobRunner) { MockJobRunner() } } var mockAppContext: MockAppContext { mock(for: .appContext) } var mockUserDefaults: MockUserDefaults { mock(defaults: .standard) } var mockCrypto: MockCrypto { mock(for: .crypto) } var mockKeychain: MockKeychain { mock(for: .keychain) { MockKeychain() } } var mockFileManager: MockFileManager { mock(for: .fileManager) { MockFileManager() } } - var mockExtensionHelper: MockExtensionHelper { mock(for: .extensionHelper) { MockExtensionHelper() } } + var mockExtensionHelper: MockExtensionHelper { mock(for: .extensionHelper) } var mockGroupPollerManager: MockGroupPollerManager { mock(for: .groupPollerManager) } var mockNotificationsManager: MockNotificationsManager { mock(for: .notificationsManager) } var mockGeneralCache: MockGeneralCache { mock(cache: .general) } @@ -3519,14 +3526,14 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { private func applyBaselineStubs() async throws { try await applyBaselineStorage() - await applyBaselineNetwork() + try await applyBaselineNetwork() await applyBaselineJobRunner() try await applyBaselineAppContext() try await applyBaselineUserDefaults() try await applyBaselineCrypto() await applyBaselineKeychain() await applyBaselineFileManager() - await applyBaselineExtensionHelper() + try await applyBaselineExtensionHelper() try await applyBaselineGroupPollerManager() try await applyBaselineNotificationsManager() try await applyBaselineGeneralCache() @@ -3550,8 +3557,8 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { } } - private func applyBaselineNetwork() async { - mockNetwork + private func applyBaselineNetwork() async throws { + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -3563,7 +3570,7 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { ) } .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1"))) - mockNetwork + try await mockNetwork .when { try await $0.getSwarm(for: .any) } .thenReturn([ LibSession.Snode( @@ -3662,11 +3669,11 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { mockFileManager.defaultInitialSetup() } - private func applyBaselineExtensionHelper() async { - mockExtensionHelper + private func applyBaselineExtensionHelper() async throws { + try await mockExtensionHelper .when { try $0.removeDedupeRecord(threadId: .any, uniqueIdentifier: .any) } .thenReturn(()) - mockExtensionHelper + try await mockExtensionHelper .when { try $0.upsertLastClearedRecord(threadId: .any) } .thenReturn(()) } diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift index 687ffc13c4..2c09bbf392 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift @@ -46,7 +46,7 @@ class MessageSenderGroupsSpec: AsyncSpec { .thenReturn(nil) } ) - @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork() + @TestState var mockNetwork: MockNetwork! = .create() @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() @TestState(singleton: .keychain, in: dependencies) var mockKeychain: MockKeychain! = MockKeychain( initialSetup: { keychain in @@ -140,8 +140,8 @@ class MessageSenderGroupsSpec: AsyncSpec { mockSnodeAPICache.defaultInitialSetup() dependencies.set(cache: .snodeAPI, to: mockSnodeAPICache) - mockNetwork.when { $0.networkStatus }.thenReturn(.singleValue(value: .connected)) - mockNetwork + try await mockNetwork.when { $0.networkStatus }.thenReturn(.singleValue(value: .connected)) + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -153,7 +153,7 @@ class MessageSenderGroupsSpec: AsyncSpec { ) } .thenReturn(Network.BatchResponse.mockConfigSyncResponse) - mockNetwork + try await mockNetwork .when { try await $0.getSwarm(for: .any) } .thenReturn([ LibSession.Snode( @@ -181,6 +181,7 @@ class MessageSenderGroupsSpec: AsyncSpec { swarmId: 1 ) ]) + dependencies.set(singleton: .network, to: mockNetwork) try await mockPoller.when { await $0.startIfNeeded() }.thenReturn(()) @@ -473,9 +474,9 @@ class MessageSenderGroupsSpec: AsyncSpec { ) .sinkAndStore(in: &disposables) - expect(mockNetwork) - .to(call(.exactly(times: 1), matchingParameters: .all) { network in - network.send( + await mockNetwork + .verify { + $0.send( endpoint: SnodeAPI.Endpoint.sequence, destination: .randomSnode(swarmPublicKey: groupId.hexString), body: try! JSONEncoder(using: dependencies).encode( @@ -503,13 +504,14 @@ class MessageSenderGroupsSpec: AsyncSpec { requestTimeout: Network.defaultTimeout, overallTimeout: Network.defaultTimeout ) - }) + } + .wasCalled(exactly: 1) } // MARK: ---- and the group configuration sync fails context("and the group configuration sync fails") { beforeEach { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -605,9 +607,9 @@ class MessageSenderGroupsSpec: AsyncSpec { .mapError { error.setting(to: $0) } .sinkAndStore(in: &disposables) - expect(mockNetwork) - .toNot(call { network in - network.send( + await mockNetwork + .verify { + $0.send( endpoint: Network.FileServer.Endpoint.file, destination: .serverUpload( server: Network.FileServer.fileServer, @@ -619,14 +621,15 @@ class MessageSenderGroupsSpec: AsyncSpec { requestTimeout: Network.fileUploadTimeout, overallTimeout: nil ) - }) + } + .wasNotCalled() } // MARK: ------ with an image context("with an image") { // MARK: ------ uploads the image it("uploads the image") { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -659,9 +662,9 @@ class MessageSenderGroupsSpec: AsyncSpec { using: dependencies ) - expect(mockNetwork) - .to(call(.exactly(times: 1), matchingParameters: .all) { network in - network.send( + await mockNetwork + .verify { + $0.send( endpoint: Network.FileServer.Endpoint.file, destination: .serverUpload( server: Network.FileServer.fileServer, @@ -673,14 +676,15 @@ class MessageSenderGroupsSpec: AsyncSpec { requestTimeout: Network.fileUploadTimeout, overallTimeout: Network.fileUploadTimeout ) - }) + } + .wasCalled(exactly: 1) } // MARK: ------ saves the image info to the group it("saves the image info to the group") { // Prevent the ConfigSyncJob network request by making the libSession cache appear empty mockLibSessionCache.when { $0.isEmpty }.thenReturn(true) - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -715,7 +719,7 @@ class MessageSenderGroupsSpec: AsyncSpec { // MARK: ------ fails if the image fails to upload it("fails if the image fails to upload") { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -837,9 +841,9 @@ class MessageSenderGroupsSpec: AsyncSpec { ) .sinkAndStore(in: &disposables) - expect(mockNetwork) - .to(call(.exactly(times: 1), matchingParameters: .all) { network in - network.send( + await mockNetwork + .verify { + $0.send( endpoint: PushNotificationAPI.Endpoint.subscribe, destination: .server( method: .post, @@ -877,7 +881,8 @@ class MessageSenderGroupsSpec: AsyncSpec { requestTimeout: Network.defaultTimeout, overallTimeout: nil ) - }) + } + .wasCalled(exactly: 1) } // MARK: ---- does not subscribe if push notifications are disabled @@ -903,16 +908,18 @@ class MessageSenderGroupsSpec: AsyncSpec { ) .sinkAndStore(in: &disposables) - expect(mockNetwork).toNot(call { network in - network.send( - endpoint: MockEndpoint.any, - destination: .any, - body: .any, - category: .any, - requestTimeout: .any, - overallTimeout: .any - ) - }) + await mockNetwork + .verify { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } + .wasNotCalled() } // MARK: ---- does not subscribe if there is no push token @@ -938,16 +945,18 @@ class MessageSenderGroupsSpec: AsyncSpec { ) .sinkAndStore(in: &disposables) - expect(mockNetwork).toNot(call { network in - network.send( - endpoint: MockEndpoint.any, - destination: .any, - body: .any, - category: .any, - requestTimeout: .any, - overallTimeout: .any - ) - }) + await mockNetwork + .verify { + $0.send( + endpoint: MockEndpoint.any, + destination: .any, + body: .any, + category: .any, + requestTimeout: .any, + overallTimeout: .any + ) + } + .wasNotCalled() } } } @@ -955,7 +964,7 @@ class MessageSenderGroupsSpec: AsyncSpec { // MARK: -- when adding members to a group context("when adding members to a group") { beforeEach { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -1072,7 +1081,7 @@ class MessageSenderGroupsSpec: AsyncSpec { // MARK: ---- and granting access to historic messages context("and granting access to historic messages") { beforeEach { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -1139,9 +1148,9 @@ class MessageSenderGroupsSpec: AsyncSpec { _ = groups_keys_load_message(groupKeysConf, &fakeHash3, pushResult, pushResultLen, 1234567890, groupInfoConf, groupMembersConf) } - expect(mockNetwork) - .to(call(.exactly(times: 1), matchingParameters: .all) { network in - network.send( + await mockNetwork + .verify { + $0.send( endpoint: SnodeAPI.Endpoint.sequence, destination: .randomSnode(swarmPublicKey: groupId.hexString), body: try! JSONEncoder(using: dependencies).encode( @@ -1186,7 +1195,8 @@ class MessageSenderGroupsSpec: AsyncSpec { requestTimeout: Network.defaultTimeout, overallTimeout: Network.defaultTimeout ) - }) + } + .wasCalled(exactly: 1) } // MARK: ---- schedules member invite jobs @@ -1293,7 +1303,7 @@ class MessageSenderGroupsSpec: AsyncSpec { // MARK: ---- and not granting access to historic messages context("and not granting access to historic messages") { beforeEach { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -1352,9 +1362,9 @@ class MessageSenderGroupsSpec: AsyncSpec { using: dependencies ).sinkUntilComplete() - expect(mockNetwork) - .to(call(.exactly(times: 1), matchingParameters: .all) { network in - network.send( + await mockNetwork + .verify { + $0.send( endpoint: SnodeAPI.Endpoint.sequence, destination: .randomSnode(swarmPublicKey: groupId.hexString), body: try! JSONEncoder(using: dependencies).encode( @@ -1387,7 +1397,8 @@ class MessageSenderGroupsSpec: AsyncSpec { requestTimeout: Network.defaultTimeout, overallTimeout: Network.defaultTimeout ) - }) + } + .wasCalled(exactly: 1) } // MARK: ---- schedules member invite jobs diff --git a/SessionMessagingKitTests/Sending & Receiving/Notifications/NotificationsManagerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Notifications/NotificationsManagerSpec.swift index 1ed9fd13e9..40279b4dcc 100644 --- a/SessionMessagingKitTests/Sending & Receiving/Notifications/NotificationsManagerSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/Notifications/NotificationsManagerSpec.swift @@ -20,11 +20,7 @@ class NotificationsManagerSpec: AsyncSpec { } ) @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() - @TestState(singleton: .extensionHelper, in: dependencies) var mockExtensionHelper: MockExtensionHelper! = MockExtensionHelper( - initialSetup: { helper in - helper.when { $0.hasDedupeRecordSinceLastCleared(threadId: .any) }.thenReturn(false) - } - ) + @TestState var mockExtensionHelper: MockExtensionHelper! = .create() @TestState(singleton: .notificationsManager, in: dependencies) var mockNotificationsManager: MockNotificationsManager! = .create() @TestState var message: Message! = VisibleMessage( sender: "05\(TestConstants.publicKey.replacingOccurrences(of: "1", with: "2"))", @@ -52,6 +48,9 @@ class NotificationsManagerSpec: AsyncSpec { dependencies.set(cache: .libSession, to: mockLibSessionCache) try await mockNotificationsManager.defaultInitialSetup() + + try await mockExtensionHelper.when { $0.hasDedupeRecordSinceLastCleared(threadId: .any) }.thenReturn(false) + dependencies.set(singleton: .extensionHelper, to: mockExtensionHelper) } // MARK: - a NotificationsManager - Ensure Should Show diff --git a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift index 6a043a6e09..1b9b3fccd8 100644 --- a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerManagerSpec.swift @@ -96,7 +96,7 @@ private class CommunityPollerManagerTestFixture: FixtureBase { ) } } - var mockNetwork: MockNetwork { mock(for: .network) { MockNetwork() } } + var mockNetwork: MockNetwork { mock(for: .network) } var mockAppContext: MockAppContext { mock(for: .appContext) } var mockUserDefaults: MockUserDefaults { mock(defaults: .standard) } var mockGeneralCache: MockGeneralCache { mock(cache: .general) } @@ -115,7 +115,7 @@ private class CommunityPollerManagerTestFixture: FixtureBase { private func applyBaselineStubs() async throws { try await applyBaselineStorage() - await applyBaselineNetwork() + try await applyBaselineNetwork() try await applyBaselineAppContext() try await applyBaselineUserDefaults() try await applyBaselineGeneralCache() @@ -157,12 +157,12 @@ private class CommunityPollerManagerTestFixture: FixtureBase { } } - private func applyBaselineNetwork() async { - mockNetwork.when { await $0.isSuspended }.thenReturn(false) - mockNetwork.when { $0.networkStatus }.thenReturn(.singleValue(value: .connected)) + private func applyBaselineNetwork() async throws { + try await mockNetwork.when { await $0.isSuspended }.thenReturn(false) + try await mockNetwork.when { $0.networkStatus }.thenReturn(.singleValue(value: .connected)) /// Delay for 10 seconds because we don't want the Poller to get stuck in a recursive loop - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, diff --git a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift index 09474e2285..20ca85cd3a 100644 --- a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift +++ b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift @@ -2066,28 +2066,28 @@ class ExtensionHelperSpec: AsyncSpec { context("when waiting for messages to be loaded") { // MARK: ---- stops waiting once messages are loaded it("stops waiting once messages are loaded") { - Task { + Task { [extensionHelper] in try? await Task.sleep(for: .milliseconds(10)) - try? await extensionHelper.loadMessages() + try? await extensionHelper?.loadMessages() } await expect { await extensionHelper.waitUntilMessagesAreLoaded(timeout: .milliseconds(150)) - }.to(beTrue()) + }.toEventually(beTrue()) } // MARK: ---- times out if it takes longer than the timeout specified it("times out if it takes longer than the timeout specified") { await expect { await extensionHelper.waitUntilMessagesAreLoaded(timeout: .milliseconds(50)) - }.to(beFalse()) + }.toEventually(beFalse()) } // MARK: ---- does not wait if messages have already been loaded it("does not wait if messages have already been loaded") { - await expect { try await extensionHelper.loadMessages() }.toNot(throwError()) + await expect { try await extensionHelper.loadMessages() }.toEventuallyNot(throwError()) await expect { await extensionHelper.waitUntilMessagesAreLoaded(timeout: .milliseconds(100)) - }.to(beTrue()) + }.toEventually(beTrue()) } // MARK: ---- waits if messages have already been loaded but we indicate we will load them again @@ -2096,7 +2096,7 @@ class ExtensionHelperSpec: AsyncSpec { await expect { await extensionHelper.waitUntilMessagesAreLoaded(timeout: .milliseconds(50)) - }.to(beFalse()) + }.toEventually(beFalse()) } } @@ -2163,7 +2163,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- successfully loads messages it("successfully loads messages") { - await expect { try await extensionHelper.loadMessages() }.toNot(throwError()) + await expect { try await extensionHelper.loadMessages() }.toEventuallyNot(throwError()) let interactions: [Interaction]? = mockStorage.read { try Interaction.fetchAll($0) } expect(interactions?.count).to(equal(1)) @@ -2180,7 +2180,7 @@ class ExtensionHelperSpec: AsyncSpec { } .thenReturn(Array(Data(hex: "0000550000"))) - await expect { try await extensionHelper.loadMessages() }.toNot(throwError()) + await expect { try await extensionHelper.loadMessages() }.toEventuallyNot(throwError()) expect(mockFileManager).to(call(matchingParameters: .all) { try $0.contentsOfDirectory( atPath: "/test/extensionCache/conversations/0000550000/config" @@ -2200,7 +2200,7 @@ class ExtensionHelperSpec: AsyncSpec { // MARK: ---- loads config messages before other messages it("loads config messages before other messages") { - await expect { try await extensionHelper.loadMessages() }.toNot(throwError()) + await expect { try await extensionHelper.loadMessages() }.toEventuallyNot(throwError()) let key: FunctionConsumer_Old.Key = FunctionConsumer_Old.Key( name: "contentsOfDirectory(atPath:)", @@ -2289,7 +2289,7 @@ class ExtensionHelperSpec: AsyncSpec { } .thenReturn(Array(Data(hex: "0000550000"))) - await expect { try await extensionHelper.loadMessages() }.toNot(throwError()) + await expect { try await extensionHelper.loadMessages() }.toEventuallyNot(throwError()) expect(mockFileManager).to(call(matchingParameters: .all) { try $0.removeItem( atPath: "/test/extensionCache/conversations/0000550000/config" @@ -2322,7 +2322,7 @@ class ExtensionHelperSpec: AsyncSpec { .when { try $0.contentsOfDirectory(atPath: "/test/extensionCache/conversations/a/read") } .thenReturn(["c"]) - await expect { try await extensionHelper.loadMessages() }.toNot(throwError()) + await expect { try await extensionHelper.loadMessages() }.toEventuallyNot(throwError()) await expect { await mockLogger.logs }.toEventually(contain( MockLogger.LogOutput( level: .info, @@ -2359,7 +2359,7 @@ class ExtensionHelperSpec: AsyncSpec { .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn(nil) - await expect { try await extensionHelper.loadMessages() }.toNot(throwError()) + await expect { try await extensionHelper.loadMessages() }.toEventuallyNot(throwError()) await expect { await mockLogger.logs }.toEventually(contain( MockLogger.LogOutput( level: .error, @@ -2412,7 +2412,7 @@ class ExtensionHelperSpec: AsyncSpec { .when { $0.generate(.plaintextWithXChaCha20(ciphertext: .any, encKey: .any)) } .thenReturn(nil) - await expect { try await extensionHelper.loadMessages() }.toNot(throwError()) + await expect { try await extensionHelper.loadMessages() }.toEventuallyNot(throwError()) await expect { await mockLogger.logs }.toEventually(contain( MockLogger.LogOutput( level: .error, @@ -2470,7 +2470,7 @@ class ExtensionHelperSpec: AsyncSpec { } .thenReturn(Array(Data(hex: "0000550000"))) - await expect { try await extensionHelper.loadMessages() }.toNot(throwError()) + await expect { try await extensionHelper.loadMessages() }.toEventuallyNot(throwError()) await expect { await mockLogger.logs }.toEventually(contain( MockLogger.LogOutput( level: .info, diff --git a/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift b/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift index 06a4562e50..6a3c83ffad 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift @@ -3,12 +3,23 @@ import Foundation import SessionNetworkingKit import SessionUtilitiesKit +import TestUtilities @testable import SessionMessagingKit -class MockExtensionHelper: Mock, ExtensionHelperType { +class MockExtensionHelper: ExtensionHelperType, Mockable { + public var handler: MockHandler + + required init(handler: MockHandler) { + self.handler = handler + } + + required init(handlerForBuilder: any MockFunctionHandler) { + self.handler = MockHandler(forwardingHandler: handlerForBuilder) + } + func deleteCache() { - mockNoReturn() + handler.mockNoReturn() } // MARK: - User Metadata @@ -18,51 +29,51 @@ class MockExtensionHelper: Mock, ExtensionHelperType { ed25519SecretKey: [UInt8], unreadCount: Int? ) throws { - try mockThrowingNoReturn(args: [sessionId, ed25519SecretKey, unreadCount]) + try handler.mockThrowingNoReturn(args: [sessionId, ed25519SecretKey, unreadCount]) } func loadUserMetadata() -> ExtensionHelper.UserMetadata? { - return mock() + return handler.mock() } // MARK: - Deduping func hasDedupeRecordSinceLastCleared(threadId: String) -> Bool { - return mock(args: [threadId]) + return handler.mock(args: [threadId]) } func dedupeRecordExists(threadId: String, uniqueIdentifier: String) -> Bool { - return mock(args: [threadId, uniqueIdentifier]) + return handler.mock(args: [threadId, uniqueIdentifier]) } func createDedupeRecord(threadId: String, uniqueIdentifier: String) throws { - return try mockThrowing(args: [threadId, uniqueIdentifier]) + return try handler.mockThrowing(args: [threadId, uniqueIdentifier]) } func removeDedupeRecord(threadId: String, uniqueIdentifier: String) throws { - return try mockThrowing(args: [threadId, uniqueIdentifier]) + return try handler.mockThrowing(args: [threadId, uniqueIdentifier]) } func upsertLastClearedRecord(threadId: String) throws { - try mockThrowingNoReturn(args: [threadId]) + try handler.mockThrowingNoReturn(args: [threadId]) } // MARK: - Config Dumps func lastUpdatedTimestamp(for sessionId: SessionId, variant: ConfigDump.Variant) -> TimeInterval { - return mock(args: [sessionId, variant]) + return handler.mock(args: [sessionId, variant]) } func replicate(dump: ConfigDump?, replaceExisting: Bool) { - mockNoReturn(args: [dump, replaceExisting]) + handler.mockNoReturn(args: [dump, replaceExisting]) } func replicateAllConfigDumpsIfNeeded(userSessionId: SessionId, allDumpSessionIds: Set) { - mockNoReturn(args: [userSessionId, allDumpSessionIds]) + handler.mockNoReturn(args: [userSessionId, allDumpSessionIds]) } func refreshDumpModifiedDate(sessionId: SessionId, variant: ConfigDump.Variant) { - mockNoReturn(args: [sessionId, variant]) + handler.mockNoReturn(args: [sessionId, variant]) } func loadUserConfigState( @@ -70,7 +81,7 @@ class MockExtensionHelper: Mock, ExtensionHelperType { userSessionId: SessionId, userEd25519SecretKey: [UInt8] ) { - mockNoReturn(args: [cache, userSessionId, userEd25519SecretKey]) + handler.mockNoReturn(args: [cache, userSessionId, userEd25519SecretKey]) } func loadGroupConfigStateIfNeeded( @@ -78,41 +89,41 @@ class MockExtensionHelper: Mock, ExtensionHelperType { swarmPublicKey: String, userEd25519SecretKey: [UInt8] ) throws -> [ConfigDump.Variant: Bool] { - return mock(args: [cache, swarmPublicKey, userEd25519SecretKey]) + return handler.mock(args: [cache, swarmPublicKey, userEd25519SecretKey]) } // MARK: - Notification Settings func replicate(settings: [String: Preferences.NotificationSettings], replaceExisting: Bool) throws { - try mockThrowingNoReturn(args: [settings, replaceExisting]) + try handler.mockThrowingNoReturn(args: [settings, replaceExisting]) } func loadNotificationSettings( previewType: Preferences.NotificationPreviewType, sound: Preferences.Sound ) -> [String: Preferences.NotificationSettings]? { - return mock(args: [previewType, sound]) + return handler.mock(args: [previewType, sound]) } // MARK: - Messages func unreadMessageCount() -> Int? { - return mock() + return handler.mock() } func saveMessage(_ message: SnodeReceivedMessage?, threadId: String, isUnread: Bool, isMessageRequest: Bool) throws { - try mockThrowingNoReturn(args: [message, threadId, isUnread, isMessageRequest]) + try handler.mockThrowingNoReturn(args: [message, threadId, isUnread, isMessageRequest]) } func willLoadMessages() { - mockNoReturn() + handler.mockNoReturn() } func loadMessages() async throws { - try mockThrowingNoReturn() + try handler.mockThrowingNoReturn() } @discardableResult func waitUntilMessagesAreLoaded(timeout: DispatchTimeInterval) async -> Bool { - return mock(args: [timeout]) + return handler.mock(args: [timeout]) } } diff --git a/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift index e16c3ff8ef..92049a5aec 100644 --- a/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift +++ b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift @@ -10,14 +10,14 @@ import Nimble @testable import SessionNetworkingKit -class PreparedRequestSendingSpec: QuickSpec { +class PreparedRequestSendingSpec: AsyncSpec { override class func spec() { // MARK: Configuration @TestState var dependencies: TestDependencies! = TestDependencies { dependencies in dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) } - @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork() + @TestState var mockNetwork: MockNetwork! = .create() @TestState var preparedRequest: Network.PreparedRequest! @TestState var error: Error? @TestState var disposables: [AnyCancellable]! = [] @@ -37,6 +37,8 @@ class PreparedRequestSendingSpec: QuickSpec { responseType: Int.self, using: dependencies ) + + dependencies.set(singleton: .network, to: mockNetwork) } // MARK: - a PreparedRequest sending Onion Requests @@ -44,7 +46,7 @@ class PreparedRequestSendingSpec: QuickSpec { // MARK: -- when sending context("when sending") { beforeEach { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, @@ -308,7 +310,7 @@ class PreparedRequestSendingSpec: QuickSpec { @TestState var receivedCompletion: Subscribers.Completion? = nil beforeEach { - mockNetwork + try await mockNetwork .when { $0.send( endpoint: MockEndpoint.any, diff --git a/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift index 93434fa620..5144f8cd5a 100644 --- a/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift +++ b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift @@ -9,23 +9,33 @@ import TestUtilities // MARK: - MockNetwork -class MockNetwork: Mock, NetworkType { +class MockNetwork: NetworkType, Mockable { + public var handler: MockHandler + + required init(handler: MockHandler) { + self.handler = handler + } + + required init(handlerForBuilder: any MockFunctionHandler) { + self.handler = MockHandler(forwardingHandler: handlerForBuilder) + } + var requestData: RequestData? - var isSuspended: Bool { mock() } - var networkStatus: AsyncStream { mock() } - var syncState: NetworkSyncState { mock() } + var isSuspended: Bool { handler.mock() } + var networkStatus: AsyncStream { handler.mock() } + var syncState: NetworkSyncState { handler.mock() } func getActivePaths() async throws -> [LibSession.Path] { - return try mockThrowing() + return try handler.mockThrowing() } func getSwarm(for swarmPublicKey: String) async throws -> Set { - return try mockThrowing(args: [swarmPublicKey]) + return try handler.mockThrowing(args: [swarmPublicKey]) } func getRandomNodes(count: Int) async throws -> Set { - return try mockThrowing(args: [count]) + return try handler.mockThrowing(args: [count]) } func send( @@ -46,7 +56,7 @@ class MockNetwork: Mock, NetworkType { overallTimeout: overallTimeout ) - return mock(args: [endpoint, destination, body, category, requestTimeout, overallTimeout]) + return handler.mock(args: [endpoint, destination, body, category, requestTimeout, overallTimeout]) } func send( @@ -67,35 +77,35 @@ class MockNetwork: Mock, NetworkType { overallTimeout: overallTimeout ) - return try mockThrowing(args: [endpoint, destination, body, category, requestTimeout, overallTimeout]) + return try handler.mockThrowing(args: [endpoint, destination, body, category, requestTimeout, overallTimeout]) } func checkClientVersion(ed25519SecretKey: [UInt8]) async throws -> (info: ResponseInfoType, value: AppVersionResponse) { - return try mockThrowing(args: [ed25519SecretKey]) + return try handler.mockThrowing(args: [ed25519SecretKey]) } func resetNetworkStatus() async { - mockNoReturn() + handler.mockNoReturn() } func setNetworkStatus(status: NetworkStatus) async { - mockNoReturn(args: [status]) + handler.mockNoReturn(args: [status]) } func suspendNetworkAccess() async { - mockNoReturn() + handler.mockNoReturn() } func resumeNetworkAccess(autoReconnect: Bool) async { - mockNoReturn(args: [autoReconnect]) + handler.mockNoReturn(args: [autoReconnect]) } func finishCurrentObservations() async { - mockNoReturn() + handler.mockNoReturn() } func clearCache() async { - mockNoReturn() + handler.mockNoReturn() } } @@ -249,6 +259,7 @@ public extension Array where Element: Mocked, Element: Codable { enum MockEndpoint: EndpointType, Mocked { static var any: MockEndpoint = .anyValue static var mockValue: MockEndpoint = .mock + static var skipTypeMatchForAnyComparison: Bool { true } case anyValue case mock diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index efd3f434e7..1b8b46aa6f 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -31,23 +31,8 @@ class OnboardingSpec: AsyncSpec { @TestState var mockGeneralCache: MockGeneralCache! = .create() @TestState var mockLibSession: MockLibSessionCache! = MockLibSessionCache() @TestState var mockUserDefaults: MockUserDefaults! = .create() - @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork() - @TestState(singleton: .extensionHelper, in: dependencies) var mockExtensionHelper: MockExtensionHelper! = MockExtensionHelper( - initialSetup: { helper in - helper - .when { $0.replicate(dump: .any, replaceExisting: .any) } - .thenReturn(()) - helper - .when { - try $0.saveUserMetadata( - sessionId: .any, - ed25519SecretKey: .any, - unreadCount: .any - ) - } - .thenReturn(()) - } - ) + @TestState var mockNetwork: MockNetwork! = .create() + @TestState var mockExtensionHelper: MockExtensionHelper! = .create() @TestState var mockSnodeAPICache: MockSnodeAPICache! = MockSnodeAPICache() @TestState var disposables: [AnyCancellable]! = [] @TestState var manager: Onboarding.Manager! @@ -104,7 +89,7 @@ class OnboardingSpec: AsyncSpec { .when { $0.generate(.signature(message: .any, ed25519SecretKey: .any)) } .thenReturn(Authentication.Signature.standard(signature: "TestSignature".bytes)) - mockNetwork.when { try await $0.getSwarm(for: .any) }.thenReturn([ + try await mockNetwork.when { try await $0.getSwarm(for: .any) }.thenReturn([ LibSession.Snode( ed25519PubkeyHex: "1234", ip: "1.2.3.4", @@ -131,10 +116,10 @@ class OnboardingSpec: AsyncSpec { return try? cache.pendingPushes(swarmPublicKey: cache.userSessionId.hexString) }() - mockNetwork + try await mockNetwork .when { try await $0.send( - endpoint: MockEndpoint.mock, + endpoint: MockEndpoint.any, destination: .any, body: .any, category: .any, @@ -168,6 +153,7 @@ class OnboardingSpec: AsyncSpec { ) ] )) + dependencies.set(singleton: .network, to: mockNetwork) try await mockUserDefaults.defaultInitialSetup() try await mockUserDefaults @@ -178,6 +164,20 @@ class OnboardingSpec: AsyncSpec { .thenReturn(false) try await mockUserDefaults.when { $0.integer(forKey: .any) }.thenReturn(2) dependencies.set(defaults: .standard, to: mockUserDefaults) + + try await mockExtensionHelper + .when { $0.replicate(dump: .any, replaceExisting: .any) } + .thenReturn(()) + try await mockExtensionHelper + .when { + try $0.saveUserMetadata( + sessionId: .any, + ed25519SecretKey: .any, + unreadCount: .any + ) + } + .thenReturn(()) + dependencies.set(singleton: .extensionHelper, to: mockExtensionHelper) } // MARK: - an Onboarding Cache - Initialization @@ -502,15 +502,33 @@ class OnboardingSpec: AsyncSpec { // MARK: -- polls for the userProfile config it("polls for the userProfile config") { - let base64EncodedDataString: String = "eyJtZXRob2QiOiJiYXRjaCIsInBhcmFtcyI6eyJyZXF1ZXN0cyI6W3sibWV0aG9kIjoicmV0cmlldmUiLCJwYXJhbXMiOnsibGFzdF9oYXNoIjoiIiwibWF4X3NpemUiOi0xLCJuYW1lc3BhY2UiOjIsInB1YmtleSI6IjA1ODg2NzJjY2I5N2Y0MGJiNTcyMzg5ODkyMjZjZjQyOWI1NzViYTM1NTQ0M2Y0N2JjNzZjNWFiMTQ0YTk2YzY1YiIsInB1YmtleV9lZDI1NTE5IjoiYmFjNmU3MWVmZDdkZmE0YTgzYzk4ZWQyNGYyNTRhYjJjMjY3ZjljY2RiMTcyYTUyODBhMDQ0NGFkMjRlODljYyIsInNpZ25hdHVyZSI6IlZHVnpkRk5wWjI1aGRIVnlaUT09IiwidGltZXN0YW1wIjoxMjM0NTY3ODkwMDAwfX1dfX0=" + let preparedRequest: Network.PreparedRequest = try SnodeAPI.preparedPoll( + namespaces: [.configUserProfile], + lastHashes: [:], + refreshingConfigHashes: [], + from: LibSession.Snode( + ed25519PubkeyHex: "1234", + ip: "1.2.3.4", + httpsPort: 1233, + quicPort: 1234, + version: "2.11.0", + swarmId: 1 + ), + authMethod: Authentication.standard( + sessionId: SessionId(.standard, hex: TestConstants.publicKey), + ed25519PublicKey: Array(Data(hex: TestConstants.edPublicKey)), + ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)) + ), + using: dependencies + ) - await expect(mockNetwork) - .toEventually(call(.exactly(times: 1), matchingParameters: .atLeast(3)) { + await mockNetwork + .verify { try await $0.send( - endpoint: MockEndpoint.mock, + endpoint: SnodeAPI.Endpoint.batch, destination: Network.Destination.snode( LibSession.Snode( - ed25519PubkeyHex: "", + ed25519PubkeyHex: "1234", ip: "1.2.3.4", httpsPort: 1233, quicPort: 1234, @@ -519,12 +537,13 @@ class OnboardingSpec: AsyncSpec { ), swarmPublicKey: "0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b" ), - body: Data(base64Encoded: base64EncodedDataString), + body: preparedRequest.body, category: .standard, requestTimeout: 10, overallTimeout: nil ) - }) + } + .wasCalled(exactly: 1, timeout: .milliseconds(100)) } // MARK: -- the display name stream to output the correct value @@ -720,13 +739,15 @@ class OnboardingSpec: AsyncSpec { // MARK: -- replicates the user metadata it("replicates the user metadata") { - expect(mockExtensionHelper).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.saveUserMetadata( - sessionId: SessionId(.standard, hex: TestConstants.publicKey), - ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)), - unreadCount: 0 - ) - }) + await mockExtensionHelper + .verify { + try $0.saveUserMetadata( + sessionId: SessionId(.standard, hex: TestConstants.publicKey), + ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)), + unreadCount: 0 + ) + } + .wasCalled(exactly: 1) } // MARK: -- stores the desired useAPNs value in the user defaults diff --git a/TestUtilities/ArgumentDescribing.swift b/TestUtilities/ArgumentDescribing.swift index a3d1421ca8..17f3378212 100644 --- a/TestUtilities/ArgumentDescribing.swift +++ b/TestUtilities/ArgumentDescribing.swift @@ -34,10 +34,7 @@ internal func summary(for argument: Any?) -> String { return "[\(sortedValues.joined(separator: ", "))]" case let data as Data: return "Data(base64Encoded: \(data.base64EncodedString()))" - - default: - // Default to the `debugDescription` if available but sort any dictionary content by keys - return sortDictionariesInReflectedString(String(reflecting: argument)) + default: return recursiveSummary(for: argument) } } @@ -53,6 +50,81 @@ private func isAnyValue(_ value: Any) -> Bool { return false } +private func recursiveSummary(for subject: Any) -> String { + let mirror: Mirror = Mirror(reflecting: subject) + let typeName: String = String(describing: Swift.type(of: subject)) + + /// Fall back to a simple description for types with no custom representation (eg. primitives) + guard let displayStyle: Mirror.DisplayStyle = mirror.displayStyle else { + return String(describing: subject) + } + + switch displayStyle { + case .struct, .class: + /// If there are no properties, just print the type name + guard !mirror.children.isEmpty else { return typeName } + + let properties: String = mirror.children + .compactMap { child -> String? in + guard let label: String = child.label else { + return nil + } + + return "\(label): \(summary(for: child.value))" + } + .sorted() + .joined(separator: ", ") + + return "\(typeName)(\(properties))" + + case .enum: + // Handle associated values first + if let child: Mirror.Child = mirror.children.first { + if let label: String = child.label { + /// Enum case with one or more named associated values + let properties: String = mirror.children + .compactMap { child -> String? in + guard let label: String = child.label else { + return nil + } + + return "\(label): \(summary(for: child.value))" + } + .sorted() + .joined(separator: ", ") + + return ".\(label)(\(properties))" + } + + /// Enum case with one or more unnamed associated values + let values: String = mirror.children + .map { summary(for: $0.value) } + .joined(separator: ", ") + + return ".\(mirror.subjectType).\(subject)(\(values))" + } + + /// Simple enum case with no associated value + return ".\(subject)" + + case .tuple: + let elements: String = mirror.children + .map { child -> String in + if let label: String = child.label { + return "\(label): \(summary(for: child.value))" + } + + return summary(for: child.value) + } + .joined(separator: ", ") + + return "(\(elements))" + + /// For other collections like Set, fall back to the default but sort any dictionary content by keys + default: return sortDictionariesInReflectedString(String(describing: subject)) + } +} + private func sortDictionariesInReflectedString(_ input: String) -> String { // Regular expression to match the headers dictionary let pattern = "\\[(.+?)\\]" diff --git a/TestUtilities/MockFunction.swift b/TestUtilities/MockFunction.swift index 303fb1ce04..2806ff5f4b 100644 --- a/TestUtilities/MockFunction.swift +++ b/TestUtilities/MockFunction.swift @@ -1,4 +1,6 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable import Foundation @@ -38,43 +40,124 @@ internal final class MockFunction { return true } + private func isEquatableMatch(lhs: E, rhs: Any) -> Bool { + if let rhs = rhs as? E { + return lhs == rhs + } + + return false + } + + private func isAnyValue(_ value: Any) -> Bool { + func open(value: T) -> Bool { + if let mockedEquatable = value as? any Equatable { + return isEquatableMatch(lhs: mockedEquatable, rhs: T.any) + } + + /// Compare using the `summary` as a fallback + return summary(for: value) == summary(for: T.any) + } + + if let mockedValue = value as? any Mocked { + return open(value: mockedValue) + } + + return false + } + private func argumentMatches(stubArg: Any?, callArg: Any?) -> Bool { - func isStubArgAnAnyMatcher(value: T) -> Bool { - return areEqual(value, T.any) + func isWildcardMatch(_ value: T) -> Bool { + if isAnyValue(value) { + /// The value is an `any` so check if the mocked type is a "super" wildcard (like `MockEndpoint.any` that + /// matches any other value + if T.skipTypeMatchForAnyComparison { + return true + } + + /// Otherwise the types need to match + return callArg is T + } + + /// Not a wildcard + return false } switch (stubArg, callArg) { case (.none, .none): return true /// Too hard to compare `nil == nil` after type erasure case (.none, .some): return false /// Expected `nil`, given a value - case (.some(let stub as any Mocked), .none): - return isStubArgAnAnyMatcher(value: stub) - - case (.some, .none): return false /// Expected non-`nil` value for non-`Mocked` type, given `nil` + case (.some(let lhs), .none): return isAnyValue(lhs) /// Allow `any == nil` case (.some(let stub), .some(let call)): /// If the `stubArg` is `Mocked.any` then we want to match anything - if let mockedStub = stub as? any Mocked, isStubArgAnAnyMatcher(value: mockedStub) { - return type(of: stub) == type(of: call) + if let mockedStub = stub as? any Mocked, isWildcardMatch(mockedStub) { + return true + } + + /// Check if there is an equatable match first (for performance reasons) + if let equatableValue = stub as? any Equatable, isEquatableMatch(lhs: equatableValue, rhs: call) { + return true } - /// Otherwise we need to do some form of equality check - return areEqual(stub, call) + /// Otherwise we need to use reflection to to a nested equality check (just in case a child element is a wildcard) + let mirrorLhs: Mirror = Mirror(reflecting: stub) + let mirrorRhs: Mirror = Mirror(reflecting: call) + + /// Since the `stub` isn't a wildcard the types need to match + guard String(describing: mirrorLhs.subjectType) == String(describing: mirrorRhs.subjectType) else { + return false + } + + switch mirrorLhs.displayStyle { + case .struct, .class, .tuple, .collection, .dictionary, .enum: + let childrenLhs: [Mirror.Child] = Array(mirrorLhs.children) + let childrenRhs: [Mirror.Child] = Array(mirrorRhs.children) + + /// If they are simple enums with no associated types then just compare the `summary` value + if childrenLhs.isEmpty && childrenRhs.isEmpty && mirrorLhs.displayStyle == .enum { + return summary(for: stub) == summary(for: call) + } + + /// Check enum case names are the same if applicable + if mirrorLhs.displayStyle == .enum { + let caseNameLhs: String = String(describing: stub).before(first: "(") + let caseNameRhs: String = String(describing: call).before(first: "(") + + if caseNameLhs != caseNameRhs { + return false + } + } + + /// If the number of args differ then there isn't a match + guard childrenLhs.count == childrenRhs.count else { return false } + + /// If any of the arguments don't match (recursively) then there is no match + for i in 0.. Bool { - func open(lhs: T, rhs: Any) -> Bool { - if let rhs = rhs as? T { - return lhs == rhs - } - - return false - } +} - if let equatableLhs = lhs as? any Equatable { - return open(lhs: equatableLhs, rhs: rhs) +private extension String { + func before(first delimiter: Character) -> String { + if let index: String.Index = firstIndex(of: delimiter) { + return String(prefix(upTo: index)) } - - /// Compare using the `summary` as a fallback - return summary(for: lhs) == summary(for: rhs) + + return self } } diff --git a/TestUtilities/MockHandler.swift b/TestUtilities/MockHandler.swift index 28f1a44a70..b8fb3537c8 100644 --- a/TestUtilities/MockHandler.swift +++ b/TestUtilities/MockHandler.swift @@ -68,6 +68,19 @@ public final class MockHandler { // MARK: - Verification + func expectedCall(for functionBlock: @escaping (T) async throws -> R) async -> RecordedCall? { + let builder: MockFunctionBuilder = createBuilder(for: functionBlock) + + guard let builtFunction = try? await builder.build() else { + return nil + } + + return RecordedCall( + name: builtFunction.name, + args: builtFunction.arguments + ) + } + func recordedCalls(for functionBlock: @escaping (T) async throws -> R) async -> [RecordedCall]? { let builder: MockFunctionBuilder = createBuilder(for: functionBlock) diff --git a/TestUtilities/Mocked.swift b/TestUtilities/Mocked.swift index 96915c1300..e9dbb74719 100644 --- a/TestUtilities/Mocked.swift +++ b/TestUtilities/Mocked.swift @@ -11,6 +11,12 @@ import UIKit.UIApplication public protocol Mocked { static var any: Self { get } static var mock: Self { get } + + static var skipTypeMatchForAnyComparison: Bool { get } +} + +public extension Mocked { + static var skipTypeMatchForAnyComparison: Bool { false } } public protocol MockedGeneric { diff --git a/TestUtilities/Nimble/NimbleVerification.swift b/TestUtilities/Nimble/NimbleVerification.swift index 35dd58619e..83c8500e82 100644 --- a/TestUtilities/Nimble/NimbleVerification.swift +++ b/TestUtilities/Nimble/NimbleVerification.swift @@ -102,15 +102,25 @@ private func beCalled( } } - var details: String = "" + let maybeExpectedCall: RecordedCall? = await info.mock.handler.expectedCall(for: info.callBlock) let maybeAllCalls: [RecordedCall]? = await info.mock.handler.allRecordedCalls(for: info.callBlock) + let funcName: String? = (maybeExpectedCall?.name ?? maybeAllCalls?.first?.name) + var details: String = "\n Expected to call \(funcName.map { "'\($0)'" } ?? "function") with parameters:" + + if let expectedCall: RecordedCall = maybeExpectedCall { + let args: String = expectedCall.args.map { summary(for: $0) }.joined(separator: ", ") + details += "\n- [\(args)]" + } + else { + details += "\n- Unable to determine the expected parameters" + } if let allCalls: [RecordedCall] = maybeAllCalls, !allCalls.isEmpty { let callDescriptions: String = allCalls .map { call in let args: String = call.args.map { summary(for: $0) }.joined(separator: ", ") - return "- \(call.name) [\(args)]" + return "- [\(args)]" } .joined(separator: "\n") diff --git a/_SharedTestUtilities/MockUserDefaults.swift b/_SharedTestUtilities/MockUserDefaults.swift index 145864b9ff..5904a10e0c 100644 --- a/_SharedTestUtilities/MockUserDefaults.swift +++ b/_SharedTestUtilities/MockUserDefaults.swift @@ -29,12 +29,28 @@ class MockUserDefaults: UserDefaultsType, Mockable { func bool(forKey defaultName: String) -> Bool { return (handler.mock(args: [defaultName]) ?? false) } func url(forKey defaultName: String) -> URL? { return handler.mock(args: [defaultName]) } - func set(_ value: Any?, forKey defaultName: String) { handler.mockNoReturn(args: [value, defaultName]) } - func set(_ value: Int, forKey defaultName: String) { handler.mockNoReturn(args: [value, defaultName]) } - func set(_ value: Float, forKey defaultName: String) { handler.mockNoReturn(args: [value, defaultName]) } - func set(_ value: Double, forKey defaultName: String) { handler.mockNoReturn(args: [value, defaultName]) } - func set(_ value: Bool, forKey defaultName: String) { handler.mockNoReturn(args: [value, defaultName]) } - func set(_ url: URL?, forKey defaultName: String) { handler.mockNoReturn(args: [url, defaultName]) } + func set(_ value: Any?, forKey defaultName: String) { + handler.mockNoReturn(generics: [Any.self], args: [value, defaultName]) + } + func set(_ value: Int, forKey defaultName: String) { + handler.mockNoReturn(generics: [Int.self], args: [value, defaultName]) + } + + func set(_ value: Float, forKey defaultName: String) { + handler.mockNoReturn(generics: [Float.self], args: [value, defaultName]) + } + + func set(_ value: Double, forKey defaultName: String) { + handler.mockNoReturn(generics: [Double.self], args: [value, defaultName]) + } + + func set(_ value: Bool, forKey defaultName: String) { + handler.mockNoReturn(generics: [Bool.self], args: [value, defaultName]) + } + + func set(_ url: URL?, forKey defaultName: String) { + handler.mockNoReturn(generics: [URL?.self], args: [url, defaultName]) + } func removeObject(forKey defaultName: String) { handler.mockNoReturn(args: [defaultName]) From b596de28e3248e64aa05dc051023eafb48e0979b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 11 Sep 2025 17:09:51 +1000 Subject: [PATCH 45/59] Fixed more failing unit tests, updated more mock types to Mockable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Updated MockKeychain to be Mockable • Updated MockImageDataManager to be Mockable • Updated MockSnodeAPICache to be Mockable • Updated feature storage in TestDependencies to be in memory instead of in user defaults (which may not be mocked) • Simplified some request building logic --- Session.xcodeproj/project.pbxproj | 4 - .../Jobs/DisplayPictureDownloadJob.swift | 9 +- .../Open Groups/OpenGroupAPI.swift | 87 ++++++---- .../Types/Request+OpenGroupAPI.swift | 2 +- .../Notifications/PushNotificationAPI.swift | 2 +- .../Types/Request+PushNotificationAPI.swift | 4 +- .../Crypto/CryptoSMKSpec.swift | 3 +- .../Models/MessageDeduplicationSpec.swift | 2 +- .../Jobs/DisplayPictureDownloadJobSpec.swift | 103 ++++++------ ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 34 ++-- .../LibSession/LibSessionGroupInfoSpec.swift | 4 +- .../LibSessionGroupMembersSpec.swift | 4 +- .../LibSession/LibSessionSpec.swift | 5 +- .../Crypto/CryptoOpenGroupAPISpec.swift | 3 +- .../Open Groups/Models/SOGSMessageSpec.swift | 2 +- .../Open Groups/OpenGroupAPISpec.swift | 6 +- .../Open Groups/OpenGroupManagerSpec.swift | 44 +++-- .../MessageReceiverGroupsSpec.swift | 35 ++-- .../MessageSenderGroupsSpec.swift | 80 +++++---- .../MessageSenderSpec.swift | 4 +- .../NotificationsManagerSpec.swift | 4 +- .../Utilities/ExtensionHelperSpec.swift | 57 ++----- .../MockDisplayPictureCache.swift | 14 -- .../_TestUtilities/MockImageDataManager.swift | 25 ++- .../MockNotificationsManager.swift | 7 +- .../_TestUtilities/MockPoller.swift | 7 +- .../LibSession/LibSession+Networking.swift | 14 +- .../SessionNetworkAPI/SessionNetworkAPI.swift | 22 ++- .../SnodeAPI/Request+SnodeAPI.swift | 8 +- SessionNetworkingKit/SnodeAPI/SnodeAPI.swift | 4 +- SessionNetworkingKit/Types/Destination.swift | 100 +++--------- .../Types/PreparedRequest.swift | 25 ++- SessionNetworkingKit/Types/Request.swift | 8 +- .../Types/BatchRequestSpec.swift | 4 +- .../Types/DestinationSpec.swift | 20 +-- .../Types/PreparedRequestSendingSpec.swift | 14 +- .../Types/PreparedRequestSpec.swift | 30 +++- .../Types/RequestSpec.swift | 19 ++- .../_TestUtilities/MockNetwork.swift | 15 +- .../_TestUtilities/MockSnodeAPICache.swift | 51 +++--- .../_TestUtilities/Mocked+SNK.swift | 4 +- ...eadNotificationSettingsViewModelSpec.swift | 5 +- .../ThreadSettingsViewModelSpec.swift | 15 +- SessionTests/Database/DatabaseSpec.swift | 3 +- SessionTests/Onboarding/OnboardingSpec.swift | 16 +- SessionUtilitiesKit/Database/Storage.swift | 2 +- .../Dependency Injection/Dependencies.swift | 21 ++- SessionUtilitiesKit/General/Feature.swift | 28 +++- .../General/GeneralCacheSpec.swift | 3 +- .../_TestUtilities/Mocked+SUK.swift | 9 +- TestUtilities/MockError.swift | 41 ++++- TestUtilities/MockFunction.swift | 140 +--------------- TestUtilities/MockFunctionBuilder.swift | 15 +- TestUtilities/MockHandler.swift | 137 ++++++++-------- TestUtilities/Mockable.swift | 9 +- TestUtilities/Mocked.swift | 13 +- TestUtilities/Nimble/NimbleVerification.swift | 84 ++++++---- TestUtilities/RecordedCall.swift | 154 +++++++++++++++++- _SharedTestUtilities/FixtureBase.swift | 22 ++- _SharedTestUtilities/MockKeychain.swift | 31 ++-- _SharedTestUtilities/TestDependencies.swift | 28 +++- 61 files changed, 936 insertions(+), 729 deletions(-) delete mode 100644 SessionMessagingKitTests/_TestUtilities/MockDisplayPictureCache.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 10ce56ceaf..2e6c7b4ffe 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -605,7 +605,6 @@ FD336F622CAA28CF00C0B51B /* ArgumentDescribing+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* ArgumentDescribing+SMK.swift */; }; FD336F632CAA28CF00C0B51B /* MockOGMCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5D2CAA28CF00C0B51B /* MockOGMCache.swift */; }; FD336F642CAA28CF00C0B51B /* MockCommunityPollerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */; }; - FD336F652CAA28CF00C0B51B /* MockDisplayPictureCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */; }; FD336F672CAA28CF00C0B51B /* MockGroupPollerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5A2CAA28CF00C0B51B /* MockGroupPollerCache.swift */; }; FD336F682CAA28CF00C0B51B /* MockPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5E2CAA28CF00C0B51B /* MockPoller.swift */; }; FD336F692CAA28CF00C0B51B /* MockLibSessionCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5B2CAA28CF00C0B51B /* MockLibSessionCache.swift */; }; @@ -1981,7 +1980,6 @@ FD336F562CAA28CF00C0B51B /* Mocked+SMK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mocked+SMK.swift"; sourceTree = ""; }; FD336F572CAA28CF00C0B51B /* ArgumentDescribing+SMK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ArgumentDescribing+SMK.swift"; sourceTree = ""; }; FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCommunityPollerCache.swift; sourceTree = ""; }; - FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDisplayPictureCache.swift; sourceTree = ""; }; FD336F5A2CAA28CF00C0B51B /* MockGroupPollerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGroupPollerCache.swift; sourceTree = ""; }; FD336F5B2CAA28CF00C0B51B /* MockLibSessionCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockLibSessionCache.swift; sourceTree = ""; }; FD336F5C2CAA28CF00C0B51B /* MockNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNotificationsManager.swift; sourceTree = ""; }; @@ -4980,7 +4978,6 @@ FD336F572CAA28CF00C0B51B /* ArgumentDescribing+SMK.swift */, FD336F562CAA28CF00C0B51B /* Mocked+SMK.swift */, FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */, - FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */, FD981BCC2DC81ABB00564172 /* MockExtensionHelper.swift */, FD336F5A2CAA28CF00C0B51B /* MockGroupPollerCache.swift */, FD78E9F12DDA9E9B00D55B50 /* MockImageDataManager.swift */, @@ -7222,7 +7219,6 @@ FD336F632CAA28CF00C0B51B /* MockOGMCache.swift in Sources */, FD481A922CAD17DE00ECC4CF /* LibSessionGroupMembersSpec.swift in Sources */, FD336F642CAA28CF00C0B51B /* MockCommunityPollerCache.swift in Sources */, - FD336F652CAA28CF00C0B51B /* MockDisplayPictureCache.swift in Sources */, FD336F672CAA28CF00C0B51B /* MockGroupPollerCache.swift in Sources */, FD336F682CAA28CF00C0B51B /* MockPoller.swift in Sources */, FD336F692CAA28CF00C0B51B /* MockLibSessionCache.swift in Sources */, diff --git a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift index c5257be82b..6726475c26 100644 --- a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift +++ b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift @@ -63,19 +63,20 @@ public enum DisplayPictureDownloadJob: JobExecutor { } } .tryMap { (preparedDownload: Network.PreparedRequest) -> Network.PreparedRequest<(Data, String, URL?)> in + let downloadUrl: URL? = try? preparedDownload.generateUrl() + guard let filePath: String = try? dependencies[singleton: .displayPictureManager].path( - for: (preparedDownload.destination.url?.absoluteString) - .defaulting(to: preparedDownload.destination.urlPathAndParamsString) + for: (downloadUrl?.absoluteString ?? preparedDownload.path) ) else { throw DisplayPictureError.invalidPath } guard !dependencies[singleton: .fileManager].fileExists(atPath: filePath) else { - throw DisplayPictureError.alreadyDownloaded(preparedDownload.destination.url) + throw DisplayPictureError.alreadyDownloaded(downloadUrl) } return preparedDownload.map { _, data in - (data, filePath, preparedDownload.destination.url) + (data, filePath, downloadUrl) } } .flatMap { $0.send(using: dependencies) } diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 8a6a011b7b..ca616b4add 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -1208,14 +1208,12 @@ public enum OpenGroupAPI { // MARK: - Authentication fileprivate static func signatureHeaders( - url: URL, method: HTTPMethod, + pathAndParamsString: String, body: Data?, authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> [HTTPHeader: String] { - let path: String = url.path - .appending(url.query.map { value in "?\(value)" }) let method: String = method.rawValue let timestamp: Int = Int(floor(dependencies.dateNow.timeIntervalSince1970)) @@ -1245,7 +1243,7 @@ public enum OpenGroupAPI { .appending(contentsOf: nonce) .appending(contentsOf: timestampBytes) .appending(contentsOf: method.bytes) - .appending(contentsOf: path.bytes) + .appending(contentsOf: pathAndParamsString.bytes) .appending(contentsOf: bodyHash ?? []) /// Sign the above message @@ -1351,12 +1349,62 @@ public enum OpenGroupAPI { preparedRequest: Network.PreparedRequest, using dependencies: Dependencies ) throws -> Network.Destination { + /// Handle the cached and invalid cases first (no need to sign them) + switch preparedRequest.destination { + case .cached: return preparedRequest.destination + case .snode, .randomSnode: throw NetworkError.unauthorised + default: break + } + guard let signingData: AdditionalSigningData = preparedRequest.additionalSignatureData as? AdditionalSigningData else { throw OpenGroupAPIError.signingFailed } - return try preparedRequest.destination - .signed(data: signingData, body: preparedRequest.body, using: dependencies) + let signatureHeaders: [HTTPHeader: String] = try OpenGroupAPI.signatureHeaders( + method: preparedRequest.method, + pathAndParamsString: preparedRequest.path, + body: preparedRequest.body, + authMethod: signingData.authMethod, + using: dependencies + ) + + switch preparedRequest.destination { + case .server(let info): + return .server( + info: Network.Destination.ServerInfo( + method: info.method, + server: info.server, + queryParameters: info.queryParameters, + headers: info.headers.updated(with: signatureHeaders), + x25519PublicKey: info.x25519PublicKey + ) + ) + + case .serverUpload(let info, let fileName): + return .serverUpload( + info: Network.Destination.ServerInfo( + method: info.method, + server: info.server, + queryParameters: info.queryParameters, + headers: info.headers.updated(with: signatureHeaders), + x25519PublicKey: info.x25519PublicKey + ), + fileName: fileName + ) + + case .serverDownload(let info): + return .serverDownload( + info: Network.Destination.ServerInfo( + method: info.method, + server: info.server, + queryParameters: info.queryParameters, + headers: info.headers.updated(with: signatureHeaders), + x25519PublicKey: info.x25519PublicKey + ) + ) + + case .snode, .randomSnode, .cached: throw OpenGroupAPIError.signingFailed + } } } @@ -1369,30 +1417,3 @@ private extension OpenGroupAPI { } } } - -private extension Network.Destination { - func signed(data: OpenGroupAPI.AdditionalSigningData, body: Data?, using dependencies: Dependencies) throws -> Network.Destination { - switch self { - case .snode, .randomSnode: throw NetworkError.unauthorised - case .cached: return self - case .server(let info): return .server(info: try info.signed(data, body, using: dependencies)) - case .serverUpload(let info, let fileName): - return .serverUpload(info: try info.signed(data, body, using: dependencies), fileName: fileName) - - case .serverDownload(let info): - return .serverDownload(info: try info.signed(data, body, using: dependencies)) - } - } -} - -private extension Network.Destination.ServerInfo { - func signed(_ data: OpenGroupAPI.AdditionalSigningData, _ body: Data?, using dependencies: Dependencies) throws -> Network.Destination.ServerInfo { - return updated(with: try OpenGroupAPI.signatureHeaders( - url: url, - method: method, - body: body, - authMethod: data.authMethod, - using: dependencies - )) - } -} diff --git a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift index 8c290dffb7..0bbd4e2aa7 100644 --- a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift @@ -23,7 +23,7 @@ public extension Request where Endpoint == OpenGroupAPI.Endpoint { throw CryptoError.signatureGenerationFailed } - self = try Request( + self = Request( endpoint: endpoint, destination: .server( method: method, diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index 92897c570f..f5348f8cc5 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -22,7 +22,7 @@ private extension Log.Category { public enum PushNotificationAPI { internal static let encryptionKeyLength: Int = 32 - private static let maxRetryCount: Int = 4 + internal static let maxRetryCount: Int = 4 private static let tokenExpirationInterval: TimeInterval = (12 * 60 * 60) public static let server: String = "https://push.getsession.org" diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift index 6a751db9ce..1c0c939946 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift @@ -14,8 +14,8 @@ public extension Request where Endpoint == PushNotificationAPI.Endpoint { headers: [HTTPHeader: String] = [:], body: T? = nil, retryCount: Int = 0 - ) throws { - self = try Request( + ) { + self = Request( endpoint: endpoint, destination: .server( method: method, diff --git a/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift b/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift index 3fb61c83df..0bcbebda58 100644 --- a/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift +++ b/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift @@ -2,6 +2,7 @@ import Foundation import SessionUtilitiesKit +import TestUtilities import Quick import Nimble @@ -14,7 +15,7 @@ class CryptoSMKSpec: AsyncSpec { @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .crypto, in: dependencies) var crypto: Crypto! = Crypto(using: dependencies) - @TestState var mockGeneralCache: MockGeneralCache! = .create() + @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) beforeEach { /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead diff --git a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift index 1857d208a6..6b41ffdcab 100644 --- a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift +++ b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift @@ -21,7 +21,7 @@ class MessageDeduplicationSpec: AsyncSpec { customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState var mockExtensionHelper: MockExtensionHelper! = .create() + @TestState var mockExtensionHelper: MockExtensionHelper! = .create(using: dependencies) @TestState var mockMessage: Message! = { let result: ReadReceipt = ReadReceipt(timestamps: [1]) result.sentTimestampMs = 12345678901234 diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index 545aa4839f..838c1aa240 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -40,19 +40,13 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { "673120e153a5cb6b869380744d493068ebc418266d6596d728cfc60b30662a089376" + "f2761e3bb6ee837a26b24b5" ) - @TestState var mockNetwork: MockNetwork! = .create() + @TestState var mockNetwork: MockNetwork! = .create(using: dependencies) @TestState(singleton: .fileManager, in: dependencies) var mockFileManager: MockFileManager! = MockFileManager( initialSetup: { $0.defaultInitialSetup() } ) - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() - @TestState(singleton: .imageDataManager, in: dependencies) var mockImageDataManager: MockImageDataManager! = MockImageDataManager( - initialSetup: { imageDataManager in - imageDataManager - .when { await $0.load(.any) } - .thenReturn(nil) - } - ) - @TestState var mockGeneralCache: MockGeneralCache! = .create() + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create(using: dependencies) + @TestState(singleton: .imageDataManager, in: dependencies) var mockImageDataManager: MockImageDataManager! = .create(using: dependencies) + @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) beforeEach { /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead @@ -103,6 +97,10 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { } .thenReturn(MockNetwork.response(data: encryptedData)) dependencies.set(singleton: .network, to: mockNetwork) + + try await mockImageDataManager + .when { await $0.load(.any) } + .thenReturn(nil) } // MARK: - a DisplayPictureDownloadJob @@ -639,9 +637,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { it("does not save the picture") { expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - expect(mockImageDataManager).toNotEventually(call { - await $0.load(.any) - }) + await mockImageDataManager + .verify { await $0.load(.any) } + .wasNotCalled(timeout: .milliseconds(50)) expect(mockStorage.read { db in try Profile.fetchOne(db) }).to(equal(profile)) } } @@ -658,9 +656,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { it("does not save the picture") { expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - expect(mockImageDataManager).toNotEventually(call { - await $0.load(.any) - }) + await mockImageDataManager + .verify { await $0.load(.any) } + .wasNotCalled(timeout: .milliseconds(50)) expect(mockStorage.read { db in try Profile.fetchOne(db) }).to(equal(profile)) } } @@ -675,9 +673,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: ------ does not save the picture it("does not save the picture") { - expect(mockImageDataManager).toNotEventually(call { - await $0.load(.any) - }) + await mockImageDataManager + .verify { await $0.load(.any) } + .wasNotCalled(timeout: .milliseconds(50)) expect(mockStorage.read { db in try Profile.fetchOne(db) }).to(equal(profile)) } } @@ -696,12 +694,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: ---- adds the image data to the displayPicture cache it("adds the image data to the displayPicture cache") { - expect(mockImageDataManager) - .toEventually(call(.exactly(times: 1), matchingParameters: .all) { - await $0.load( - .url(URL(fileURLWithPath: "/test/DisplayPictures/5465737448617368")) - ) - }) + await mockImageDataManager + .verify { await $0.load(.url(URL(fileURLWithPath: "/test/DisplayPictures/5465737448617368"))) } + .wasCalled(exactly: 1, timeout: .milliseconds(50)) } // MARK: ---- successfully completes the job @@ -750,9 +745,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { .wasNotCalled() expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - await expect(mockImageDataManager).toNotEventually(call { - await $0.load(.any) - }) + await mockImageDataManager + .verify { await $0.load(.any) } + .wasNotCalled(timeout: .milliseconds(50)) expect(mockStorage.read { db in try Profile.fetchOne(db) }).to(beNil()) } } @@ -777,9 +772,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { .wasNotCalled() expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - await expect(mockImageDataManager).toNotEventually(call { - await $0.load(.any) - }) + await mockImageDataManager + .verify { await $0.load(.any) } + .wasNotCalled(timeout: .milliseconds(50)) expect(mockStorage.read { db in try Profile.fetchOne(db) }) .toNot(equal( Profile( @@ -813,9 +808,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { .wasNotCalled() expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - await expect(mockImageDataManager).toNotEventually(call { - await $0.load(.any) - }) + await mockImageDataManager + .verify { await $0.load(.any) } + .wasNotCalled(timeout: .milliseconds(50)) expect(mockStorage.read { db in try Profile.fetchOne(db) }) .toNot(equal( Profile( @@ -854,12 +849,13 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { ) }) - await expect(mockImageDataManager) - .toEventually(call(.exactly(times: 1), matchingParameters: .all) { + await mockImageDataManager + .verify { await $0.load( .url(URL(fileURLWithPath: "/test/DisplayPictures/5465737448617368")) ) - }) + } + .wasCalled(exactly: 1, timeout: .milliseconds(50)) expect(mockStorage.read { db in try Profile.fetchOne(db) }) .to(equal( Profile( @@ -944,9 +940,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { .wasNotCalled() expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - await expect(mockImageDataManager).toNotEventually(call { - await $0.load(.any) - }) + await mockImageDataManager + .verify { await $0.load(.any) } + .wasNotCalled(timeout: .milliseconds(50)) expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) }).to(beNil()) } } @@ -970,9 +966,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { .wasNotCalled() expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - await expect(mockImageDataManager).toNotEventually(call { - await $0.load(.any) - }) + await mockImageDataManager + .verify { await $0.load(.any) } + .wasNotCalled(timeout: .milliseconds(50)) expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) }) .toNot(equal( ClosedGroup( @@ -1010,9 +1006,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { .wasNotCalled() expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - await expect(mockImageDataManager).toNotEventually(call { - await $0.load(.any) - }) + await mockImageDataManager + .verify { await $0.load(.any) } + .wasNotCalled(timeout: .milliseconds(50)) expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) }) .toNot(equal( ClosedGroup( @@ -1117,7 +1113,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { it("does not save the picture") { expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - expect(mockImageDataManager).toNotEventually(call { await $0.load(.any) }) + await mockImageDataManager + .verify { await $0.load(.any) } + .wasNotCalled(timeout: .milliseconds(50)) expect(mockStorage.read { db in try OpenGroup.fetchOne(db) }).to(beNil()) } } @@ -1138,7 +1136,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { it("does not save the picture") { expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) - expect(mockImageDataManager).toNotEventually(call { await $0.load(.any) }) + await mockImageDataManager + .verify { await $0.load(.any) } + .wasNotCalled(timeout: .milliseconds(50)) expect(mockStorage.read { db in try OpenGroup.fetchOne(db) }) .toNot(equal( OpenGroup( @@ -1176,12 +1176,13 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { attributes: nil ) }) - expect(mockImageDataManager) - .toEventually(call(.exactly(times: 1), matchingParameters: .all) { + await mockImageDataManager + .verify { await $0.load( .url(URL(fileURLWithPath: "/test/DisplayPictures/5465737448617368")) ) - }) + } + .wasCalled(exactly: 1, timeout: .milliseconds(50)) expect(mockStorage.read { db in try OpenGroup.fetchOne(db) }) .to(equal( OpenGroup( diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index b2d7aba9d2..cebaf36ec0 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -23,8 +23,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState var mockUserDefaults: MockUserDefaults! = .create() - @TestState var mockNetwork: MockNetwork! = .create() + @TestState var mockUserDefaults: MockUserDefaults! = .create(using: dependencies) + @TestState var mockNetwork: MockNetwork! = .create(using: dependencies) @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( initialSetup: { jobRunner in jobRunner @@ -39,7 +39,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { } ) @TestState var mockOGMCache: MockOGMCache! = MockOGMCache() - @TestState var mockGeneralCache: MockGeneralCache! = .create() + @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) @TestState var job: Job! = Job(variant: .retrieveDefaultOpenGroupRooms) @TestState var error: Error? = nil @TestState var permanentFailure: Bool! = false @@ -306,14 +306,20 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { .verify { $0.send( endpoint: OpenGroupAPI.Endpoint.sequence, - destination: expectedRequest.destination, + destination: .server(info: Network.Destination.ServerInfo( + method: .post, + server: OpenGroupAPI.defaultServer, + queryParameters: [:], + headers: .any, + x25519PublicKey: OpenGroupAPI.defaultServerPublicKey + )), body: expectedRequest.body, category: .standard, requestTimeout: expectedRequest.requestTimeout, overallTimeout: expectedRequest.overallTimeout ) } - .wasCalled(exactly: 1) + .wasCalled(exactly: 1, timeout: .milliseconds(50)) } // MARK: -- permanently fails if it gets an error @@ -343,8 +349,6 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { using: dependencies ) - expect(error).to(matchError(NetworkError.parsingFailed)) - expect(permanentFailure).to(beTrue()) await mockNetwork .verify { $0.send( @@ -356,7 +360,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { overallTimeout: .any ) } - .wasCalled(exactly: 1) + .wasCalled(exactly: 1, timeout: .milliseconds(50)) + expect(error).to(matchError(NetworkError.parsingFailed)) + expect(permanentFailure).to(beTrue()) } // MARK: -- stores the updated capabilities @@ -370,7 +376,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { using: dependencies ) - let capabilities: [Capability]? = mockStorage.read { db in try Capability.fetchAll(db) } + let capabilities: [Capability]? = await expect { mockStorage.read { db in try Capability.fetchAll(db) } } + .toEventuallyNot(beNil()) + .retrieveValue() expect(capabilities?.count).to(equal(2)) expect(capabilities?.map { $0.openGroupServer }) .to(equal([OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer])) @@ -389,7 +397,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { using: dependencies ) - let openGroups: [OpenGroup]? = mockStorage.read { db in try OpenGroup.fetchAll(db) } + let openGroups: [OpenGroup]? = await expect { mockStorage.read { db in try OpenGroup.fetchAll(db) } } + .toEventuallyNot(beNil()) + .retrieveValue() expect(openGroups?.count).to(equal(3)) // 1 for the entry used to fetch the default rooms expect(openGroups?.map { $0.server }) .to(equal([OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer])) @@ -462,7 +472,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { using: dependencies ) - let openGroups: [OpenGroup]? = mockStorage.read { db in try OpenGroup.fetchAll(db) } + let openGroups: [OpenGroup]? = await expect { mockStorage.read { db in try OpenGroup.fetchAll(db) } } + .toEventuallyNot(beNil()) + .retrieveValue() expect(openGroups?.count).to(equal(2)) // 1 for the entry used to fetch the default rooms expect(openGroups?.map { $0.server }) .to(equal([OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer])) diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift index c9a022cf9f..e11514e57e 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift @@ -21,12 +21,12 @@ class LibSessionGroupInfoSpec: AsyncSpec { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) dependencies.forceSynchronous = true } - @TestState var mockGeneralCache: MockGeneralCache! = .create() + @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState var mockNetwork: MockNetwork! = .create() + @TestState var mockNetwork: MockNetwork! = .create(using: dependencies) @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( initialSetup: { jobRunner in jobRunner diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift index e79931e31f..9d90a75744 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift @@ -20,12 +20,12 @@ class LibSessionGroupMembersSpec: AsyncSpec { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) dependencies.forceSynchronous = true } - @TestState var mockGeneralCache: MockGeneralCache! = .create() + @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState var mockNetwork: MockNetwork! = .create() + @TestState var mockNetwork: MockNetwork! = .create(using: dependencies) @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( initialSetup: { jobRunner in jobRunner diff --git a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift index a608dd8147..82842b2ec3 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift @@ -4,6 +4,7 @@ import Foundation import GRDB import SessionUtil import SessionUtilitiesKit +import TestUtilities import Quick import Nimble @@ -19,12 +20,12 @@ class LibSessionSpec: AsyncSpec { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) dependencies.forceSynchronous = true } - @TestState var mockGeneralCache: MockGeneralCache! = .create() + @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create(using: dependencies) @TestState var createGroupOutput: LibSession.CreatedGroupInfo! @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() @TestState var userGroupsConfig: LibSession.Config! diff --git a/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift index 4e38d6797b..e0abeb86a6 100644 --- a/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift @@ -2,6 +2,7 @@ import Foundation import SessionUtilitiesKit +import TestUtilities import Quick import Nimble @@ -14,7 +15,7 @@ class CryptoOpenGroupAPISpec: AsyncSpec { @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .crypto, in: dependencies) var crypto: Crypto! = Crypto(using: dependencies) - @TestState var mockGeneralCache: MockGeneralCache! = .create() + @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) beforeEach { /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead diff --git a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift index 6d696ffd5d..e6c4e224ec 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift @@ -29,7 +29,7 @@ class SOGSMessageSpec: AsyncSpec { """ @TestState var messageData: Data! = messageJson.data(using: .utf8)! @TestState var dependencies: TestDependencies! = TestDependencies() - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create(using: dependencies) @TestState var decoder: JSONDecoder! = JSONDecoder(using: dependencies) // MARK: - a SOGSMessage diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index d8288cb12e..57b43b1534 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -20,9 +20,9 @@ class OpenGroupAPISpec: AsyncSpec { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) dependencies.forceSynchronous = true } - @TestState var mockNetwork: MockNetwork! = .create() - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() - @TestState var mockGeneralCache: MockGeneralCache! = .create() + @TestState var mockNetwork: MockNetwork! = .create(using: dependencies) + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create(using: dependencies) + @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() @TestState var disposables: [AnyCancellable]! = [] @TestState var error: Error? diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 86cde3afe8..21ee08e906 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -126,30 +126,16 @@ class OpenGroupManagerSpec: AsyncSpec { .thenReturn([:]) } ) - @TestState var mockNetwork: MockNetwork! = .create() - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() - @TestState var mockUserDefaults: MockUserDefaults! = .create() - @TestState var mockAppGroupDefaults: MockUserDefaults! = .create() - @TestState var mockGeneralCache: MockGeneralCache! = .create() + @TestState var mockNetwork: MockNetwork! = .create(using: dependencies) + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create(using: dependencies) + @TestState var mockUserDefaults: MockUserDefaults! = .create(using: dependencies) + @TestState var mockAppGroupDefaults: MockUserDefaults! = .create(using: dependencies) + @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() @TestState var mockOGMCache: MockOGMCache! = MockOGMCache() - @TestState var mockPoller: MockPoller! = .create() - @TestState(singleton: .communityPollerManager, in: dependencies) var mockCommunityPollerManager: MockCommunityPollerManager! = .create() - @TestState(singleton: .keychain, in: dependencies) var mockKeychain: MockKeychain! = MockKeychain( - initialSetup: { keychain in - keychain - .when { - try $0.getOrGenerateEncryptionKey( - forKey: .any, - length: .any, - cat: .any, - legacyKey: .any, - legacyService: .any - ) - } - .thenReturn(Data([1, 2, 3])) - } - ) + @TestState var mockPoller: MockPoller! = .create(using: dependencies) + @TestState(singleton: .communityPollerManager, in: dependencies) var mockCommunityPollerManager: MockCommunityPollerManager! = .create(using: dependencies) + @TestState(singleton: .keychain, in: dependencies) var mockKeychain: MockKeychain! = .create(using: dependencies) @TestState(singleton: .fileManager, in: dependencies) var mockFileManager: MockFileManager! = MockFileManager( initialSetup: { $0.defaultInitialSetup() } ) @@ -245,6 +231,18 @@ class OpenGroupManagerSpec: AsyncSpec { .when { $0.syncState } .thenReturn(CommunityPollerManagerSyncState()) + try await mockKeychain + .when { + try $0.getOrGenerateEncryptionKey( + forKey: .any, + length: .any, + cat: .any, + legacyKey: .any, + legacyService: .any + ) + } + .thenReturn(Data([1, 2, 3])) + try await mockUserDefaults.defaultInitialSetup() try await mockUserDefaults.when { $0.integer(forKey: .any) }.thenReturn(0) dependencies.set(defaults: .standard, to: mockUserDefaults) @@ -762,7 +760,7 @@ class OpenGroupManagerSpec: AsyncSpec { ) ) } - .wasCalled(timeout: .milliseconds(100)) + .wasCalled(timeout: .milliseconds(50)) await mockPoller.verify { await $0.startIfNeeded() }.wasCalled() } diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index 7a8097315b..386c9b7afa 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -300,6 +300,9 @@ class MessageReceiverGroupsSpec: AsyncSpec { try await fixture.mockUserDefaults .when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) } .thenReturn(true) + fixture.mockLibSessionCache + .when { $0.isMessageRequest(threadId: .any, threadVariant: .any) } + .thenReturn(true) fixture.mockStorage.write { db in try MessageReceiver.handleGroupUpdateMessage( @@ -690,7 +693,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { overallTimeout: nil ) } - .wasCalled(exactly: 1) + .wasCalled(exactly: PushNotificationAPI.maxRetryCount + 1, timeout: .milliseconds(50)) } } } @@ -2588,6 +2591,12 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ---- and the current user is not an admin context("and the current user is not an admin") { + beforeEach { + try await fixture.mockLibSessionCache + .when { $0.isAdmin(groupSessionId: .any) } + .thenReturn(false) + } + // MARK: ------ does not delete the messages from the swarm it("does not delete the messages from the swarm") { fixture.deleteContentMessage = GroupUpdateDeleteMemberContentMessage( @@ -2871,7 +2880,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { overallTimeout: nil ) } - .wasCalled(exactly: 1) + .wasCalled(exactly: PushNotificationAPI.maxRetryCount + 1, timeout: .milliseconds(50)) } // MARK: ---- and the group is an invitation @@ -3246,15 +3255,15 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { var mockAppContext: MockAppContext { mock(for: .appContext) } var mockUserDefaults: MockUserDefaults { mock(defaults: .standard) } var mockCrypto: MockCrypto { mock(for: .crypto) } - var mockKeychain: MockKeychain { mock(for: .keychain) { MockKeychain() } } + var mockKeychain: MockKeychain { mock(for: .keychain) } var mockFileManager: MockFileManager { mock(for: .fileManager) { MockFileManager() } } var mockExtensionHelper: MockExtensionHelper { mock(for: .extensionHelper) } var mockGroupPollerManager: MockGroupPollerManager { mock(for: .groupPollerManager) } var mockNotificationsManager: MockNotificationsManager { mock(for: .notificationsManager) } var mockGeneralCache: MockGeneralCache { mock(cache: .general) } var mockLibSessionCache: MockLibSessionCache { mock(cache: .libSession) { MockLibSessionCache() } } - var mockSnodeAPICache: MockSnodeAPICache { mock(cache: .snodeAPI) { MockSnodeAPICache() } } - let mockPoller: MockPoller = .create() + var mockSnodeAPICache: MockSnodeAPICache { mock(cache: .snodeAPI) } + var mockPoller: MockPoller { mock() } let userGroupsConfig: LibSession.Config let convoInfoVolatileConfig: LibSession.Config @@ -3531,14 +3540,14 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { try await applyBaselineAppContext() try await applyBaselineUserDefaults() try await applyBaselineCrypto() - await applyBaselineKeychain() + try await applyBaselineKeychain() await applyBaselineFileManager() try await applyBaselineExtensionHelper() try await applyBaselineGroupPollerManager() try await applyBaselineNotificationsManager() try await applyBaselineGeneralCache() await applyBaselineLibSessionCache() - await applyBaselineSnodeAPICache() + try await applyBaselineSnodeAPICache() try await applyBaselinePoller() } @@ -3639,8 +3648,8 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { .thenReturn("TestHash".bytes) } - private func applyBaselineKeychain() async { - mockKeychain + private func applyBaselineKeychain() async throws { + try await mockKeychain .when { try $0.migrateLegacyKeyIfNeeded( legacyKey: .any, @@ -3649,7 +3658,7 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { ) } .thenReturn(()) - mockKeychain + try await mockKeychain .when { try $0.getOrGenerateEncryptionKey( forKey: .any, @@ -3660,7 +3669,7 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { ) } .thenReturn(Data([1, 2, 3])) - mockKeychain + try await mockKeychain .when { try $0.data(forKey: .pushNotificationEncryptionKey) } .thenReturn(Data((0.., DisplayPictureCacheType { - var downloadsToSchedule: Set { - get { return mock() } - set { mockNoReturn(args: [newValue]) } - } -} diff --git a/SessionMessagingKitTests/_TestUtilities/MockImageDataManager.swift b/SessionMessagingKitTests/_TestUtilities/MockImageDataManager.swift index d409bede10..583e89a8de 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockImageDataManager.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockImageDataManager.swift @@ -2,36 +2,47 @@ import Foundation import SessionUIKit +import TestUtilities @testable import SessionMessagingKit -class MockImageDataManager: Mock, ImageDataManagerType { +class MockImageDataManager: ImageDataManagerType, Mockable { + public var handler: MockHandler + + required init(handler: MockHandler) { + self.handler = handler + } + + required init(handlerForBuilder: any MockFunctionHandler) { + self.handler = MockHandler(forwardingHandler: handlerForBuilder) + } + @discardableResult func load( _ source: ImageDataManager.DataSource ) async -> ImageDataManager.ProcessedImageData? { - return mock(args: [source]) + return handler.mock(args: [source]) } func load( _ source: ImageDataManager.DataSource, onComplete: @escaping (ImageDataManager.ProcessedImageData?) -> Void ) { - mockNoReturn(args: [source], untrackedArgs: [onComplete]) + handler.mockNoReturn(args: [source]) } func cacheImage(_ image: UIImage, for identifier: String) async { - mockNoReturn(args: [image, identifier]) + handler.mockNoReturn(args: [image, identifier]) } func cachedImage(identifier: String) async -> ImageDataManager.ProcessedImageData? { - return mock(args: [identifier]) + return handler.mock(args: [identifier]) } func removeImage(identifier: String) async { - mockNoReturn(args: [identifier]) + handler.mockNoReturn(args: [identifier]) } func clearCache() async { - mockNoReturn() + handler.mockNoReturn() } } diff --git a/SessionMessagingKitTests/_TestUtilities/MockNotificationsManager.swift b/SessionMessagingKitTests/_TestUtilities/MockNotificationsManager.swift index 6e57935cbd..52f43df63f 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockNotificationsManager.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockNotificationsManager.swift @@ -9,7 +9,7 @@ import TestUtilities class MockNotificationsManager: NotificationsManagerType, Mockable { let handler: MockHandler - let dependencies: Dependencies = TestDependencies.any + var dependencies: Dependencies { handler.erasedDependencies as! Dependencies } required init(handler: MockHandler) { self.handler = handler @@ -20,7 +20,10 @@ class MockNotificationsManager: NotificationsManagerType, Mockable { } public required init(using dependencies: Dependencies) { - handler = MockHandler(dummyProvider: { _ in MockNotificationsManager(handler: .invalid()) }) + handler = MockHandler( + dummyProvider: { _ in MockNotificationsManager(handler: .invalid()) }, + using: dependencies + ) handler.mockNoReturn() } diff --git a/SessionMessagingKitTests/_TestUtilities/MockPoller.swift b/SessionMessagingKitTests/_TestUtilities/MockPoller.swift index 3d25aa2c6c..629d39bc66 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockPoller.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockPoller.swift @@ -13,7 +13,7 @@ actor MockPoller: PollerType, Mockable { nonisolated let handler: MockHandler - var dependencies: Dependencies { handler.mock() } + var dependencies: Dependencies { handler.erasedDependencies as! Dependencies } var pollerName: String { handler.mock() } var destination: PollerDestination { handler.mock() } var logStartAndStopCalls: Bool { handler.mock() } @@ -46,7 +46,10 @@ actor MockPoller: PollerType, Mockable { customAuthMethod: (any AuthenticationMethod)?, using dependencies: Dependencies ) { - handler = MockHandler(dummyProvider: { _ in MockPoller(handler: .invalid()) }) + handler = MockHandler( + dummyProvider: { _ in MockPoller(handler: .invalid()) }, + using: dependencies + ) handler.mockNoReturn( args: [ pollerName, diff --git a/SessionNetworkingKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift index 233cf624a8..44ae95308e 100644 --- a/SessionNetworkingKit/LibSession/LibSession+Networking.swift +++ b/SessionNetworkingKit/LibSession/LibSession+Networking.swift @@ -972,7 +972,12 @@ private extension LibSessionNetwork { _ callback: (UnsafePointer) -> Result ) throws -> Result { return try withBodyPointer(request.body) { cBodyPtr, bodySize in - try info.pathAndParamsString.withCString { cEndpoint in + let pathWithParams: String = try Network.Destination.generatePathWithParams( + endpoint: request.endpoint, + queryParameters: info.queryParameters + ) + + return try pathWithParams.withCString { cEndpoint in try withFileNamePtr(uploadFileName) { cUploadFileNamePtr in try info.withServerInfoPointer { cServerDestinationPtr in let params: session_request_params = session_request_params( @@ -1039,16 +1044,15 @@ private extension LibSessionNetwork { private extension Network.Destination.ServerInfo { func withServerInfoPointer(_ body: (UnsafePointer) -> Result) throws -> Result { - let url: URL = try self.url let x25519PublicKey: String = String(x25519PublicKey.suffix(64)) // Quick way to drop '05' prefix if present - guard let host: String = url.host else { throw NetworkError.invalidURL } + guard let host: String = self.host else { throw NetworkError.invalidURL } guard x25519PublicKey.count == 64 || x25519PublicKey.count == 66 else { throw LibSessionError.invalidCConversion } - let targetScheme: String = (url.scheme ?? "https") - let port: UInt16 = UInt16(url.port ?? (targetScheme == "https" ? 443 : 80)) + let targetScheme: String = (self.scheme ?? "https") + let port: UInt16 = UInt16(self.port ?? (targetScheme == "https" ? 443 : 80)) let headersArray: [String] = headers.flatMap { [$0.key, $0.value] } // Use scoped closure to avoid manual memory management (crazy nesting but it ends up safer) diff --git a/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift index d922740dcd..6d319a39b1 100644 --- a/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift +++ b/SessionNetworkingKit/SessionNetworkAPI/SessionNetworkAPI.swift @@ -100,18 +100,24 @@ public enum SessionNetworkAPI { using dependencies: Dependencies ) throws -> Network.Destination { guard - let url: URL = preparedRequest.destination.url, + let url: URL = try? preparedRequest.generateUrl(), case let .server(info) = preparedRequest.destination else { throw NetworkError.invalidPreparedRequest } return .server( - info: info.updated( - with: try signatureHeaders( - url: url, - method: preparedRequest.method, - body: preparedRequest.body, - using: dependencies - ) + info: Network.Destination.ServerInfo( + method: info.method, + server: info.server, + queryParameters: info.queryParameters, + headers: info.headers.updated( + with: try signatureHeaders( + url: url, + method: preparedRequest.method, + body: preparedRequest.body, + using: dependencies + ) + ), + x25519PublicKey: info.x25519PublicKey ) ) } diff --git a/SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift index 7d15ebe4e6..5b39824dbc 100644 --- a/SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift +++ b/SessionNetworkingKit/SnodeAPI/Request+SnodeAPI.swift @@ -16,8 +16,8 @@ public extension Request where Endpoint == SnodeAPI.Endpoint { requestTimeout: TimeInterval = Network.defaultTimeout, overallTimeout: TimeInterval? = nil, retryCount: Int = 0 - ) throws { - self = try Request( + ) { + self = Request( endpoint: endpoint, destination: .snode( snode, @@ -37,8 +37,8 @@ public extension Request where Endpoint == SnodeAPI.Endpoint { requestTimeout: TimeInterval = Network.defaultTimeout, overallTimeout: TimeInterval? = nil, retryCount: Int = 0 - ) throws { - self = try Request( + ) { + self = Request( endpoint: endpoint, destination: .randomSnode(swarmPublicKey: swarmPublicKey), body: body, diff --git a/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift index aba2b75416..bbd8d8dced 100644 --- a/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift +++ b/SessionNetworkingKit/SnodeAPI/SnodeAPI.swift @@ -96,7 +96,7 @@ public final class SnodeAPI { request: { switch snode { case .none: - return try Request( + return Request( endpoint: .batch, swarmPublicKey: swarmPublicKey, body: Network.BatchRequest(requestsKey: .requests, requests: requests), @@ -105,7 +105,7 @@ public final class SnodeAPI { ) case .some(let snode): - return try Request( + return Request( endpoint: .batch, snode: snode, swarmPublicKey: swarmPublicKey, diff --git a/SessionNetworkingKit/Types/Destination.swift b/SessionNetworkingKit/Types/Destination.swift index 8595463d2a..ec69a65abd 100644 --- a/SessionNetworkingKit/Types/Destination.swift +++ b/SessionNetworkingKit/Types/Destination.swift @@ -8,32 +8,19 @@ import SessionUtilitiesKit public extension Network { enum Destination: Equatable { public struct ServerInfo: Equatable { - private static let invalidServer: String = "INVALID_SERVER" - private static let invalidUrl: URL = URL(fileURLWithPath: "INVALID_URL") - - private let server: String - private let queryParameters: [HTTPQueryParam: String] - private let _url: URL - private let _pathAndParamsString: String - public let method: HTTPMethod + public let server: String + public let queryParameters: [HTTPQueryParam: String] public let headers: [HTTPHeader: String] public let x25519PublicKey: String - public var url: URL { - get throws { - guard _url != ServerInfo.invalidUrl else { throw NetworkError.invalidURL } - - return _url - } - } - public var pathAndParamsString: String { - get throws { - guard _url != ServerInfo.invalidUrl else { throw NetworkError.invalidPreparedRequest } - - return _pathAndParamsString - } - } + // Use iOS URL processing to extract the values from `server` + + public var host: String? { URL(string: server)?.host } + public var scheme: String? { URL(string: server)?.scheme } + public var port: Int? { URL(string: server)?.port } + + // MARK: - Initialization public init( method: HTTPMethod, @@ -42,9 +29,6 @@ public extension Network { headers: [HTTPHeader: String], x25519PublicKey: String ) { - self._url = ServerInfo.invalidUrl - self._pathAndParamsString = "" - self.method = method self.server = server self.queryParameters = queryParameters @@ -60,49 +44,20 @@ public extension Network { queryParameters: [HTTPQueryParam: String] = [:], headers: [HTTPHeader: String], x25519PublicKey: String - ) { - self._url = url - self._pathAndParamsString = (pathAndParamsString ?? url.path) - + ) throws { self.method = method - self.server = { + self.server = try { if let explicitServer: String = server { return explicitServer } if let urlHost: String = url.host { return "\(url.scheme.map { "\($0)://" } ?? "")\(urlHost)" } - return ServerInfo.invalidServer + throw NetworkError.invalidURL }() self.queryParameters = queryParameters self.headers = headers self.x25519PublicKey = x25519PublicKey } - - fileprivate func updated(for endpoint: E) throws -> ServerInfo { - let pathAndParamsString: String = generatePathsAndParams(endpoint: endpoint, queryParameters: queryParameters) - - return ServerInfo( - method: method, - url: try (URL(string: "\(server)\(pathAndParamsString)") ?? { throw NetworkError.invalidURL }()), - server: server, - pathAndParamsString: pathAndParamsString, - queryParameters: queryParameters, - headers: headers, - x25519PublicKey: x25519PublicKey - ) - } - - public func updated(with headers: [HTTPHeader: String]) -> ServerInfo { - return ServerInfo( - method: method, - url: _url, - server: server, - pathAndParamsString: _pathAndParamsString, - queryParameters: queryParameters, - headers: self.headers.updated(with: headers), - x25519PublicKey: x25519PublicKey - ) - } } case snode(LibSession.Snode, swarmPublicKey: String?) @@ -121,11 +76,10 @@ public extension Network { } } - public var url: URL? { + public var server: String? { switch self { - case .server(let info), .serverUpload(let info, _), .serverDownload(let info): return try? info.url - case .snode, .randomSnode: return nil - case .cached: return nil + case .server(let info), .serverUpload(let info, _), .serverDownload(let info): return info.server + default: return nil } } @@ -139,11 +93,12 @@ public extension Network { } } - public var urlPathAndParamsString: String { + public var queryParameters: [HTTPQueryParam: String] { switch self { case .server(let info), .serverUpload(let info, _), .serverDownload(let info): - return ((try? info.pathAndParamsString) ?? "") - default: return "" + return info.queryParameters + + default: return [:] } } @@ -188,8 +143,8 @@ public extension Network { headers: [HTTPHeader: String] = [:], x25519PublicKey: String, fileName: String? - ) -> Destination { - return .serverDownload(info: ServerInfo( + ) throws -> Destination { + return try .serverDownload(info: ServerInfo( method: .get, url: url, server: nil, @@ -220,7 +175,7 @@ public extension Network { // MARK: - Convenience - internal static func generatePathsAndParams(endpoint: E, queryParameters: [HTTPQueryParam: String]) -> String { + internal static func generatePathWithParams(endpoint: E, queryParameters: [HTTPQueryParam: String]) -> String { return [ "/\(endpoint.path)", queryParameters @@ -232,17 +187,6 @@ public extension Network { .joined(separator: "?") } - internal func withGeneratedUrl(for endpoint: E) throws -> Destination { - switch self { - case .server(let info): return .server(info: try info.updated(for: endpoint)) - case .serverUpload(let info, let fileName): - return .serverUpload(info: try info.updated(for: endpoint), fileName: fileName) - case .serverDownload(let info): return .serverDownload(info: try info.updated(for: endpoint)) - - default: return self - } - } - // MARK: - Equatable public static func == (lhs: Destination, rhs: Destination) -> Bool { diff --git a/SessionNetworkingKit/Types/PreparedRequest.swift b/SessionNetworkingKit/Types/PreparedRequest.swift index 1a9597f8f2..62db515a79 100644 --- a/SessionNetworkingKit/Types/PreparedRequest.swift +++ b/SessionNetworkingKit/Types/PreparedRequest.swift @@ -217,7 +217,10 @@ public extension Network { // The following data is needed in this type for handling batch requests self.method = request.destination.method self.endpointName = E.name - self.path = request.destination.urlPathAndParamsString + self.path = Destination.generatePathWithParams( + endpoint: endpoint, + queryParameters: request.destination.queryParameters + ) self.headers = request.destination.headers self.batchEndpoints = batchEndpoints @@ -316,6 +319,26 @@ public extension Network { self.b64 = b64 self.bytes = bytes } + + // MARK: - Functions + + public func generateUrl() throws -> URL { + switch destination { + case .server(let info), .serverUpload(let info, _), .serverDownload(let info): + let pathWithParams: String = Destination.generatePathWithParams( + endpoint: endpoint, + queryParameters: info.queryParameters + ) + + guard let url: URL = URL(string: "\(info.server)\(pathWithParams)") else { + throw NetworkError.invalidURL + } + + return url + + default: throw NetworkError.invalidURL + } + } } } diff --git a/SessionNetworkingKit/Types/Request.swift b/SessionNetworkingKit/Types/Request.swift index 4b08a0e563..9104a4b86a 100644 --- a/SessionNetworkingKit/Types/Request.swift +++ b/SessionNetworkingKit/Types/Request.swift @@ -55,9 +55,9 @@ public struct Request { requestTimeout: TimeInterval = Network.defaultTimeout, overallTimeout: TimeInterval? = nil, retryCount: Int = 0 - ) throws { + ) { self.endpoint = endpoint - self.destination = try destination.withGeneratedUrl(for: endpoint) + self.destination = destination self.body = body self.category = category self.requestTimeout = requestTimeout @@ -102,8 +102,8 @@ public extension Request where T == NoBody { requestTimeout: TimeInterval = Network.defaultTimeout, overallTimeout: TimeInterval? = nil, retryCount: Int = 0 - ) throws { - self = try Request( + ) { + self = Request( endpoint: endpoint, destination: destination, body: nil, diff --git a/SessionNetworkingKitTests/Types/BatchRequestSpec.swift b/SessionNetworkingKitTests/Types/BatchRequestSpec.swift index ccdb6d5e42..4e746eb3db 100644 --- a/SessionNetworkingKitTests/Types/BatchRequestSpec.swift +++ b/SessionNetworkingKitTests/Types/BatchRequestSpec.swift @@ -22,7 +22,7 @@ class BatchRequestSpec: QuickSpec { context("when encoding") { // MARK: ---- correctly strips specified headers from sub requests it("correctly strips specified headers from sub requests") { - let httpRequest: Request = try! Request( + let httpRequest: Request = Request( endpoint: .endpoint1, destination: .server( server: "testServer", @@ -58,7 +58,7 @@ class BatchRequestSpec: QuickSpec { // MARK: ---- does not strip unspecified headers from sub requests it("does not strip unspecified headers from sub requests") { - let httpRequest: Request = try! Request( + let httpRequest: Request = Request( endpoint: .endpoint1, destination: .server( server: "testServer", diff --git a/SessionNetworkingKitTests/Types/DestinationSpec.swift b/SessionNetworkingKitTests/Types/DestinationSpec.swift index e226a25f21..ba5e6cc188 100644 --- a/SessionNetworkingKitTests/Types/DestinationSpec.swift +++ b/SessionNetworkingKitTests/Types/DestinationSpec.swift @@ -15,7 +15,7 @@ class DestinationSpec: QuickSpec { context("when generating a path") { // MARK: ---- adds a leading forward slash to the endpoint path it("adds a leading forward slash to the endpoint path") { - let result: String = Network.Destination.generatePathsAndParams( + let result: String = Network.Destination.generatePathWithParams( endpoint: TestEndpoint.test1, queryParameters: [:] ) @@ -25,7 +25,7 @@ class DestinationSpec: QuickSpec { // MARK: ---- creates a valid URL with no query parameters it("creates a valid URL with no query parameters") { - let result: String = Network.Destination.generatePathsAndParams( + let result: String = Network.Destination.generatePathWithParams( endpoint: TestEndpoint.test1, queryParameters: [:] ) @@ -35,7 +35,7 @@ class DestinationSpec: QuickSpec { // MARK: ---- creates a valid URL when query parameters are provided it("creates a valid URL when query parameters are provided") { - let result: String = Network.Destination.generatePathsAndParams( + let result: String = Network.Destination.generatePathWithParams( endpoint: TestEndpoint.test1, queryParameters: [ .testParam: "123" @@ -45,20 +45,6 @@ class DestinationSpec: QuickSpec { expect(result).to(equal("/test1?testParam=123")) } } - - // MARK: -- for a server - context("for a server") { - // MARK: ---- throws an error if the generated URL is invalid - it("throws an error if the generated URL is invalid") { - expect { - _ = try Network.Destination.server( - server: "ftp:// test Server", - x25519PublicKey: "" - ).withGeneratedUrl(for: TestEndpoint.testParams("test", 123)) - } - .to(throwError(NetworkError.invalidURL)) - } - } } } } diff --git a/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift index 92049a5aec..da569ce58e 100644 --- a/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift +++ b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift @@ -17,13 +17,13 @@ class PreparedRequestSendingSpec: AsyncSpec { @TestState var dependencies: TestDependencies! = TestDependencies { dependencies in dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) } - @TestState var mockNetwork: MockNetwork! = .create() + @TestState var mockNetwork: MockNetwork! = .create(using: dependencies) @TestState var preparedRequest: Network.PreparedRequest! @TestState var error: Error? @TestState var disposables: [AnyCancellable]! = [] beforeEach { - let request: Request = try Request( + let request: Request = Request( endpoint: .endpoint1, destination: .server( method: .post, @@ -258,7 +258,7 @@ class PreparedRequestSendingSpec: AsyncSpec { context("a batch request") { // MARK: ---- with a BatchResponseMap context("with a BatchResponseMap") { - @TestState var subRequest1: Request! = try! Request( + @TestState var subRequest1: Request! = Request( endpoint: TestEndpoint.endpoint1, destination: .server( method: .post, @@ -266,7 +266,7 @@ class PreparedRequestSendingSpec: AsyncSpec { x25519PublicKey: "" ) ) - @TestState var subRequest2: Request! = try! Request( + @TestState var subRequest2: Request! = Request( endpoint: TestEndpoint.endpoint2, destination: .server( method: .post, @@ -275,7 +275,7 @@ class PreparedRequestSendingSpec: AsyncSpec { ) ) @TestState var preparedBatchRequest: Network.PreparedRequest>! = { - let request = try! Request( + let request = Request( endpoint: TestEndpoint.batch, destination: .server( method: .post, @@ -359,7 +359,7 @@ class PreparedRequestSendingSpec: AsyncSpec { // MARK: ------ supports transformations on subrequests it("supports transformations on subrequests") { preparedBatchRequest = { - let request = try! Request( + let request = Request( endpoint: TestEndpoint.batch, destination: .server( method: .post, @@ -420,7 +420,7 @@ class PreparedRequestSendingSpec: AsyncSpec { // MARK: ------ supports event handling on sub requests it("supports event handling on sub requests") { preparedBatchRequest = { - let request = try! Request( + let request = Request( endpoint: TestEndpoint.batch, destination: .server( method: .post, diff --git a/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift b/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift index b472404a17..9e78fa3db9 100644 --- a/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift +++ b/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift @@ -24,7 +24,7 @@ class PreparedRequestSpec: QuickSpec { describe("a PreparedRequest") { // MARK: -- generates the request correctly it("generates the request correctly") { - request = try! Request( + request = Request( endpoint: .endpoint, destination: .server( method: .post, @@ -62,7 +62,7 @@ class PreparedRequestSpec: QuickSpec { // MARK: -- does not strip excluded subrequest headers it("does not strip excluded subrequest headers") { - request = try! Request( + request = Request( endpoint: .endpoint, destination: .server( method: .post, @@ -85,6 +85,31 @@ class PreparedRequestSpec: QuickSpec { expect(TestEndpoint.excludedSubRequestHeaders).to(equal([HTTPHeader.testHeader])) expect(preparedRequest.headers.keys).to(contain([HTTPHeader.testHeader])) } + + // MARK: ---- throws an error if the generated URL is invalid + it("throws an error if the generated URL is invalid") { + request = Request( + endpoint: .testParams("test", 123), + destination: .server( + method: .post, + server: "ftp:// test Server", + queryParameters: [:], + headers: [ + "TestCustomHeader": "TestCustom", + HTTPHeader.testHeader: "Test" + ], + x25519PublicKey: "" + ), + body: nil + ) + preparedRequest = try! Network.PreparedRequest( + request: request, + responseType: TestType.self, + using: dependencies + ) + + expect { try preparedRequest.generateUrl() }.to(throwError(NetworkError.invalidURL)) + } } // MARK: - a Decodable @@ -166,6 +191,7 @@ fileprivate extension HTTPHeader { fileprivate enum TestEndpoint: EndpointType { case endpoint + case testParams(String, Int) static var name: String { "TestEndpoint" } static var batchRequestVariant: Network.BatchRequest.Child.Variant { .storageServer } diff --git a/SessionNetworkingKitTests/Types/RequestSpec.swift b/SessionNetworkingKitTests/Types/RequestSpec.swift index fd0255ec88..9f0f49d5d2 100644 --- a/SessionNetworkingKitTests/Types/RequestSpec.swift +++ b/SessionNetworkingKitTests/Types/RequestSpec.swift @@ -21,7 +21,7 @@ class RequestSpec: QuickSpec { describe("a Request") { // MARK: -- is initialized with the correct default values it("is initialized with the correct default values") { - let request: Request = try! Request( + let request: Request = Request( endpoint: .test1, destination: .server( server: "testServer", @@ -36,7 +36,7 @@ class RequestSpec: QuickSpec { // MARK: ---- sets all the values correctly it("sets all the values correctly") { - let request: Request = try! Request( + let request: Request = Request( endpoint: .test1, destination: .server( method: .delete, @@ -48,9 +48,10 @@ class RequestSpec: QuickSpec { ) ) - expect(request.destination.url?.absoluteString).to(equal("testServer/test1")) + expect(request.endpoint).to(equal(.test1)) + expect(request.destination.server).to(equal("testServer")) + expect(request.destination.queryParameters).to(equal([:])) expect(request.destination.method.rawValue).to(equal("DELETE")) - expect(request.destination.urlPathAndParamsString).to(equal("/test1")) expect(request.destination.headers).to(equal(["TestHeader": "test"])) expect(request.body).to(beNil()) } @@ -59,7 +60,7 @@ class RequestSpec: QuickSpec { context("with a base64 string body") { // MARK: ------ successfully encodes the body it("successfully encodes the body") { - let request: Request = try! Request( + let request: Request = Request( endpoint: .test1, destination: .server( server: "testServer", @@ -76,7 +77,7 @@ class RequestSpec: QuickSpec { // MARK: ------ throws an error if the body is not base64 encoded it("throws an error if the body is not base64 encoded") { - let request: Request = try! Request( + let request: Request = Request( endpoint: .test1, destination: .server( server: "testServer", @@ -96,7 +97,7 @@ class RequestSpec: QuickSpec { context("with a byte body") { // MARK: ------ successfully encodes the body it("successfully encodes the body") { - let request: Request<[UInt8], TestEndpoint> = try! Request( + let request: Request<[UInt8], TestEndpoint> = Request( endpoint: .test1, destination: .server( server: "testServer", @@ -115,7 +116,7 @@ class RequestSpec: QuickSpec { context("with a JSON body") { // MARK: ------ successfully encodes the body it("successfully encodes the body") { - let request: Request = try! Request( + let request: Request = Request( endpoint: .test1, destination: .server( server: "testServer", @@ -135,7 +136,7 @@ class RequestSpec: QuickSpec { // MARK: ------ successfully encodes no body it("successfully encodes no body") { - let request: Request = try! Request( + let request: Request = Request( endpoint: .test1, destination: .server( server: "testServer", diff --git a/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift index 5144f8cd5a..b55886a0ac 100644 --- a/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift +++ b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift @@ -49,7 +49,8 @@ class MockNetwork: NetworkType, Mockable { requestData = RequestData( method: destination.method, headers: destination.headers, - urlPathAndParamsString: destination.urlPathAndParamsString, + path: endpoint.path, + queryParameters: destination.queryParameters, body: body, category: category, requestTimeout: requestTimeout, @@ -70,7 +71,8 @@ class MockNetwork: NetworkType, Mockable { requestData = RequestData( method: destination.method, headers: destination.headers, - urlPathAndParamsString: destination.urlPathAndParamsString, + path: endpoint.path, + queryParameters: destination.queryParameters, body: body, category: category, requestTimeout: requestTimeout, @@ -206,7 +208,8 @@ struct RequestData: Codable, Mocked { static let any: RequestData = RequestData( method: .get, headers: .any, - urlPathAndParamsString: .any, + path: .any, + queryParameters: .any, body: .any, category: .standard, requestTimeout: .any, @@ -215,7 +218,8 @@ struct RequestData: Codable, Mocked { static let mock: RequestData = RequestData( method: .get, headers: [:], - urlPathAndParamsString: "", + path: "/mock", + queryParameters: [:], body: nil, category: .standard, requestTimeout: 0, @@ -224,7 +228,8 @@ struct RequestData: Codable, Mocked { let method: HTTPMethod let headers: [HTTPHeader: String] - let urlPathAndParamsString: String + let path: String + let queryParameters: [HTTPQueryParam: String] let body: Data? let category: Network.RequestCategory let requestTimeout: TimeInterval diff --git a/SessionNetworkingKitTests/_TestUtilities/MockSnodeAPICache.swift b/SessionNetworkingKitTests/_TestUtilities/MockSnodeAPICache.swift index 90be7933ed..df2c5dfe51 100644 --- a/SessionNetworkingKitTests/_TestUtilities/MockSnodeAPICache.swift +++ b/SessionNetworkingKitTests/_TestUtilities/MockSnodeAPICache.swift @@ -5,44 +5,55 @@ import Foundation import Foundation import Combine import SessionUtilitiesKit +import TestUtilities @testable import SessionNetworkingKit -class MockSnodeAPICache: Mock, SnodeAPICacheType { +class MockSnodeAPICache: SnodeAPICacheType, Mockable { + public var handler: MockHandler + + required init(handler: MockHandler) { + self.handler = handler + } + + required init(handlerForBuilder: any MockFunctionHandler) { + self.handler = MockHandler(forwardingHandler: handlerForBuilder) + } + var hardfork: Int { - get { return mock() } - set { mockNoReturn(args: [newValue]) } + get { return handler.mock() } + set { handler.mockNoReturn(args: [newValue]) } } var softfork: Int { - get { return mock() } - set { mockNoReturn(args: [newValue]) } + get { return handler.mock() } + set { handler.mockNoReturn(args: [newValue]) } } - var clockOffsetMs: Int64 { mock() } + var clockOffsetMs: Int64 { handler.mock() } func currentOffsetTimestampMs() -> T where T: Numeric { - return mock(generics: [T.self]) + return handler.mock(generics: [T.self]) } func setClockOffsetMs(_ clockOffsetMs: Int64) { - mockNoReturn(args: [clockOffsetMs]) + handler.mockNoReturn(args: [clockOffsetMs]) } } // MARK: - Convenience -extension Mock where T == SnodeAPICacheType { - func defaultInitialSetup() { - self.when { $0.hardfork }.thenReturn(0) - self.when { $0.hardfork = .any }.thenReturn(()) - self.when { $0.softfork }.thenReturn(0) - self.when { $0.softfork = .any }.thenReturn(()) - self.when { $0.clockOffsetMs }.thenReturn(0) - self.when { $0.setClockOffsetMs(.any) }.thenReturn(()) - self.when { $0.currentOffsetTimestampMs() }.thenReturn(Double(1234567890000)) - self.when { $0.currentOffsetTimestampMs() }.thenReturn(Int(1234567890000)) - self.when { $0.currentOffsetTimestampMs() }.thenReturn(Int64(1234567890000)) - self.when { $0.currentOffsetTimestampMs() }.thenReturn(UInt64(1234567890000)) +extension MockSnodeAPICache { + func defaultInitialSetup() async throws { + try await self.when { $0.hardfork }.thenReturn(0) + try await self.when { $0.hardfork = .any }.thenReturn(()) + try await self.when { $0.softfork }.thenReturn(0) + try await self.when { $0.softfork = .any }.thenReturn(()) + try await self.when { $0.clockOffsetMs }.thenReturn(0) + try await self.when { $0.setClockOffsetMs(.any) }.thenReturn(()) + try await self.when { $0.currentOffsetTimestampMs() }.thenReturn(Double(1234567890000)) + try await self.when { $0.currentOffsetTimestampMs() }.thenReturn(Int(1234567890000)) + try await self.when { $0.currentOffsetTimestampMs() }.thenReturn(Int64(1234567890000)) + try await self.when { $0.currentOffsetTimestampMs() }.thenReturn(UInt64(1234567890000)) } } diff --git a/SessionNetworkingKitTests/_TestUtilities/Mocked+SNK.swift b/SessionNetworkingKitTests/_TestUtilities/Mocked+SNK.swift index 1246509917..cdeb947518 100644 --- a/SessionNetworkingKitTests/_TestUtilities/Mocked+SNK.swift +++ b/SessionNetworkingKitTests/_TestUtilities/Mocked+SNK.swift @@ -40,11 +40,11 @@ extension Network.Destination: @retroactive Mocked { headers: .any, x25519PublicKey: .any ) - public static var mock: Network.Destination = try! Network.Destination.server( + public static var mock: Network.Destination = Network.Destination.server( server: "testServer", headers: [:], x25519PublicKey: "" - ).withGeneratedUrl(for: MockEndpoint.mock) + ) } extension Network.RequestCategory: @retroactive Mocked { diff --git a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift index f8f7408dbc..78d9435aee 100644 --- a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift @@ -8,6 +8,7 @@ import SessionUIKit import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit +import TestUtilities @testable import Session @@ -33,7 +34,7 @@ class ThreadNotificationSettingsViewModelSpec: AsyncSpec { .thenReturn(nil) } ) - @TestState(singleton: .notificationsManager, in: dependencies) var mockNotificationsManager: MockNotificationsManager! = .create() + @TestState(singleton: .notificationsManager, in: dependencies) var mockNotificationsManager: MockNotificationsManager! = .create(using: dependencies) @TestState var viewModel: ThreadNotificationSettingsViewModel! @TestState var cancellables: [AnyCancellable]! @@ -433,7 +434,7 @@ class ThreadNotificationSettingsViewModelSpec: AsyncSpec { mutedUntil: Date.distantFuture.timeIntervalSince1970 ) } - .wasCalled(exactly: 1, timeout: .milliseconds(100)) + .wasCalled(exactly: 1, timeout: .milliseconds(50)) } } } diff --git a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift index 33c73808f7..9b63141f02 100644 --- a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift @@ -7,6 +7,7 @@ import Nimble import SessionUIKit import SessionNetworkingKit import SessionUtilitiesKit +import TestUtilities @testable import SessionUIKit @testable import SessionMessagingKit @@ -31,7 +32,7 @@ class ThreadSettingsViewModelSpec: AsyncSpec { customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState var mockGeneralCache: MockGeneralCache! = .create() + @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( initialSetup: { jobRunner in jobRunner @@ -46,8 +47,8 @@ class ThreadSettingsViewModelSpec: AsyncSpec { } ) @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() - @TestState var mockSnodeAPICache: MockSnodeAPICache! = MockSnodeAPICache() + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create(using: dependencies) + @TestState var mockSnodeAPICache: MockSnodeAPICache! = .create(using: dependencies) @TestState var threadVariant: SessionThread.Variant! = .contact @TestState var didTriggerSearchCallbackTriggered: Bool! = false @TestState var viewModel: ThreadSettingsViewModel! @@ -91,10 +92,10 @@ class ThreadSettingsViewModelSpec: AsyncSpec { dependencies.set(cache: .libSession, to: mockLibSessionCache) var timestampMs: Int64 = 1234567890000 - mockSnodeAPICache.when { $0.clockOffsetMs }.thenReturn(0) - mockSnodeAPICache + try await mockSnodeAPICache.when { $0.clockOffsetMs }.thenReturn(0) + try await mockSnodeAPICache .when { $0.currentOffsetTimestampMs() } - .thenReturn { _, _ in + .thenReturn { _ in /// **Note:** We need to increment this value every time it's accessed because otherwise any functions which /// insert multiple `Interaction` values can end up running into unique constraint conflicts due to the timestamp /// being identical between different interactions @@ -755,7 +756,7 @@ class ThreadSettingsViewModelSpec: AsyncSpec { message: try GroupUpdateInfoChangeMessage( changeType: .name, updatedName: "TestNewGroupName", - sentTimestampMs: UInt64(1234567890002), + sentTimestampMs: UInt64(1234567890001), authMethod: Authentication.groupAdmin( groupSessionId: SessionId(.group, hex: groupPubkey), ed25519SecretKey: [1, 2, 3] diff --git a/SessionTests/Database/DatabaseSpec.swift b/SessionTests/Database/DatabaseSpec.swift index 4b5c1876d3..3836fdc61f 100644 --- a/SessionTests/Database/DatabaseSpec.swift +++ b/SessionTests/Database/DatabaseSpec.swift @@ -7,6 +7,7 @@ import Nimble import SessionUtil import SessionUIKit import SessionNetworkingKit +import TestUtilities @testable import Session @testable import SessionMessagingKit @@ -29,7 +30,7 @@ class DatabaseSpec: AsyncSpec { @TestState(singleton: .fileManager, in: dependencies) var mockFileManager: MockFileManager! = MockFileManager( initialSetup: { $0.defaultInitialSetup() } ) - @TestState var mockGeneralCache: MockGeneralCache! = .create() + @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) @TestState var libSessionCache: LibSession.Cache! = LibSession.Cache( userSessionId: SessionId(.standard, hex: TestConstants.publicKey), using: dependencies diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index 1b8b46aa6f..3ba6c0f9c1 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -27,13 +27,13 @@ class OnboardingSpec: AsyncSpec { customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() - @TestState var mockGeneralCache: MockGeneralCache! = .create() + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create(using: dependencies) + @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) @TestState var mockLibSession: MockLibSessionCache! = MockLibSessionCache() - @TestState var mockUserDefaults: MockUserDefaults! = .create() - @TestState var mockNetwork: MockNetwork! = .create() - @TestState var mockExtensionHelper: MockExtensionHelper! = .create() - @TestState var mockSnodeAPICache: MockSnodeAPICache! = MockSnodeAPICache() + @TestState var mockUserDefaults: MockUserDefaults! = .create(using: dependencies) + @TestState var mockNetwork: MockNetwork! = .create(using: dependencies) + @TestState var mockExtensionHelper: MockExtensionHelper! = .create(using: dependencies) + @TestState var mockSnodeAPICache: MockSnodeAPICache! = .create(using: dependencies) @TestState var disposables: [AnyCancellable]! = [] @TestState var manager: Onboarding.Manager! @@ -55,7 +55,7 @@ class OnboardingSpec: AsyncSpec { .thenReturn(nil) dependencies.set(cache: .libSession, to: mockLibSession) - mockSnodeAPICache.defaultInitialSetup() + try await mockSnodeAPICache.defaultInitialSetup() dependencies.set(cache: .snodeAPI, to: mockSnodeAPICache) try await mockStorage.perform(migrations: SNMessagingKit.migrations) @@ -543,7 +543,7 @@ class OnboardingSpec: AsyncSpec { overallTimeout: nil ) } - .wasCalled(exactly: 1, timeout: .milliseconds(100)) + .wasCalled(exactly: 1, timeout: .milliseconds(50)) } // MARK: -- the display name stream to output the correct value diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 0d3fbc63f2..59b3c63fa8 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -580,7 +580,7 @@ open class Storage { /// Do this outside of the actually db operation as it's more for debugging queries running on the main thread /// than trying to slow the query itself - if !SNUtilitiesKit.isRunningTests && dependencies[feature: .forceSlowDatabaseQueries] { + if dependencies[feature: .forceSlowDatabaseQueries] { try await Task.sleep(for: .seconds(1)) } diff --git a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift index 06cb74537d..67b91c6772 100644 --- a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift +++ b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift @@ -5,7 +5,7 @@ import Foundation import Combine -public class Dependencies { +public class Dependencies: FeatureStorageType { static let userInfoKey: CodingUserInfoKey = CodingUserInfoKey(rawValue: "session.dependencies.codingOptions")! /// The `isRTLRetriever` is handled differently from normal dependencies because it's not really treated as such (it's more of @@ -21,7 +21,7 @@ public class Dependencies { public subscript(singleton singleton: SingletonConfig) -> S { getOrCreate(singleton) } public subscript(cache cache: CacheConfig) -> I { getOrCreate(cache).immutable(cache: cache, using: self) } public subscript(defaults defaults: UserDefaultsConfig) -> UserDefaultsType { getOrCreate(defaults) } - public subscript(feature feature: FeatureConfig) -> T { getOrCreate(feature).currentValue(using: self) } + public subscript(feature feature: FeatureConfig) -> T { getOrCreate(feature).currentValue(in: self) } // MARK: - Global Values, Timing and Async Handling @@ -172,6 +172,19 @@ public class Dependencies { public func waitUntilInitialised(cache: CacheConfig) async throws { try await waitUntilInitialised(targetKey: DependencyStorage.Key.Variant.cache.key(cache.identifier)) } + + // MARK: - FeatureStorageType + + public var hardfork: Int { self[defaults: .standard, key: .hardfork] } + public var softfork: Int { self[defaults: .standard, key: .hardfork] } + + public func rawFeatureValue(forKey defaultName: String) -> Any? { + return self[defaults: .appGroup].object(forKey: defaultName) + } + + public func storeFeatureValue(_ value: Any?, forKey defaultName: String) { + return self[defaults: .appGroup].set(value, forKey: defaultName) + } } // MARK: - Cache Management @@ -211,7 +224,7 @@ public extension Dependencies { typedValue?.value(as: Feature.self) ?? feature.createInstance(self) ) - instance.setValue(to: updatedFeature, using: self) + instance.setValue(to: updatedFeature, in: self) setValue(instance, typedStorage: .feature(instance), key: feature.identifier) /// Notify observers @@ -229,7 +242,7 @@ public extension Dependencies { _storage.perform { storage in storage.instances[key]? .value(as: Feature.self)? - .setValue(to: nil, using: self) + .setValue(to: nil, in: self) } removeValue(feature.identifier, of: .feature) diff --git a/SessionUtilitiesKit/General/Feature.swift b/SessionUtilitiesKit/General/Feature.swift index 0e512347e0..8fd4802c67 100644 --- a/SessionUtilitiesKit/General/Feature.swift +++ b/SessionUtilitiesKit/General/Feature.swift @@ -161,14 +161,14 @@ public struct Feature: FeatureType { return (dependencies[defaults: .appGroup].object(forKey: identifier) != nil) } - internal func currentValue(using dependencies: Dependencies) -> T { + internal func currentValue(in featureStorage: any FeatureStorageType) -> T { let maybeSelectedOption: T? = { // `Int` defaults to `0` and `Bool` defaults to `false` so rather than those (in case we want // a default value that isn't `0` or `false` which might be considered valid cases) we check // if an entry exists and return `nil` if not before retrieving an `Int` representation of // the value and converting to the desired type - guard dependencies[defaults: .appGroup].object(forKey: identifier) != nil else { return nil } - guard let selectedOption: T.RawValue = dependencies[defaults: .appGroup].object(forKey: identifier) as? T.RawValue else { + guard featureStorage.rawFeatureValue(forKey: identifier) != nil else { return nil } + guard let selectedOption: T.RawValue = featureStorage.rawFeatureValue(forKey: identifier) as? T.RawValue else { Log.error("Unable to retrieve feature option for \(identifier) due to incorrect storage type") return nil } @@ -181,11 +181,12 @@ public struct Feature: FeatureType { guard let selectedOption: T = maybeSelectedOption, selectedOption.isValidOption else { func automaticChangeConditionMet(_ condition: ChangeCondition) -> Bool { switch condition { - case .after(let timestamp): return (dependencies.dateNow.timeIntervalSince1970 >= timestamp) + case .after(let timestamp): + return (featureStorage.dateNow.timeIntervalSince1970 >= timestamp) case .afterFork(let hard, let soft): - let currentHardFork: Int = dependencies[defaults: .standard, key: .hardfork] - let currentSoftFork: Int = dependencies[defaults: .standard, key: .softfork] + let currentHardFork: Int = featureStorage.hardfork + let currentSoftFork: Int = featureStorage.softfork let currentVersion: Version = Version(major: currentHardFork, minor: currentSoftFork, patch: 0) let requiredVersion: Version = Version(major: hard, minor: soft, patch: 0) @@ -218,11 +219,22 @@ public struct Feature: FeatureType { return selectedOption } - internal func setValue(to updatedValue: T?, using dependencies: Dependencies) { - dependencies[defaults: .appGroup].set(updatedValue?.rawValue, forKey: identifier) + internal func setValue(to updatedValue: T?, in featureStorage: any FeatureStorageType) { + featureStorage.storeFeatureValue(updatedValue?.rawValue, forKey: identifier) } } +// MARK: - Feature Storage + +public protocol FeatureStorageType { + var hardfork: Int { get } + var softfork: Int { get } + var dateNow: Date { get } + + func rawFeatureValue(forKey defaultName: String) -> Any? + func storeFeatureValue(_ value: Any?, forKey defaultName: String) +} + // MARK: - Convenience public struct FeatureValue { diff --git a/SessionUtilitiesKitTests/General/GeneralCacheSpec.swift b/SessionUtilitiesKitTests/General/GeneralCacheSpec.swift index c09bbcc568..ade2bd6e3a 100644 --- a/SessionUtilitiesKitTests/General/GeneralCacheSpec.swift +++ b/SessionUtilitiesKitTests/General/GeneralCacheSpec.swift @@ -1,6 +1,7 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import Foundation +import TestUtilities import Quick import Nimble @@ -12,7 +13,7 @@ class GeneralCacheSpec: AsyncSpec { // MARK: Configuration @TestState var dependencies: TestDependencies! = TestDependencies() - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create() + @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create(using: dependencies) beforeEach { try await mockCrypto diff --git a/SessionUtilitiesKitTests/_TestUtilities/Mocked+SUK.swift b/SessionUtilitiesKitTests/_TestUtilities/Mocked+SUK.swift index 0afc50cecf..f214a050cb 100644 --- a/SessionUtilitiesKitTests/_TestUtilities/Mocked+SUK.swift +++ b/SessionUtilitiesKitTests/_TestUtilities/Mocked+SUK.swift @@ -7,7 +7,14 @@ import TestUtilities @testable import SessionUtilitiesKit -extension SessionId { static var any: SessionId { SessionId.invalid } } +extension SessionId: @retroactive Mocked { + public static let any: SessionId = SessionId(.standard, publicKey: [255, 255, 255, 255, 255]) + public static let mock: SessionId = SessionId(.standard, publicKey: [ + 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8 + ]) +} + extension Dependencies { static var any: Dependencies { TestDependencies { dependencies in diff --git a/TestUtilities/MockError.swift b/TestUtilities/MockError.swift index 0bc0ab6112..6598de8291 100644 --- a/TestUtilities/MockError.swift +++ b/TestUtilities/MockError.swift @@ -8,10 +8,17 @@ public enum MockError: Error, CustomStringConvertible { case any case mock case noStubFound(function: String, args: [Any?]) - case stubbedValueIsWrongType(expected: Any.Type, actual: Any.Type?) - case cannotCreateDummyValue(type: Any.Type, function: String) + case noMatchingStubFound(function: String, expectedArgs: [Any?], mockedArgs: [[Any?]]) + case stubbedValueIsWrongType(function: String, expected: Any.Type, actual: Any.Type?) case invalidWhenBlock(message: String) + public var shouldLogFailure: Bool { + switch self { + case .noStubFound, .noMatchingStubFound: return true + default: return false + } + } + public var description: String { switch self { case .any: return "AnyError" @@ -20,16 +27,34 @@ public enum MockError: Error, CustomStringConvertible { let argsDescription: String = args.map { summary(for: $0) }.joined(separator: ", ") return "MockError: No stub found for '\(function)(\(argsDescription))'. Use .when { ... } to provide a return value or action." - - case .stubbedValueIsWrongType(let expected, let actual): - return "MockError: A stub for this function was found, but its return value is the wrong type. Expected '\(expected)', but found '\(actual.map { "\($0)" } ?? "nil")'." - case .cannotCreateDummyValue(let type, let function): - return "MockError: The function '\(function)' being stubbed returns a non-optional value of type '\(type)', which does not conform to the 'Mocked' protocol. Please add conformance to provide a default value." + case .noMatchingStubFound(let function, let expectedArgs, let mockedArgs): + guard !expectedArgs.isEmpty && !mockedArgs.isEmpty else { + return MockError.noStubFound(function: function, args: []).description + } + + var errorDescription: String = "MockError: A stub for \(function) was found, but the parameters didn't match." + + errorDescription += "\n\nCalled with parameters:" + let args: String = expectedArgs.map { summary(for: $0) }.joined(separator: ", ") + errorDescription += "\n- [\(args)]" + + let callDescriptions: String = mockedArgs + .map { args in + let argString: String = args.map { summary(for: $0) }.joined(separator: ", ") + + return "- [\(argString)]" + } + .joined(separator: "\n") + errorDescription += "\n\nAll stubs:\n\(callDescriptions)" + + return errorDescription + + case .stubbedValueIsWrongType(let function, let expected, let actual): + return "MockError: A stub for \(function) was found, but its return value is the wrong type. Expected '\(expected)', but found '\(actual.map { "\($0)" } ?? "nil")'." case .invalidWhenBlock(let message): return "MockError: Invalid `when` block. \(message)" - } } } diff --git a/TestUtilities/MockFunction.swift b/TestUtilities/MockFunction.swift index 2806ff5f4b..94d9596789 100644 --- a/TestUtilities/MockFunction.swift +++ b/TestUtilities/MockFunction.swift @@ -1,6 +1,4 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable import Foundation @@ -9,14 +7,18 @@ internal final class MockFunction { let generics: [Any.Type] let arguments: [Any?] let returnValue: Any? + let dynamicReturnValueRetriever: (([Any?]) -> Any?)? let returnError: (any Error)? let actions: [([Any?]) -> Void] + var asCall: RecordedCall { RecordedCall(name: name, generics: generics, arguments: arguments) } + init( name: String, generics: [Any.Type], arguments: [Any?], returnValue: Any?, + dynamicReturnValueRetriever: (([Any?]) -> Any?)?, returnError: Error?, actions: [([Any?]) -> Void] ) { @@ -24,140 +26,8 @@ internal final class MockFunction { self.generics = generics self.arguments = arguments self.returnValue = returnValue + self.dynamicReturnValueRetriever = dynamicReturnValueRetriever self.returnError = returnError self.actions = actions } - - func matches(args: [Any?]) -> Bool { - guard args.count == arguments.count else { return false } - - for (stubArg, callArg) in zip(arguments, args) { - if !argumentMatches(stubArg: stubArg, callArg: callArg) { - return false - } - } - - return true - } - - private func isEquatableMatch(lhs: E, rhs: Any) -> Bool { - if let rhs = rhs as? E { - return lhs == rhs - } - - return false - } - - private func isAnyValue(_ value: Any) -> Bool { - func open(value: T) -> Bool { - if let mockedEquatable = value as? any Equatable { - return isEquatableMatch(lhs: mockedEquatable, rhs: T.any) - } - - /// Compare using the `summary` as a fallback - return summary(for: value) == summary(for: T.any) - } - - if let mockedValue = value as? any Mocked { - return open(value: mockedValue) - } - - return false - } - - private func argumentMatches(stubArg: Any?, callArg: Any?) -> Bool { - func isWildcardMatch(_ value: T) -> Bool { - if isAnyValue(value) { - /// The value is an `any` so check if the mocked type is a "super" wildcard (like `MockEndpoint.any` that - /// matches any other value - if T.skipTypeMatchForAnyComparison { - return true - } - - /// Otherwise the types need to match - return callArg is T - } - - /// Not a wildcard - return false - } - - switch (stubArg, callArg) { - case (.none, .none): return true /// Too hard to compare `nil == nil` after type erasure - case (.none, .some): return false /// Expected `nil`, given a value - case (.some(let lhs), .none): return isAnyValue(lhs) /// Allow `any == nil` - case (.some(let stub), .some(let call)): - /// If the `stubArg` is `Mocked.any` then we want to match anything - if let mockedStub = stub as? any Mocked, isWildcardMatch(mockedStub) { - return true - } - - /// Check if there is an equatable match first (for performance reasons) - if let equatableValue = stub as? any Equatable, isEquatableMatch(lhs: equatableValue, rhs: call) { - return true - } - - /// Otherwise we need to use reflection to to a nested equality check (just in case a child element is a wildcard) - let mirrorLhs: Mirror = Mirror(reflecting: stub) - let mirrorRhs: Mirror = Mirror(reflecting: call) - - /// Since the `stub` isn't a wildcard the types need to match - guard String(describing: mirrorLhs.subjectType) == String(describing: mirrorRhs.subjectType) else { - return false - } - - switch mirrorLhs.displayStyle { - case .struct, .class, .tuple, .collection, .dictionary, .enum: - let childrenLhs: [Mirror.Child] = Array(mirrorLhs.children) - let childrenRhs: [Mirror.Child] = Array(mirrorRhs.children) - - /// If they are simple enums with no associated types then just compare the `summary` value - if childrenLhs.isEmpty && childrenRhs.isEmpty && mirrorLhs.displayStyle == .enum { - return summary(for: stub) == summary(for: call) - } - - /// Check enum case names are the same if applicable - if mirrorLhs.displayStyle == .enum { - let caseNameLhs: String = String(describing: stub).before(first: "(") - let caseNameRhs: String = String(describing: call).before(first: "(") - - if caseNameLhs != caseNameRhs { - return false - } - } - - /// If the number of args differ then there isn't a match - guard childrenLhs.count == childrenRhs.count else { return false } - - /// If any of the arguments don't match (recursively) then there is no match - for i in 0.. String { - if let index: String.Index = firstIndex(of: delimiter) { - return String(prefix(upTo: index)) - } - - return self - } } diff --git a/TestUtilities/MockFunctionBuilder.swift b/TestUtilities/MockFunctionBuilder.swift index db207f155c..39f7710594 100644 --- a/TestUtilities/MockFunctionBuilder.swift +++ b/TestUtilities/MockFunctionBuilder.swift @@ -8,7 +8,7 @@ internal protocol Buildable { public class MockFunctionBuilder { private let handler: MockHandler - private let callBlock: (T) async throws -> R + private let callBlock: (inout T) async throws -> R private let dummyProvider: (any MockFunctionHandler) -> T private var capturedFunctionName: String? @@ -16,12 +16,13 @@ public class MockFunctionBuilder { private var capturedArguments: [Any?] = [] private var returnValue: Any? + private var dynamicReturnValueRetriever: (([Any?]) -> Any?)? private var returnError: Error? private var actions: [([Any?]) -> Void] = [] public init( handler: MockHandler, - callBlock: @escaping (T) async throws -> R, + callBlock: @escaping (inout T) async throws -> R, dummyProvider: @escaping (any MockFunctionHandler) -> T ) { self.handler = handler @@ -41,6 +42,11 @@ public class MockFunctionBuilder { try await finalize() } + public func thenReturn(_ closure: @escaping ([Any?]) -> R) async throws { + self.dynamicReturnValueRetriever = closure + try await finalize() + } + public func thenThrow(_ error: Error) async throws { self.returnError = error try await finalize() @@ -57,8 +63,8 @@ public class MockFunctionBuilder { /// Only run capture once guard capturedFunctionName == nil else { return } - let dummy: T = dummyProvider(self) - _ = try? await callBlock(dummy) + var dummy: T = dummyProvider(self) + _ = try? await callBlock(&dummy) } } @@ -75,6 +81,7 @@ extension MockFunctionBuilder: Buildable { generics: capturedGenerics, arguments: capturedArguments, returnValue: returnValue, + dynamicReturnValueRetriever: dynamicReturnValueRetriever, returnError: returnError, actions: actions ) diff --git a/TestUtilities/MockHandler.swift b/TestUtilities/MockHandler.swift index b8fb3537c8..1d5672f25d 100644 --- a/TestUtilities/MockHandler.swift +++ b/TestUtilities/MockHandler.swift @@ -3,25 +3,30 @@ import Foundation public final class MockHandler { + public let erasedDependencies: Any? + private let lock = NSLock() private let dummyProvider: (any MockFunctionHandler) -> T private let failureReporter: TestFailureReporter private let forwardingHandler: (any MockFunctionHandler)? - private var stubs: [Key: [MockFunction]] = [:] - private var calls: [Key: [RecordedCall]] = [:] + private var stubs: [RecordedCall.Key: [MockFunction]] = [:] + private var calls: [RecordedCall.Key: [RecordedCall]] = [:] // MARK: - Initialization public init( dummyProvider: @escaping (any MockFunctionHandler) -> T, - failureReporter: TestFailureReporter = NimbleFailureReporter() + failureReporter: TestFailureReporter = NimbleFailureReporter(), + using erasedDependencies: Any? ) { + self.erasedDependencies = erasedDependencies self.dummyProvider = dummyProvider self.failureReporter = failureReporter self.forwardingHandler = nil } public init(forwardingHandler: any MockFunctionHandler) { + self.erasedDependencies = nil self.dummyProvider = { _ in fatalError("A dummy instance cannot create other dummies.") } self.failureReporter = NimbleFailureReporter() self.forwardingHandler = forwardingHandler @@ -29,12 +34,12 @@ public final class MockHandler { public static func invalid() -> MockHandler { - return MockHandler(dummyProvider: { _ in fatalError("Should not call mock on a mock") } ) + return MockHandler(dummyProvider: { _ in fatalError("Should not call mock on a mock") }, using: nil) } // MARK: - Setup - func createBuilder(for callBlock: @escaping (T) async throws -> R) -> MockFunctionBuilder { + func createBuilder(for callBlock: @escaping (inout T) async throws -> R) -> MockFunctionBuilder { return MockFunctionBuilder( handler: self, callBlock: callBlock, @@ -43,32 +48,28 @@ public final class MockHandler { } internal func register(stub: MockFunction) { - let key: Key = Key(name: stub.name, generics: stub.generics, paramCount: stub.arguments.count) + let key: RecordedCall.Key = RecordedCall.Key( + name: stub.name, + generics: stub.generics, + paramCount: stub.arguments.count + ) locked { stubs[key, default: []].append(stub) } } - internal func removeStubs(for functionBlock: @escaping (T) async throws -> R) async { - let builder: MockFunctionBuilder = createBuilder(for: functionBlock) - - guard let builtFunction: MockFunction = try? await builder.build() else { return } - - let key: Key = Key( - name: builtFunction.name, - generics: builtFunction.generics, - paramCount: builtFunction.arguments.count - ) + internal func removeStubs(for functionBlock: @escaping (inout T) async throws -> R) async { + guard let expectedCall: RecordedCall = await expectedCall(for: functionBlock) else { return } locked { - stubs.removeValue(forKey: key) + stubs.removeValue(forKey: expectedCall.key) } } // MARK: - Verification - func expectedCall(for functionBlock: @escaping (T) async throws -> R) async -> RecordedCall? { + func expectedCall(for functionBlock: @escaping (inout T) async throws -> R) async -> RecordedCall? { let builder: MockFunctionBuilder = createBuilder(for: functionBlock) guard let builtFunction = try? await builder.build() else { @@ -76,45 +77,37 @@ public final class MockHandler { } return RecordedCall( - name: builtFunction.name, - args: builtFunction.arguments - ) - } - - func recordedCalls(for functionBlock: @escaping (T) async throws -> R) async -> [RecordedCall]? { - let builder: MockFunctionBuilder = createBuilder(for: functionBlock) - - guard let builtFunction = try? await builder.build() else { - return nil - } - - let key: Key = Key( name: builtFunction.name, generics: builtFunction.generics, - paramCount: builtFunction.arguments.count + arguments: builtFunction.arguments ) - - guard let callsForKey: [RecordedCall] = locked({ calls[key] }) else { return [] } - - return callsForKey.filter { builtFunction.matches(args: $0.args) } } - func allRecordedCalls(for functionBlock: @escaping (T) async throws -> R) async -> [RecordedCall]? { + typealias CallInfo = ( + expected: RecordedCall, + matching: [RecordedCall], + all: [RecordedCall] + ) + + func recordedCallInfo(for functionBlock: @escaping (inout T) async throws -> R) async -> CallInfo? { let builder: MockFunctionBuilder = createBuilder(for: functionBlock) guard let builtFunction = try? await builder.build() else { return nil } - let key: Key = Key( + let expectedCall: RecordedCall = RecordedCall( name: builtFunction.name, generics: builtFunction.generics, - paramCount: builtFunction.arguments.count + arguments: builtFunction.arguments ) + let allCalls: [RecordedCall] = (locked { calls[expectedCall.key] } ?? []) - return locked { - calls[key] - } + return ( + expectedCall, + allCalls.filter { expectedCall.matches(args: $0.arguments) }, + allCalls + ) } // MARK: - Test Lifecycle @@ -137,34 +130,46 @@ public final class MockHandler { private func findAndExecute( funcName: String, generics: [Any.Type], - args: [Any?], - fileID: String, - file: String, - line: UInt + args: [Any?] ) -> Result { - let key: Key = Key(name: funcName, generics: generics, paramCount: args.count) - let recordedCall: RecordedCall = RecordedCall(name: funcName, args: args) + typealias CallMatches = ( + matchingCall: MockFunction?, + allCalls: [MockFunction] + ) + let recordedCall: RecordedCall = RecordedCall(name: funcName, generics: generics, arguments: args) /// Get the `last` value as it was the one called most recently - let maybeMatchingCall: MockFunction? = locked { - calls[key, default: []].append(recordedCall) + let maybeCallMatches: CallMatches? = locked { + calls[recordedCall.key, default: []].append(recordedCall) - return stubs[key]?.last(where: { $0.matches(args: args) }) + return stubs[recordedCall.key].map { allStubs in + ( + allStubs.last(where: { $0.asCall.matches(args: args) }), + allStubs + ) + } } - guard let matchingCall: MockFunction = maybeMatchingCall else { + guard let callMatches: CallMatches = maybeCallMatches else { return .failure(MockError.noStubFound(function: funcName, args: args)) } + guard let matchingCall: MockFunction = callMatches.matchingCall else { + return .failure(MockError.noMatchingStubFound( + function: funcName, + expectedArgs: args, + mockedArgs: callMatches.allCalls.map { $0.arguments } + )) + } /// Perform any actions for action in matchingCall.actions { action(args) } - return execute(stub: matchingCall) + return execute(stub: matchingCall, args: args) } - private func execute(stub: MockFunction) -> Result { + private func execute(stub: MockFunction, args: [Any?]) -> Result { if let error: Error = stub.returnError { return .failure(error) } @@ -174,12 +179,18 @@ public final class MockHandler { return .success(() as! Output) } + /// Try the `dynamicReturnValueRetriever` if there is one + if let returnValue: Any = stub.dynamicReturnValueRetriever?(args), let typedValue: Output = returnValue as? Output { + return .success(typedValue) + } + /// Then handle the proper typed return value if let returnValue: Any = stub.returnValue, let typedValue: Output = returnValue as? Output { return .success(typedValue) } return .failure(MockError.stubbedValueIsWrongType( + function: stub.name, expected: Output.self, actual: type(of: stub.returnValue) )) @@ -217,14 +228,11 @@ public extension MockHandler { return forwardedHandler.mock(funcName: funcName, generics: generics, args: args) } - return handlingNonThrowingResult( + return handleNonThrowingResult( result: findAndExecute( funcName: funcName, generics: generics, - args: args, - fileID: fileID, - file: file, - line: line + args: args ), funcName: funcName, fileID: fileID, @@ -259,10 +267,7 @@ public extension MockHandler { return try findAndExecute( funcName: funcName, generics: generics, - args: args, - fileID: fileID, - file: file, - line: line + args: args ).get() } @@ -277,7 +282,7 @@ public extension MockHandler { let _: Void = try mockThrowing(funcName: funcName, generics: generics, args: args, fileID: fileID, file: file, line: line) } - private func handlingNonThrowingResult( + private func handleNonThrowingResult( result: Result, funcName: String, fileID: String, @@ -288,9 +293,9 @@ public extension MockHandler { case .success(let value): return value case .failure(let error): /// Log if the failure was due to a missing mock - if case MockError.noStubFound(_, _) = error { + if (error as? MockError)?.shouldLogFailure == true { failureReporter.reportFailure( - "Mocking Error: An unstubbed function was called: `\(funcName)`", + "\(error)", fileID: fileID, file: file, line: line diff --git a/TestUtilities/Mockable.swift b/TestUtilities/Mockable.swift index 4534e16b3f..51160535d4 100644 --- a/TestUtilities/Mockable.swift +++ b/TestUtilities/Mockable.swift @@ -12,21 +12,22 @@ public protocol Mockable { } public extension Mockable { - static func create() -> M { + static func create(using erasedDependencies: Any?) -> M { let handler: MockHandler = MockHandler( dummyProvider: { builderHandler in return M(handlerForBuilder: builderHandler) as! M.MockedType - } + }, + using: erasedDependencies ) return M(handler: handler) } - func when(_ callBlock: @escaping (MockedType) async throws -> R) -> MockFunctionBuilder { + func when(_ callBlock: @escaping (inout MockedType) async throws -> R) -> MockFunctionBuilder { return handler.createBuilder(for: callBlock) } - func removeMocksFor(_ callBlock: @escaping (MockedType) async throws -> R) async { + func removeMocksFor(_ callBlock: @escaping (inout MockedType) async throws -> R) async { await handler.removeStubs(for: callBlock) } } diff --git a/TestUtilities/Mocked.swift b/TestUtilities/Mocked.swift index e9dbb74719..cba18c8143 100644 --- a/TestUtilities/Mocked.swift +++ b/TestUtilities/Mocked.swift @@ -103,6 +103,11 @@ extension Dictionary: Mocked { return [hashableKey: anyValue] as! Self } + /// Try to handle generic dictionaries + if Key.self == AnyHashable.self && (Value.self == AnyHashable.self || Value.self == Any.self) { + return [String.any: String.any] as! Self + } + return [:] } public static var mock: Self { [:] } @@ -129,7 +134,7 @@ extension Set: Mocked { } extension UIApplication.State: Mocked { - public static let any: UIApplication.State = .active + public static let any: UIApplication.State = UIApplication.State(rawValue: .any)! public static let mock: UIApplication.State = .active } extension UnsafeMutablePointer?: Mocked { @@ -168,16 +173,16 @@ extension AsyncStream: Mocked { } extension FileManager.ItemReplacementOptions: Mocked { - public static let any: FileManager.ItemReplacementOptions = FileManager.ItemReplacementOptions() + public static let any: FileManager.ItemReplacementOptions = FileManager.ItemReplacementOptions(rawValue: .any) public static let mock: FileManager.ItemReplacementOptions = FileManager.ItemReplacementOptions() } extension FileProtectionType: Mocked { - public static let any: FileProtectionType = .complete + public static let any: FileProtectionType = FileProtectionType(rawValue: .any) public static let mock: FileProtectionType = .complete } extension Data.WritingOptions: Mocked { - public static let any: Data.WritingOptions = .fileProtectionMask + public static let any: Data.WritingOptions = Data.WritingOptions(rawValue: .any) public static let mock: Data.WritingOptions = .atomic } diff --git a/TestUtilities/Nimble/NimbleVerification.swift b/TestUtilities/Nimble/NimbleVerification.swift index 83c8500e82..b10fe28ca2 100644 --- a/TestUtilities/Nimble/NimbleVerification.swift +++ b/TestUtilities/Nimble/NimbleVerification.swift @@ -8,7 +8,7 @@ internal import Nimble public struct NimbleVerification { fileprivate struct VerificationData { fileprivate let mock: M - fileprivate let callBlock: (M.MockedType) async throws -> R + fileprivate let callBlock: (inout M.MockedType) async throws -> R } fileprivate let data: VerificationData @@ -25,7 +25,11 @@ public struct NimbleVerification { } else { await expect(fileID: fileID, file: file, line: line, self.data) - .toEventually(beCalled(exactly: times), timeout: timeout.nimbleInterval) + .toEventually( + beCalled(exactly: times), + timeout: timeout.nimbleInterval, + description: "Timed out waiting for call count to be \(times)." + ) } } @@ -41,7 +45,11 @@ public struct NimbleVerification { } else { await expect(fileID: fileID, file: file, line: line, self.data) - .toEventually(beCalled(atLeast: times), timeout: timeout.nimbleInterval) + .toEventually( + beCalled(atLeast: times), + timeout: timeout.nimbleInterval, + description: "Timed out waiting for call count to be at least \(times)." + ) } } @@ -56,13 +64,17 @@ public struct NimbleVerification { } else { await expect(fileID: fileID, file: file, line: line, self.data) - .toEventually(beCalled(exactly: 0), timeout: timeout.nimbleInterval) + .toEventually( + beCalled(exactly: 0), + timeout: timeout.nimbleInterval, + description: "Timed out waiting for call count to be 0." + ) } } } public extension Mockable { - func verify(_ callBlock: @escaping (MockedType) async throws -> R) async -> NimbleVerification { + func verify(_ callBlock: @escaping (inout MockedType) async throws -> R) async -> NimbleVerification { return NimbleVerification( data: NimbleVerification.VerificationData(mock: self, callBlock: callBlock) ) @@ -74,65 +86,81 @@ private func beCalled( atLeast atLeastTimes: Int? = nil ) -> AsyncMatcher.VerificationData> { return AsyncMatcher { actualExpression in - let message: ExpectationMessage = (atLeastTimes != nil ? - ExpectationMessage.expectedTo("be called at least \(atLeastTimes ?? 1) time(s)") : - ExpectationMessage.expectedTo("be called exactly \(exactTimes ?? 1) time(s)") - ) + let message: ExpectationMessage + + switch (exactTimes, atLeastTimes) { + case (_, 1), (.none, .none): message = ExpectationMessage.expectedTo("be called at least 1 time") + case (_, .some(let atLeastTimes)): + message = ExpectationMessage.expectedTo("be called at least \(atLeastTimes) time(s)") + + case (0, _): message = ExpectationMessage.expectedTo("not be called") + case (1, _): message = ExpectationMessage.expectedTo("be called exactly 1 time") + case (.some(let exactTimes), _): + message = ExpectationMessage.expectedTo("be called exactly \(exactTimes) time(s)") + } guard let info = try await actualExpression.evaluate() else { return MatcherResult(status: .fail, message: message.appendedBeNilHint()) } - let matchingCalls: [RecordedCall] = (await info.mock.handler.recordedCalls(for: info.callBlock) ?? []) + let callInfo: MockHandler.CallInfo? = await info.mock.handler.recordedCallInfo(for: info.callBlock) switch (exactTimes, atLeastTimes) { case (.some(let times), _): - if matchingCalls.count == times { + if callInfo?.matching.count == times { return MatcherResult(status: .matches, message: message) } case (_, .some(let times)): - if matchingCalls.count >= times { + if (callInfo?.matching.count ?? 0) >= times { return MatcherResult(status: .matches, message: message) } case (.none, .none): - if matchingCalls.count >= 1 { + if (callInfo?.matching.count ?? 0) >= 1 { return MatcherResult(status: .matches, message: message) } } - let maybeExpectedCall: RecordedCall? = await info.mock.handler.expectedCall(for: info.callBlock) - let maybeAllCalls: [RecordedCall]? = await info.mock.handler.allRecordedCalls(for: info.callBlock) - let funcName: String? = (maybeExpectedCall?.name ?? maybeAllCalls?.first?.name) - var details: String = "\n Expected to call \(funcName.map { "'\($0)'" } ?? "function") with parameters:" + var details: String = "" - if let expectedCall: RecordedCall = maybeExpectedCall { - let args: String = expectedCall.args.map { summary(for: $0) }.joined(separator: ", ") - details += "\n- [\(args)]" - } - else { - details += "\n- Unable to determine the expected parameters" + if (exactTimes ?? 0) > 0 || (atLeastTimes ?? 0) > 0 { + details += "\nExpected to call \((callInfo?.expected.name).map { "'\($0)'" } ?? "function") with parameters:" + + if let expectedCall: RecordedCall = callInfo?.expected { + let args: String = expectedCall.arguments.map { summary(for: $0) }.joined(separator: ", ") + details += "\n- [\(args)]" + } + else { + details += "\n- Unable to determine the expected parameters" + } + + details += "\n" } - if let allCalls: [RecordedCall] = maybeAllCalls, !allCalls.isEmpty { + if let allCalls: [RecordedCall] = callInfo?.all, !allCalls.isEmpty { let callDescriptions: String = allCalls .map { call in - let args: String = call.args.map { summary(for: $0) }.joined(separator: ", ") + let args: String = call.arguments.map { summary(for: $0) }.joined(separator: ", ") return "- [\(args)]" } .joined(separator: "\n") - details += "\n\nAll calls to this function with different arguments:\n\(callDescriptions)" + details += "\nAll calls to this function with different arguments:\n\(callDescriptions)" } else { - details += "\n\nNo other calls were made to this function." + details += "\nNo other calls were made to this function." } + let gotMessage: String = ((exactTimes ?? 0) > 0 || (atLeastTimes ?? 0) > 0 ? + ", got \(callInfo?.matching.count ?? 0) matching call(s)." : + ", got called \(callInfo?.matching.count ?? 0) time(s)." + ) + return MatcherResult( status: .fail, message: message - .appended(message: ", got \(matchingCalls.count) matching call(s).") + .appended(message: gotMessage) .appended(details: details) ) } diff --git a/TestUtilities/RecordedCall.swift b/TestUtilities/RecordedCall.swift index 912bcfd562..1a70e6c345 100644 --- a/TestUtilities/RecordedCall.swift +++ b/TestUtilities/RecordedCall.swift @@ -1,8 +1,160 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable import Foundation public struct RecordedCall { let name: String - let args: [Any?] + let generics: [Any.Type] + let arguments: [Any?] + + internal var key: Key { Key(name: name, generics: generics, paramCount: arguments.count) } +} + +internal extension RecordedCall { + struct Key: Equatable, Hashable { + let name: String + let generics: [String] + let paramCount: Int + + init(name: String, generics: [Any.Type], paramCount: Int) { + self.name = name + self.generics = generics.map { String(describing: $0) } + self.paramCount = paramCount + } + } + + func matches(args: [Any?]) -> Bool { + guard args.count == arguments.count else { return false } + + for (stubArg, callArg) in zip(arguments, args) { + if !argumentMatches(stubArg: stubArg, callArg: callArg) { + return false + } + } + + return true + } + + private func isEquatableMatch(lhs: E, rhs: Any) -> Bool { + if let rhs = rhs as? E { + return lhs == rhs + } + + return false + } + + private func isAnyValue(_ value: Any) -> Bool { + func open(value: T) -> Bool { + if let mockedEquatable = value as? any Equatable { + return isEquatableMatch(lhs: mockedEquatable, rhs: T.any) + } + + /// Compare using the `summary` as a fallback + return summary(for: value) == summary(for: T.any) + } + + if let mockedValue = value as? any Mocked { + return open(value: mockedValue) + } + + return false + } + + private func argumentMatches(stubArg: Any?, callArg: Any?) -> Bool { + func isWildcardMatch(_ value: T) -> Bool { + if isAnyValue(value) { + /// The value is an `any` so check if the mocked type is a "super" wildcard (like `MockEndpoint.any` that + /// matches any other value + if T.skipTypeMatchForAnyComparison { + return true + } + + /// Otherwise the types need to match + return callArg is T + } + + /// Not a wildcard + return false + } + + switch (stubArg, callArg) { + case (.none, .none): return true /// Too hard to compare `nil == nil` after type erasure + case (.none, .some): return false /// Expected `nil`, given a value + case (.some(let lhs), .none): return isAnyValue(lhs) /// Allow `any == nil` + case (.some(let stub), .some(let call)): + /// If the `stubArg` is `Mocked.any` then we want to match anything + if let mockedStub = stub as? any Mocked, isWildcardMatch(mockedStub) { + return true + } + + /// Check if there is an equatable match first (for performance reasons) + if let equatableValue = stub as? any Equatable, isEquatableMatch(lhs: equatableValue, rhs: call) { + return true + } + + /// Otherwise we need to use reflection to to a nested equality check (just in case a child element is a wildcard) + let mirrorLhs: Mirror = Mirror(reflecting: stub) + let mirrorRhs: Mirror = Mirror(reflecting: call) + + /// Since the `stub` isn't a wildcard the types need to match + guard String(describing: mirrorLhs.subjectType) == String(describing: mirrorRhs.subjectType) else { + return false + } + + switch mirrorLhs.displayStyle { + case .struct, .class, .tuple, .collection, .dictionary, .enum: + let childrenLhs: [Mirror.Child] = Array(mirrorLhs.children) + let childrenRhs: [Mirror.Child] = Array(mirrorRhs.children) + + /// If they are simple enums with no associated types then just compare the `summary` value + if childrenLhs.isEmpty && childrenRhs.isEmpty && mirrorLhs.displayStyle == .enum { + return summary(for: stub) == summary(for: call) + } + + /// Check enum case names are the same if applicable + if mirrorLhs.displayStyle == .enum { + let caseNameLhs: String = String(describing: stub).before(first: "(") + let caseNameRhs: String = String(describing: call).before(first: "(") + + if caseNameLhs != caseNameRhs { + return false + } + } + + /// If the number of args differ then there isn't a match + guard childrenLhs.count == childrenRhs.count else { return false } + + /// If any of the arguments don't match (recursively) then there is no match + for i in 0.. String { + if let index: String.Index = firstIndex(of: delimiter) { + return String(prefix(upTo: index)) + } + + return self + } } diff --git a/_SharedTestUtilities/FixtureBase.swift b/_SharedTestUtilities/FixtureBase.swift index 0f113680bc..8fd9088d35 100644 --- a/_SharedTestUtilities/FixtureBase.swift +++ b/_SharedTestUtilities/FixtureBase.swift @@ -65,6 +65,18 @@ open class FixtureBase { return value } + private func mock(_ creation: (TestDependencies) -> T) -> T { + if let existingMock: T = dependencies.get(other: ObjectIdentifier(T.self)) { + return existingMock + } + + let value: T = creation(dependencies) + (value as? DependenciesSettable)?.setDependencies(dependencies) + dependencies.set(other: ObjectIdentifier(T.self), to: value) + + return value + } + // MARK: - No Dependencies Convenience public func mock( @@ -90,15 +102,19 @@ open class FixtureBase { // MARK: - Mockable Convenience + public func mock() -> R { + return mock { dependencies in R.create(using: dependencies) } + } + public func mock(for singleton: SingletonConfig) -> R { - return mock(for: singleton) { _ in R.create() } + return mock(for: singleton) { dependencies in R.create(using: dependencies) } } public func mock(cache: CacheConfig) -> R { - return mock(cache: cache) { _ in R.create() } + return mock(cache: cache) { dependencies in R.create(using: dependencies) } } public func mock(defaults: UserDefaultsConfig) -> T { - return mock(for: defaults) { _ in T.create() } + return mock(for: defaults) { dependencies in T.create(using: dependencies) } } } diff --git a/_SharedTestUtilities/MockKeychain.swift b/_SharedTestUtilities/MockKeychain.swift index 8b92b49522..3964cad6e9 100644 --- a/_SharedTestUtilities/MockKeychain.swift +++ b/_SharedTestUtilities/MockKeychain.swift @@ -2,36 +2,47 @@ import Foundation import SessionUtilitiesKit +import TestUtilities -class MockKeychain: Mock, KeychainStorageType { +class MockKeychain: KeychainStorageType, Mockable { + public var handler: MockHandler + + required init(handler: MockHandler) { + self.handler = handler + } + + required init(handlerForBuilder: any MockFunctionHandler) { + self.handler = MockHandler(forwardingHandler: handlerForBuilder) + } + func string(forKey key: KeychainStorage.StringKey) throws -> String { - return try mockThrowing(args: [key]) + return try handler.mockThrowing(args: [key]) } func set(string: String, forKey key: KeychainStorage.StringKey) throws { - return try mockThrowing(args: [key]) + return try handler.mockThrowing(args: [key]) } func remove(key: KeychainStorage.StringKey) throws { - return try mockThrowing(args: [key]) + return try handler.mockThrowing(args: [key]) } func data(forKey key: KeychainStorage.DataKey) throws -> Data { - return try mockThrowing(args: [key]) + return try handler.mockThrowing(args: [key]) } func set(data: Data, forKey key: KeychainStorage.DataKey) throws { - return try mockThrowing(args: [key]) + return try handler.mockThrowing(args: [key]) } func remove(key: KeychainStorage.DataKey) throws { - return try mockThrowing(args: [key]) + return try handler.mockThrowing(args: [key]) } - func removeAll() throws { try mockThrowingNoReturn() } + func removeAll() throws { try handler.mockThrowingNoReturn() } func migrateLegacyKeyIfNeeded(legacyKey: String, legacyService: String?, toKey key: KeychainStorage.DataKey) throws { - try mockThrowingNoReturn(args: [legacyKey, legacyService, key]) + try handler.mockThrowingNoReturn(args: [legacyKey, legacyService, key]) } func getOrGenerateEncryptionKey( @@ -41,6 +52,6 @@ class MockKeychain: Mock, KeychainStorageType { legacyKey: String?, legacyService: String? ) throws -> Data { - return try mockThrowing(args: [key, length, cat, legacyKey, legacyService]) + return try handler.mockThrowing(args: [key, length, cat, legacyKey, legacyService]) } } diff --git a/_SharedTestUtilities/TestDependencies.swift b/_SharedTestUtilities/TestDependencies.swift index 7d073da0ea..a2eda674a6 100644 --- a/_SharedTestUtilities/TestDependencies.swift +++ b/_SharedTestUtilities/TestDependencies.swift @@ -12,6 +12,8 @@ public class TestDependencies: Dependencies { @ThreadSafeObject private var cacheInstances: [String: MutableCacheType] = [:] @ThreadSafeObject private var defaultsInstances: [String: (any UserDefaultsType)] = [:] @ThreadSafeObject private var featureInstances: [String: (any FeatureType)] = [:] + @ThreadSafeObject private var featureValues: [String: Any] = [:] + @ThreadSafeObject private var otherInstances: [ObjectIdentifier: Any] = [:] // MARK: - Subscript Access @@ -60,10 +62,10 @@ public class TestDependencies: Dependencies { guard let value: Feature = (featureInstances[feature.identifier] as? Feature) else { let value: Feature = feature.createInstance(self) _featureInstances.performUpdate { $0.setting(feature.identifier, value) } - return value.currentValue(using: self) + return value.currentValue(in: self) } - return value.currentValue(using: self) + return value.currentValue(in: self) } public subscript(feature feature: FeatureConfig) -> T? { @@ -222,6 +224,10 @@ public class TestDependencies: Dependencies { return _featureInstances.performMap { $0[feature.identifier] as? T } } + public func get(other: ObjectIdentifier) -> T? { + return _otherInstances.performMap { $0[other] as? T } + } + public override func set(singleton: SingletonConfig, to instance: S) { (instance as? DependenciesSettable)?.setDependencies(self) _singletonInstances.performUpdate { $0.setting(singleton.identifier, instance) } @@ -242,9 +248,27 @@ public class TestDependencies: Dependencies { _featureInstances.performUpdate { $0.setting(feature.identifier, instance) } } + public func set(other: ObjectIdentifier, to instance: T) { + (instance as? DependenciesSettable)?.setDependencies(self) + _otherInstances.performUpdate { $0.setting(other, instance) } + } + public override func remove(cache: CacheConfig) { _cacheInstances.performUpdate { $0.setting(cache.identifier, nil) } } + + // MARK: - FeatureStorageType + + public override var hardfork: Int { 2 } + public override var softfork: Int { 11 } + + public override func rawFeatureValue(forKey defaultName: String) -> Any? { + return _featureValues.performMap { $0[defaultName] } + } + + public override func storeFeatureValue(_ value: Any?, forKey defaultName: String) { + _featureValues.performUpdate { $0.setting(defaultName, value) } + } } // MARK: - DependenciesSettable From 20feb155eeb116259aa5a211fd8cac95ec36de39 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 15 Sep 2025 09:16:52 +1000 Subject: [PATCH 46/59] Finished fixing up broken tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Updated MockFileManager to use Mockable • Updated MockOGMCache to use Mockable • Updated MockJobRunner to use Mockable • Updated MockLibSessionCache to use Mockable • Removed old mocking system • Fixed some tests that were broken but not caught by the old mocking system • Fixed remaining failing tests --- Session.xcodeproj/project.pbxproj | 88 +- Session/Path/PathVC.swift | 2 +- .../LibSession+SessionMessagingKit.swift | 2 +- .../Open Groups/OpenGroupManager.swift | 18 +- .../Types/NotificationCategory.swift | 2 +- SessionMessagingKit/Utilities/AppSetup.swift | 2 +- .../Utilities/ExtensionHelper.swift | 58 +- .../Utilities/Preferences+Sound.swift | 2 +- .../Crypto/CryptoSMKSpec.swift | 5 +- .../Models/MessageDeduplicationSpec.swift | 3 +- .../Jobs/DisplayPictureDownloadJobSpec.swift | 115 +- .../Jobs/MessageSendJobSpec.swift | 75 +- ...RetrieveDefaultOpenGroupRoomsJobSpec.swift | 68 +- .../LibSession/LibSessionGroupInfoSpec.swift | 57 +- .../LibSessionGroupMembersSpec.swift | 37 +- .../LibSession/LibSessionSpec.swift | 51 +- .../Crypto/CryptoOpenGroupAPISpec.swift | 5 +- .../Open Groups/Models/SOGSMessageSpec.swift | 6 +- .../Open Groups/OpenGroupAPISpec.swift | 16 +- .../Open Groups/OpenGroupManagerSpec.swift | 127 +- .../MessageReceiverGroupsSpec.swift | 130 +- .../MessageSenderGroupsSpec.swift | 145 +- .../MessageSenderSpec.swift | 7 +- .../NotificationsManagerSpec.swift | 52 +- .../Pollers/CommunityPollerManagerSpec.swift | 10 +- .../SessionThreadViewModelSpec.swift | 3 +- .../Utilities/ExtensionHelperSpec.swift | 1162 +++++++++-------- .../_TestUtilities/MockExtensionHelper.swift | 2 +- .../_TestUtilities/MockLibSessionCache.swift | 288 ++-- .../_TestUtilities/MockOGMCache.swift | 25 +- .../_TestUtilities/Mocked+SMK.swift | 15 +- .../Types/RequestCategory.swift | 4 +- .../_TestUtilities/Mocked+SNK.swift | 24 +- ...eadDisappearingMessagesViewModelSpec.swift | 22 +- ...eadNotificationSettingsViewModelSpec.swift | 26 +- .../ThreadSettingsViewModelSpec.swift | 59 +- SessionTests/Database/DatabaseSpec.swift | 12 +- SessionTests/Onboarding/OnboardingSpec.swift | 37 +- .../NotificationContentViewModelSpec.swift | 38 +- SessionUtilitiesKit/JobRunner/JobRunner.swift | 14 +- .../Utilities/MutableIdentifiable.swift | 7 - .../Database/Models/IdentitySpec.swift | 3 +- .../General/GeneralCacheSpec.swift | 3 +- .../JobRunner/JobRunnerSpec.swift | 7 +- .../_TestUtilities}/MockAppContext.swift | 0 .../_TestUtilities}/MockCrypto.swift | 0 .../_TestUtilities/MockFileManager.swift | 167 +++ .../_TestUtilities}/MockGeneralCache.swift | 0 .../_TestUtilities}/MockJobRunner.swift | 44 +- .../_TestUtilities}/MockKeychain.swift | 0 .../_TestUtilities}/MockLogger.swift | 0 .../_TestUtilities}/MockUserDefaults.swift | 0 .../_TestUtilities/Mocked+SUK.swift | 15 +- TestUtilities/ArgumentDescribing.swift | 47 +- TestUtilities/MockFunctionBuilder.swift | 10 +- TestUtilities/MockHandler.swift | 26 +- TestUtilities/Mocked.swift | 84 +- TestUtilities/Nimble/NimbleVerification.swift | 50 +- TestUtilities/RecordedCall.swift | 42 +- .../Utilities/Collection+Utilities.swift | 8 - .../Utilities/Combine+Utilities.swift | 16 - _SharedTestUtilities/FixtureBase.swift | 27 - _SharedTestUtilities/GRDBExtensions.swift | 14 - _SharedTestUtilities/Mock.swift | 1023 --------------- _SharedTestUtilities/MockFileManager.swift | 156 --- _SharedTestUtilities/NimbleExtensions.swift | 326 ----- _SharedTestUtilities/TestDependencies.swift | 20 - 67 files changed, 1860 insertions(+), 3049 deletions(-) delete mode 100644 SessionUtilitiesKit/Utilities/MutableIdentifiable.swift rename {_SharedTestUtilities => SessionUtilitiesKitTests/_TestUtilities}/MockAppContext.swift (100%) rename {_SharedTestUtilities => SessionUtilitiesKitTests/_TestUtilities}/MockCrypto.swift (100%) create mode 100644 SessionUtilitiesKitTests/_TestUtilities/MockFileManager.swift rename {_SharedTestUtilities => SessionUtilitiesKitTests/_TestUtilities}/MockGeneralCache.swift (100%) rename {_SharedTestUtilities => SessionUtilitiesKitTests/_TestUtilities}/MockJobRunner.swift (61%) rename {_SharedTestUtilities => SessionUtilitiesKitTests/_TestUtilities}/MockKeychain.swift (100%) rename {_SharedTestUtilities => SessionUtilitiesKitTests/_TestUtilities}/MockLogger.swift (100%) rename {_SharedTestUtilities => SessionUtilitiesKitTests/_TestUtilities}/MockUserDefaults.swift (100%) rename _SharedTestUtilities/TestExtensions.swift => TestUtilities/Utilities/Collection+Utilities.swift (51%) rename _SharedTestUtilities/CombineExtensions.swift => TestUtilities/Utilities/Combine+Utilities.swift (57%) delete mode 100644 _SharedTestUtilities/GRDBExtensions.swift delete mode 100644 _SharedTestUtilities/Mock.swift delete mode 100644 _SharedTestUtilities/MockFileManager.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 2e6c7b4ffe..eaef25ea80 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -400,10 +400,6 @@ FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; }; FD00CDCB2D5317A7006B96D3 /* Scheduler+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD00CDCA2D5317A3006B96D3 /* Scheduler+Utilities.swift */; }; FD0150262CA23D5B005B08A1 /* SessionUtilitiesKit.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = FD0150252CA23D5B005B08A1 /* SessionUtilitiesKit.xctestplan */; }; - FD0150282CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150272CA23DB7005B08A1 /* GRDBExtensions.swift */; }; - FD0150292CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150272CA23DB7005B08A1 /* GRDBExtensions.swift */; }; - FD01502A2CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150272CA23DB7005B08A1 /* GRDBExtensions.swift */; }; - FD01502C2CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150272CA23DB7005B08A1 /* GRDBExtensions.swift */; }; FD0150382CA24328005B08A1 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150372CA24328005B08A1 /* MockJobRunner.swift */; }; FD0150392CA24328005B08A1 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150372CA24328005B08A1 /* MockJobRunner.swift */; }; FD01503A2CA24328005B08A1 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150372CA24328005B08A1 /* MockJobRunner.swift */; }; @@ -413,10 +409,6 @@ FD0150422CA2433D005B08A1 /* VersionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD01503F2CA2433D005B08A1 /* VersionSpec.swift */; }; FD0150452CA243BB005B08A1 /* LibSessionUtilSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150442CA243BB005B08A1 /* LibSessionUtilSpec.swift */; }; FD0150462CA243BB005B08A1 /* LibSessionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150432CA243BB005B08A1 /* LibSessionSpec.swift */; }; - FD0150482CA243CB005B08A1 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150472CA243CB005B08A1 /* Mock.swift */; }; - FD0150492CA243CB005B08A1 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150472CA243CB005B08A1 /* Mock.swift */; }; - FD01504B2CA243CB005B08A1 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150472CA243CB005B08A1 /* Mock.swift */; }; - FD01504C2CA243CB005B08A1 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150472CA243CB005B08A1 /* Mock.swift */; }; FD01504E2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD01504D2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift */; }; FD0150502CA24468005B08A1 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; platformFilter = ios; }; FD0150522CA2446D005B08A1 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = FD0150512CA2446D005B08A1 /* Quick */; }; @@ -572,10 +564,6 @@ FD23CE332A67C4D90000B97C /* MockNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE312A67C38D0000B97C /* MockNetwork.swift */; }; FD23CE342A67C4D90000B97C /* MockNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE312A67C38D0000B97C /* MockNetwork.swift */; }; FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; }; - FD23EA5E28ED00FD0058676E /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; - FD23EA6128ED0B260058676E /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; }; - FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; }; - FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; }; FD245C50285065C700B966DD /* VisibleMessage+Quote.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A7552553A3AB00C340D1 /* VisibleMessage+Quote.swift */; }; FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5FB2554B0A000555489 /* MessageReceiver.swift */; }; FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF224255B6D5D007E1867 /* SignalAttachment.swift */; }; @@ -756,6 +744,12 @@ FD6B927A2E6F8B90004463B5 /* ServiceNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD10AF112AF85D11007709E5 /* ServiceNetwork.swift */; }; FD6B927C2E6F8BB2004463B5 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B927B2E6F8BAC004463B5 /* Router.swift */; }; FD6B927E2E6FEDFF004463B5 /* MockFallbackRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B927D2E6FEDFE004463B5 /* MockFallbackRegistry.swift */; }; + FD6B92822E77819F004463B5 /* Combine+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92812E77819B004463B5 /* Combine+Utilities.swift */; }; + FD6B92842E7781BC004463B5 /* Collection+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92832E7781B7004463B5 /* Collection+Utilities.swift */; }; + FD6B92852E77821E004463B5 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B927F2E778150004463B5 /* NimbleExtensions.swift */; }; + FD6B92862E77821E004463B5 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B927F2E778150004463B5 /* NimbleExtensions.swift */; }; + FD6B92872E77821E004463B5 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B927F2E778150004463B5 /* NimbleExtensions.swift */; }; + FD6B92882E77821E004463B5 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B927F2E778150004463B5 /* NimbleExtensions.swift */; }; FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */; }; FD6D9CF92CA152B300F706A8 /* Session+SNUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754BB2C9B9A8E002A2623 /* Session+SNUIKit.swift */; }; FD6DA9CF2D015B440092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9CE2D015B440092085A /* Lucide */; }; @@ -899,9 +893,6 @@ FD99D0922D10F5EE005D2E15 /* ThreadSafeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD99D0912D10F5EB005D2E15 /* ThreadSafeSpec.swift */; }; FD9AECA52AAA9609009B3406 /* NotificationResolution.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9AECA42AAA9609009B3406 /* NotificationResolution.swift */; }; FD9BDE012A5D24EA005F1EBC /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; }; - FD9DD2712A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; }; - FD9DD2722A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; }; - FD9DD2732A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; }; FDA335F52D91157A007E0EB6 /* SessionImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA335F42D911576007E0EB6 /* SessionImageView.swift */; }; FDAA16762AC28A3B00DDBF77 /* UserDefaultsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAA16752AC28A3B00DDBF77 /* UserDefaultsType.swift */; }; FDAA167D2AC528A200DDBF77 /* Preferences+Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAA167C2AC528A200DDBF77 /* Preferences+Sound.swift */; }; @@ -944,9 +935,6 @@ FDB5DB0B2A981F92002C8721 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; }; FDB5DB0C2A981F96002C8721 /* MockNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE312A67C38D0000B97C /* MockNetwork.swift */; }; FDB5DB102A981FA3002C8721 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; }; - FDB5DB112A981FA6002C8721 /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; }; - FDB5DB122A981FA8002C8721 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; - FDB5DB142A981FAE002C8721 /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; }; FDB5DB152A981FB0002C8721 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; }; FDB6A87C2AD75B7F002D4F96 /* PhotosUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDB6A87B2AD75B7F002D4F96 /* PhotosUI.framework */; }; FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB7400A28EB99A70094D718 /* TimeInterval+Utilities.swift */; }; @@ -966,7 +954,6 @@ FDC1BD662CFD6C4F002CDC71 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD652CFD6C4E002CDC71 /* Config.swift */; }; FDC1BD682CFE6EEB002CDC71 /* DeveloperSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */; }; FDC1BD6A2CFE7B6B002CDC71 /* DirectoryArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD692CFE7B67002CDC71 /* DirectoryArchiver.swift */; }; - FDC289472C881A3800020BC2 /* MutableIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */; }; FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908627D7047F005DAE71 /* RoomSpec.swift */; }; FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */; }; FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */; }; @@ -976,8 +963,6 @@ FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */; }; FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */; }; FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */; }; - FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; - FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */; }; FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; }; FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */; }; @@ -1822,7 +1807,6 @@ FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; FD00CDCA2D5317A3006B96D3 /* Scheduler+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Scheduler+Utilities.swift"; sourceTree = ""; }; FD0150252CA23D5B005B08A1 /* SessionUtilitiesKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SessionUtilitiesKit.xctestplan; sourceTree = ""; }; - FD0150272CA23DB7005B08A1 /* GRDBExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBExtensions.swift; sourceTree = ""; }; FD01502D2CA24310005B08A1 /* BatchRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestSpec.swift; sourceTree = ""; }; FD01502E2CA24310005B08A1 /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = ""; }; FD01502F2CA24310005B08A1 /* HeaderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderSpec.swift; sourceTree = ""; }; @@ -1834,7 +1818,6 @@ FD01503F2CA2433D005B08A1 /* VersionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionSpec.swift; sourceTree = ""; }; FD0150432CA243BB005B08A1 /* LibSessionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionSpec.swift; sourceTree = ""; }; FD0150442CA243BB005B08A1 /* LibSessionUtilSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionUtilSpec.swift; sourceTree = ""; }; - FD0150472CA243CB005B08A1 /* Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mock.swift; sourceTree = ""; }; FD01504D2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeConversionUtilitiesSpec.swift; sourceTree = ""; }; FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableLabel.swift; sourceTree = ""; }; FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+PushNotificationAPI.swift"; sourceTree = ""; }; @@ -1971,7 +1954,6 @@ FD23CE252A676B5B0000B97C /* DependenciesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependenciesSpec.swift; sourceTree = ""; }; FD23CE272A67755C0000B97C /* MockCrypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCrypto.swift; sourceTree = ""; }; FD23CE312A67C38D0000B97C /* MockNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetwork.swift; sourceTree = ""; }; - FD23EA6028ED0B260058676E /* CombineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = ""; }; FD245C612850664300B966DD /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; FD28A4F527EAD44C00FF65E7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; FD29598C2A43BC0B00888A17 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; @@ -2097,6 +2079,9 @@ FD6B926B2E6A763D004463B5 /* Async+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Async+Utilities.swift"; sourceTree = ""; }; FD6B927B2E6F8BAC004463B5 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; FD6B927D2E6FEDFE004463B5 /* MockFallbackRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFallbackRegistry.swift; sourceTree = ""; }; + FD6B927F2E778150004463B5 /* NimbleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbleExtensions.swift; sourceTree = ""; }; + FD6B92812E77819B004463B5 /* Combine+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Combine+Utilities.swift"; sourceTree = ""; }; + FD6B92832E7781B7004463B5 /* Collection+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Utilities.swift"; sourceTree = ""; }; FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopSessionCallManager.swift; sourceTree = ""; }; FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _021_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = ""; }; @@ -2221,7 +2206,6 @@ FD99D0862D0FA72E005D2E15 /* ThreadSafe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSafe.swift; sourceTree = ""; }; FD99D0912D10F5EB005D2E15 /* ThreadSafeSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSafeSpec.swift; sourceTree = ""; }; FD9AECA42AAA9609009B3406 /* NotificationResolution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationResolution.swift; sourceTree = ""; }; - FD9DD2702A72516D00ECB68E /* TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestExtensions.swift; sourceTree = ""; }; FDA335F42D911576007E0EB6 /* SessionImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionImageView.swift; sourceTree = ""; }; FDAA16752AC28A3B00DDBF77 /* UserDefaultsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsType.swift; sourceTree = ""; }; FDAA167C2AC528A200DDBF77 /* Preferences+Sound.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Sound.swift"; sourceTree = ""; }; @@ -2279,7 +2263,6 @@ FDC1BD652CFD6C4E002CDC71 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsViewModel.swift; sourceTree = ""; }; FDC1BD692CFE7B67002CDC71 /* DirectoryArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryArchiver.swift; sourceTree = ""; }; - FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableIdentifiable.swift; sourceTree = ""; }; FDC2908627D7047F005DAE71 /* RoomSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSpec.swift; sourceTree = ""; }; FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollInfoSpec.swift; sourceTree = ""; }; FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequestSpec.swift; sourceTree = ""; }; @@ -2290,7 +2273,6 @@ FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSErrorSpec.swift; sourceTree = ""; }; FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalizationSpec.swift; sourceTree = ""; }; FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupManagerSpec.swift; sourceTree = ""; }; - FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbleExtensions.swift; sourceTree = ""; }; FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPIError.swift; sourceTree = ""; }; FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = ""; }; FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSEndpoint.swift; sourceTree = ""; }; @@ -4027,7 +4009,6 @@ FD09796A27F6C67500936362 /* Failable.swift */, FD47E0AE2AA692F400A55E41 /* JSONDecoder+Utilities.swift */, FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */, - FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */, FD09797C27FBDB2000936362 /* Notification+Utilities.swift */, FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */, FD09797127FAA2F500936362 /* Optional+Utilities.swift */, @@ -4421,6 +4402,14 @@ isa = PBXGroup; children = ( FD6B92632E696EDC004463B5 /* Mocked+SUK.swift */, + FD481A932CAE0ADD00ECC4CF /* MockAppContext.swift */, + FD23CE272A67755C0000B97C /* MockCrypto.swift */, + FD3FAB6A2AF1B27800DC5421 /* MockFileManager.swift */, + FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */, + FD0150372CA24328005B08A1 /* MockJobRunner.swift */, + FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */, + FDB11A552DD17C3000BEF49F /* MockLogger.swift */, + FD83B9D127D59495005E1583 /* MockUserDefaults.swift */, ); path = _TestUtilities; sourceTree = ""; @@ -4429,6 +4418,8 @@ isa = PBXGroup; children = ( FD6B926B2E6A763D004463B5 /* Async+Utilities.swift */, + FD6B92832E7781B7004463B5 /* Collection+Utilities.swift */, + FD6B92812E77819B004463B5 /* Combine+Utilities.swift */, ); path = Utilities; sourceTree = ""; @@ -4671,21 +4662,9 @@ isa = PBXGroup; children = ( FD6B925D2E695ACD004463B5 /* FixtureBase.swift */, - FD0150472CA243CB005B08A1 /* Mock.swift */, - FD481A932CAE0ADD00ECC4CF /* MockAppContext.swift */, - FD23CE272A67755C0000B97C /* MockCrypto.swift */, - FD3FAB6A2AF1B27800DC5421 /* MockFileManager.swift */, - FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */, - FD83B9D127D59495005E1583 /* MockUserDefaults.swift */, - FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */, - FD0150372CA24328005B08A1 /* MockJobRunner.swift */, - FDB11A552DD17C3000BEF49F /* MockLogger.swift */, FD83B9BD27CF2243005E1583 /* TestConstants.swift */, FD6531892AA025C500DFEEAA /* TestDependencies.swift */, - FD9DD2702A72516D00ECB68E /* TestExtensions.swift */, - FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */, - FD23EA6028ED0B260058676E /* CombineExtensions.swift */, - FD0150272CA23DB7005B08A1 /* GRDBExtensions.swift */, + FD6B927F2E778150004463B5 /* NimbleExtensions.swift */, FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */, ); path = _SharedTestUtilities; @@ -6535,7 +6514,6 @@ FD74434C2D07CA9F00862443 /* CGSize+Utilities.swift in Sources */, FD74434D2D07CA9F00862443 /* CGPoint+Utilities.swift in Sources */, FD74434E2D07CA9F00862443 /* CGRect+Utilities.swift in Sources */, - FDC289472C881A3800020BC2 /* MutableIdentifiable.swift in Sources */, FD6F5B5E2E657A24009A8D01 /* CancellationAwareAsyncStream.swift in Sources */, C300A60D2554B31900555489 /* Logging.swift in Sources */, FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */, @@ -7064,9 +7042,11 @@ FD6B927E2E6FEDFF004463B5 /* MockFallbackRegistry.swift in Sources */, FD1BDBDF2E655735008EF998 /* MockFunctionHandler.swift in Sources */, FD1BDBD92E653868008EF998 /* MockError.swift in Sources */, + FD6B92822E77819F004463B5 /* Combine+Utilities.swift in Sources */, FD1BDBDB2E6538B4008EF998 /* MockFunctionBuilder.swift in Sources */, FD1BDBD32E653660008EF998 /* MockFunction.swift in Sources */, FD1BDBD72E653852008EF998 /* RecordedCall.swift in Sources */, + FD6B92842E7781BC004463B5 /* Collection+Utilities.swift in Sources */, FD1BDBD02E653625008EF998 /* Mockable.swift in Sources */, FD1BDBFE2E656562008EF998 /* NimbleFailureReporter.swift in Sources */, FD1BDBFB2E656539008EF998 /* TestFailureReporter.swift in Sources */, @@ -7084,6 +7064,7 @@ FD481A9C2CB4D58300ECC4CF /* MockSnodeAPICache.swift in Sources */, FD52CB5A2E12166F00A4DA70 /* OnboardingSpec.swift in Sources */, FD71161728D00DA400B47552 /* ThreadSettingsViewModelSpec.swift in Sources */, + FD6B92852E77821E004463B5 /* NimbleExtensions.swift in Sources */, FD6B92652E697012004463B5 /* Mocked+SUK.swift in Sources */, FD481A972CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */, @@ -7092,7 +7073,6 @@ FD23CE332A67C4D90000B97C /* MockNetwork.swift in Sources */, FD71161528D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift in Sources */, FD01503A2CA24328005B08A1 /* MockJobRunner.swift in Sources */, - FD23EA5E28ED00FD0058676E /* NimbleExtensions.swift in Sources */, FD481A9B2CB4CAF100ECC4CF /* ArgumentDescribing+SMK.swift in Sources */, FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */, FD6B925E2E695ACE004463B5 /* FixtureBase.swift in Sources */, @@ -7101,17 +7081,13 @@ FD52CB5B2E123FBC00A4DA70 /* Mocked+SNK.swift in Sources */, FD52CB5C2E12536400A4DA70 /* MockExtensionHelper.swift in Sources */, 9499E68B2DF92F4E00091434 /* ThreadNotificationSettingsViewModelSpec.swift in Sources */, - FD01504B2CA243CB005B08A1 /* Mock.swift in Sources */, FD481A9A2CB4CAE500ECC4CF /* Mocked+SMK.swift in Sources */, FD3FAB6C2AF1B28B00DC5421 /* MockFileManager.swift in Sources */, - FD23EA6128ED0B260058676E /* CombineExtensions.swift in Sources */, - FD9DD2712A72516D00ECB68E /* TestExtensions.swift in Sources */, FD65318A2AA025C500DFEEAA /* TestDependencies.swift in Sources */, FD2AAAED28ED3E1000A49611 /* MockGeneralCache.swift in Sources */, FD481A992CB4CAAA00ECC4CF /* MockLibSessionCache.swift in Sources */, FDB11A572DD17D0600BEF49F /* MockLogger.swift in Sources */, FD49E2462B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, - FD01502C2CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7126,30 +7102,26 @@ FD99D0922D10F5EE005D2E15 /* ThreadSafeSpec.swift in Sources */, FDD23AF02E459EDD0057E853 /* _020_AddJobUniqueHash.swift in Sources */, FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */, - FD9DD2732A72516D00ECB68E /* TestExtensions.swift in Sources */, FD3FAB6E2AF1B28C00DC5421 /* MockFileManager.swift in Sources */, FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */, FDB11A592DD17D0600BEF49F /* MockLogger.swift in Sources */, FD8A5B302DC18D61004C689B /* GeneralCacheSpec.swift in Sources */, FD6B92612E695ACE004463B5 /* FixtureBase.swift in Sources */, FDD23AEE2E459E470057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */, - FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FD0150402CA2433D005B08A1 /* BencodeDecoderSpec.swift in Sources */, FD0150412CA2433D005B08A1 /* BencodeEncoderSpec.swift in Sources */, FD0150422CA2433D005B08A1 /* VersionSpec.swift in Sources */, FD42ECD42E32FF2E002D03EA /* StringUtilitiesSpec.swift in Sources */, - FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */, FD6B92642E696EE0004463B5 /* Mocked+SUK.swift in Sources */, - FD0150482CA243CB005B08A1 /* Mock.swift in Sources */, FDFE75B42ABD46B600655640 /* MockUserDefaults.swift in Sources */, FD0150392CA24328005B08A1 /* MockJobRunner.swift in Sources */, + FD6B92882E77821E004463B5 /* NimbleExtensions.swift in Sources */, FD23CE282A67755C0000B97C /* MockCrypto.swift in Sources */, FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */, FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */, FD0B77B229B82B7A009169BA /* ArrayUtilitiesSpec.swift in Sources */, FD23CE262A676B5B0000B97C /* DependenciesSpec.swift in Sources */, FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */, - FD0150292CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, FDD23AEF2E459EC90057E853 /* _012_AddJobPriority.swift in Sources */, FD481A942CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, ); @@ -7163,12 +7135,10 @@ FD49E2482B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, FD336F702CABB96C00C0B51B /* BatchRequestSpec.swift in Sources */, FD3765E22AD8F53B00DC1489 /* Mocked+SNK.swift in Sources */, - FDB5DB142A981FAE002C8721 /* CombineExtensions.swift in Sources */, FD0150382CA24328005B08A1 /* MockJobRunner.swift in Sources */, FD481A952CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, FD3765DF2AD8F03100DC1489 /* MockSnodeAPICache.swift in Sources */, FDB11A582DD17D0600BEF49F /* MockLogger.swift in Sources */, - FDB5DB112A981FA6002C8721 /* TestExtensions.swift in Sources */, FDB5DB092A981F8D002C8721 /* MockCrypto.swift in Sources */, FD65318C2AA025C500DFEEAA /* TestDependencies.swift in Sources */, FDB5DB102A981FA3002C8721 /* TestConstants.swift in Sources */, @@ -7176,15 +7146,13 @@ FD6B92672E697012004463B5 /* Mocked+SUK.swift in Sources */, FD336F712CABB97800C0B51B /* DestinationSpec.swift in Sources */, FD336F722CABB97800C0B51B /* BatchResponseSpec.swift in Sources */, + FD6B92872E77821E004463B5 /* NimbleExtensions.swift in Sources */, FD336F732CABB97800C0B51B /* HeaderSpec.swift in Sources */, FD336F742CABB97800C0B51B /* PreparedRequestSpec.swift in Sources */, FD336F752CABB97800C0B51B /* RequestSpec.swift in Sources */, FD6B92602E695ACE004463B5 /* FixtureBase.swift in Sources */, FD336F762CABB97800C0B51B /* BencodeResponseSpec.swift in Sources */, FDB5DB0B2A981F92002C8721 /* MockGeneralCache.swift in Sources */, - FD0150492CA243CB005B08A1 /* Mock.swift in Sources */, - FDB5DB122A981FA8002C8721 /* NimbleExtensions.swift in Sources */, - FD0150282CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, FDB5DB152A981FB0002C8721 /* SynchronousStorage.swift in Sources */, FDE754AC2C9B967E002A2623 /* FileUploadResponseSpec.swift in Sources */, ); @@ -7201,7 +7169,6 @@ FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */, FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */, FDE754A82C9B964D002A2623 /* MessageReceiverGroupsSpec.swift in Sources */, - FD01504C2CA243CB005B08A1 /* Mock.swift in Sources */, FD981BC92DC4641100564172 /* ExtensionHelperSpec.swift in Sources */, FD981BCB2DC4A21C00564172 /* MessageDeduplicationSpec.swift in Sources */, FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */, @@ -7215,6 +7182,7 @@ FD981BCD2DC81ABF00564172 /* MockExtensionHelper.swift in Sources */, FD336F602CAA28CF00C0B51B /* Mocked+SMK.swift in Sources */, FD336F612CAA28CF00C0B51B /* MockNotificationsManager.swift in Sources */, + FD6B92862E77821E004463B5 /* NimbleExtensions.swift in Sources */, FD336F622CAA28CF00C0B51B /* ArgumentDescribing+SMK.swift in Sources */, FD336F632CAA28CF00C0B51B /* MockOGMCache.swift in Sources */, FD481A922CAD17DE00ECC4CF /* LibSessionGroupMembersSpec.swift in Sources */, @@ -7224,7 +7192,6 @@ FD336F692CAA28CF00C0B51B /* MockLibSessionCache.swift in Sources */, FD0150462CA243BB005B08A1 /* LibSessionSpec.swift in Sources */, FD3765E02AD8F05100DC1489 /* MockSnodeAPICache.swift in Sources */, - FD9DD2722A72516D00ECB68E /* TestExtensions.swift in Sources */, FDE754AA2C9B964D002A2623 /* MessageSenderSpec.swift in Sources */, FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */, FD65318B2AA025C500DFEEAA /* TestDependencies.swift in Sources */, @@ -7232,12 +7199,10 @@ FD6B92662E697012004463B5 /* Mocked+SUK.swift in Sources */, FD3765E32AD8F56200DC1489 /* Mocked+SNK.swift in Sources */, FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */, - FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */, FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */, FD23CE342A67C4D90000B97C /* MockNetwork.swift in Sources */, FD61FCF92D308CC9005752DE /* GroupMemberSpec.swift in Sources */, FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */, - FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */, FD481A902CAD16F100ECC4CF /* LibSessionGroupInfoSpec.swift in Sources */, FDE754A92C9B964D002A2623 /* MessageSenderGroupsSpec.swift in Sources */, @@ -7248,7 +7213,6 @@ FD336F6C2CAA29C600C0B51B /* CommunityPollerManagerSpec.swift in Sources */, FD481AA32CB889AE00ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift in Sources */, FDFD645D27F273F300808CA1 /* MockGeneralCache.swift in Sources */, - FD01502A2CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */, FD01503B2CA24328005B08A1 /* MockJobRunner.swift in Sources */, FD3F2EE72DE6CC4100FD6849 /* NotificationsManagerSpec.swift in Sources */, diff --git a/Session/Path/PathVC.swift b/Session/Path/PathVC.swift index 07df30599f..e85ad1112a 100644 --- a/Session/Path/PathVC.swift +++ b/Session/Path/PathVC.swift @@ -175,7 +175,7 @@ final class PathVC: BaseVC { switch path.category { case .standard: return true case .download, .upload: return false - case .none: + case .none, .invalid: guard let pubkey: String = path.destinationPubkey else { return false } diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 4d1317abbc..4b958333aa 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -182,7 +182,7 @@ public extension LibSession { dump: ConfigDump? ) - enum CacheBehaviour { + enum CacheBehaviour: Int, CaseIterable { case skipAutomaticConfigSync case skipGroupAdminCheck } diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index a2bf724eb0..fd465531db 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -187,14 +187,16 @@ public final class OpenGroupManager { // Set the group to active and reset the sequenceNumber (handle groups which have // been deactivated) - _ = try? OpenGroup - .filter(id: OpenGroup.idFor(roomToken: roomToken, server: targetServer)) - .updateAllAndConfig( - db, - OpenGroup.Columns.isActive.set(to: true), - OpenGroup.Columns.sequenceNumber.set(to: 0), - using: dependencies - ) + if (try? OpenGroup.select(.isActive).filter(id: threadId).asRequest(of: Bool.self).fetchOne(db)) != true { + _ = try? OpenGroup + .filter(id: OpenGroup.idFor(roomToken: roomToken, server: targetServer)) + .updateAllAndConfig( + db, + OpenGroup.Columns.isActive.set(to: true), + OpenGroup.Columns.sequenceNumber.set(to: 0), + using: dependencies + ) + } return true } diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/NotificationCategory.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Types/NotificationCategory.swift index 3786203383..da03212c1b 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/NotificationCategory.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Types/NotificationCategory.swift @@ -4,7 +4,7 @@ import Foundation -public enum NotificationCategory: CaseIterable, Equatable { +public enum NotificationCategory: Int, CaseIterable, Equatable { case incomingMessage case errorMessage case threadlessErrorMessage diff --git a/SessionMessagingKit/Utilities/AppSetup.swift b/SessionMessagingKit/Utilities/AppSetup.swift index af7ce2b400..742c8ea778 100644 --- a/SessionMessagingKit/Utilities/AppSetup.swift +++ b/SessionMessagingKit/Utilities/AppSetup.swift @@ -96,7 +96,7 @@ public enum AppSetup { ) Task.detached(priority: .medium) { - dependencies[singleton: .extensionHelper].replicateAllConfigDumpsIfNeeded( + await dependencies[singleton: .extensionHelper].replicateAllConfigDumpsIfNeeded( userSessionId: userInfo.sessionId, allDumpSessionIds: userInfo.dumpSessionIds ) diff --git a/SessionMessagingKit/Utilities/ExtensionHelper.swift b/SessionMessagingKit/Utilities/ExtensionHelper.swift index 19900fffc2..3e7c41a57c 100644 --- a/SessionMessagingKit/Utilities/ExtensionHelper.swift +++ b/SessionMessagingKit/Utilities/ExtensionHelper.swift @@ -336,7 +336,7 @@ public class ExtensionHelper: ExtensionHelperType { public func replicateAllConfigDumpsIfNeeded( userSessionId: SessionId, allDumpSessionIds: Set - ) { + ) async { struct ReplicatedDumpInfo { struct DumpState { let variant: ConfigDump.Variant @@ -401,37 +401,29 @@ public class ExtensionHelper: ExtensionHelperType { let fetchTimestamp: TimeInterval = dependencies.dateNow.timeIntervalSince1970 let missingDumpIds: Set = Set(missingReplicatedDumpInfo.map { $0.sessionId.hexString }) - dependencies[singleton: .storage].readAsync( - retrieve: { db in - try ConfigDump - .filter(missingDumpIds.contains(ConfigDump.Columns.publicKey)) - .fetchAll(db) - }, - completion: { [weak self] result in - guard - let self = self, - let dumps: [ConfigDump] = try? result.get() - else { return } - - /// Persist each dump to disk (if there isn't already one there, or it was updated before the dump was fetched from - /// the database) - /// - /// **Note:** Because it's likely that this function runs in the background it's possible that another thread could trigger - /// a config update which would result in the dump getting replicated - if that occurs then we don't want to override what - /// is likely a newer dump, but do need to replace what might be an invalid dump file (hence the timestamp check) - dumps.forEach { dump in - let dumpLastUpdated: TimeInterval = self.lastUpdatedTimestamp( - for: dump.sessionId, - variant: dump.variant - ) - - self.replicate( - dump: dump, - replaceExisting: (dumpLastUpdated < fetchTimestamp) - ) - } - } - ) + let dumps: [ConfigDump] = ((try? await dependencies[singleton: .storage].readAsync { db in + try ConfigDump + .filter(missingDumpIds.contains(ConfigDump.Columns.publicKey)) + .fetchAll(db) + }) ?? []) + + /// Persist each dump to disk (if there isn't already one there, or it was updated before the dump was fetched from + /// the database) + /// + /// **Note:** Because it's likely that this function runs in the background it's possible that another thread could trigger + /// a config update which would result in the dump getting replicated - if that occurs then we don't want to override what + /// is likely a newer dump, but do need to replace what might be an invalid dump file (hence the timestamp check) + dumps.forEach { dump in + let dumpLastUpdated: TimeInterval = lastUpdatedTimestamp( + for: dump.sessionId, + variant: dump.variant + ) + + replicate( + dump: dump, + replaceExisting: (dumpLastUpdated < fetchTimestamp) + ) + } } public func refreshDumpModifiedDate(sessionId: SessionId, variant: ConfigDump.Variant) { @@ -977,7 +969,7 @@ public protocol ExtensionHelperType { func lastUpdatedTimestamp(for sessionId: SessionId, variant: ConfigDump.Variant) -> TimeInterval func replicate(dump: ConfigDump?, replaceExisting: Bool) - func replicateAllConfigDumpsIfNeeded(userSessionId: SessionId, allDumpSessionIds: Set) + func replicateAllConfigDumpsIfNeeded(userSessionId: SessionId, allDumpSessionIds: Set) async func refreshDumpModifiedDate(sessionId: SessionId, variant: ConfigDump.Variant) func loadUserConfigState( into cache: LibSessionCacheType, diff --git a/SessionMessagingKit/Utilities/Preferences+Sound.swift b/SessionMessagingKit/Utilities/Preferences+Sound.swift index 3e7184d002..5576572a90 100644 --- a/SessionMessagingKit/Utilities/Preferences+Sound.swift +++ b/SessionMessagingKit/Utilities/Preferences+Sound.swift @@ -15,7 +15,7 @@ private extension Log.Category { // MARK: - Preferences public extension Preferences { - enum Sound: Int, Sendable, Codable, Equatable, Differentiable, ThreadSafeType { + enum Sound: Int, Sendable, Codable, CaseIterable, Equatable, Differentiable, ThreadSafeType { public static var defaultiOSIncomingRingtone: Sound = .opening public static var defaultNotificationSound: Sound = .note diff --git a/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift b/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift index 0bcbebda58..754ee651d2 100644 --- a/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift +++ b/SessionMessagingKitTests/Crypto/CryptoSMKSpec.swift @@ -14,11 +14,12 @@ class CryptoSMKSpec: AsyncSpec { // MARK: Configuration @TestState var dependencies: TestDependencies! = TestDependencies() - @TestState(singleton: .crypto, in: dependencies) var crypto: Crypto! = Crypto(using: dependencies) + @TestState var crypto: Crypto! = Crypto(using: dependencies) @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) beforeEach { - /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + dependencies.set(singleton: .crypto, to: crypto) + try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) } diff --git a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift index 6b41ffdcab..f90de1d7bf 100644 --- a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift +++ b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift @@ -17,7 +17,7 @@ class MessageDeduplicationSpec: AsyncSpec { @TestState var dependencies: TestDependencies! = TestDependencies() - @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( + @TestState var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), using: dependencies ) @@ -31,6 +31,7 @@ class MessageDeduplicationSpec: AsyncSpec { beforeEach { try await mockStorage.perform(migrations: SNMessagingKit.migrations) + dependencies.set(singleton: .storage, to: mockStorage) try await mockExtensionHelper.when { $0.deleteCache() }.thenReturn(()) try await mockExtensionHelper diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index 838c1aa240..2b7a5f20d1 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -23,8 +23,8 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { dependencies.forceSynchronous = true dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) } - @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() - @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( + @TestState var mockLibSessionCache: MockLibSessionCache! = .create(using: dependencies) + @TestState var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), using: dependencies ) @@ -41,21 +41,21 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { "f2761e3bb6ee837a26b24b5" ) @TestState var mockNetwork: MockNetwork! = .create(using: dependencies) - @TestState(singleton: .fileManager, in: dependencies) var mockFileManager: MockFileManager! = MockFileManager( - initialSetup: { $0.defaultInitialSetup() } - ) - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create(using: dependencies) - @TestState(singleton: .imageDataManager, in: dependencies) var mockImageDataManager: MockImageDataManager! = .create(using: dependencies) + @TestState var mockFileManager: MockFileManager! = .create(using: dependencies) + @TestState var mockCrypto: MockCrypto! = .create(using: dependencies) + @TestState var mockImageDataManager: MockImageDataManager! = .create(using: dependencies) @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) beforeEach { - /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) - mockLibSessionCache.defaultInitialSetup() + try await mockLibSessionCache.defaultInitialSetup() dependencies.set(cache: .libSession, to: mockLibSessionCache) + try await mockFileManager.defaultInitialSetup() + dependencies.set(singleton: .fileManager, to: mockFileManager) + try await mockStorage.perform(migrations: SNMessagingKit.migrations) try await mockStorage.writeAsync { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) @@ -63,6 +63,7 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) } + dependencies.set(singleton: .storage, to: mockStorage) try await mockCrypto.when { $0.generate(.uuid()) }.thenReturn(UUID(uuidString: "00000000-0000-0000-0000-000000001234")) try await mockCrypto @@ -83,6 +84,7 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { try await mockCrypto .when { $0.generate(.signatureBlind15(message: .any, serverPublicKey: .any, ed25519SecretKey: .any)) } .thenReturn("TestSogsSignature".bytes) + dependencies.set(singleton: .crypto, to: mockCrypto) try await mockNetwork .when { @@ -101,6 +103,7 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { try await mockImageDataManager .when { await $0.load(.any) } .thenReturn(nil) + dependencies.set(singleton: .imageDataManager, to: mockImageDataManager) } // MARK: - a DisplayPictureDownloadJob @@ -635,8 +638,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: ------ does not save the picture it("does not save the picture") { - expect(mockFileManager) - .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) + await mockFileManager + .verify { $0.createFile(atPath: .any, contents: .any, attributes: .any) } + .wasNotCalled() await mockImageDataManager .verify { await $0.load(.any) } .wasNotCalled(timeout: .milliseconds(50)) @@ -654,8 +658,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: ------ does not save the picture it("does not save the picture") { - expect(mockFileManager) - .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) + await mockFileManager + .verify { $0.createFile(atPath: .any, contents: .any, attributes: .any) } + .wasNotCalled() await mockImageDataManager .verify { await $0.load(.any) } .wasNotCalled(timeout: .milliseconds(50)) @@ -666,7 +671,7 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: ---- when it fails to write to disk context("when it fails to write to disk") { beforeEach { - mockFileManager + try await mockFileManager .when { $0.createFile(atPath: .any, contents: .any, attributes: .any) } .thenReturn(false) } @@ -682,14 +687,15 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: ---- writes the file to disk it("writes the file to disk") { - expect(mockFileManager) - .to(call(.exactly(times: 1), matchingParameters: .all) { mockFileManager in - mockFileManager.createFile( + await mockFileManager + .verify { + $0.createFile( atPath: "/test/DisplayPictures/5465737448617368", contents: imageData, attributes: nil ) - }) + } + .wasCalled(exactly: 1) } // MARK: ---- adds the image data to the displayPicture cache @@ -743,8 +749,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { await mockCrypto .verify { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } .wasNotCalled() - expect(mockFileManager) - .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) + await mockFileManager + .verify { $0.createFile(atPath: .any, contents: .any, attributes: .any) } + .wasNotCalled() await mockImageDataManager .verify { await $0.load(.any) } .wasNotCalled(timeout: .milliseconds(50)) @@ -770,8 +777,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { await mockCrypto .verify { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } .wasNotCalled() - expect(mockFileManager) - .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) + await mockFileManager + .verify { $0.createFile(atPath: .any, contents: .any, attributes: .any) } + .wasNotCalled() await mockImageDataManager .verify { await $0.load(.any) } .wasNotCalled(timeout: .milliseconds(50)) @@ -806,8 +814,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { await mockCrypto .verify { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } .wasNotCalled() - expect(mockFileManager) - .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) + await mockFileManager + .verify { $0.createFile(atPath: .any, contents: .any, attributes: .any) } + .wasNotCalled() await mockImageDataManager .verify { await $0.load(.any) } .wasNotCalled(timeout: .milliseconds(50)) @@ -841,14 +850,15 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { await mockCrypto .verify { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } .wasCalled(exactly: 1) - expect(mockFileManager).to(call(.exactly(times: 1), matchingParameters: .all) { - $0.createFile( - atPath: "/test/DisplayPictures/5465737448617368", - contents: imageData, - attributes: nil - ) - }) - + await mockFileManager + .verify { + $0.createFile( + atPath: "/test/DisplayPictures/5465737448617368", + contents: imageData, + attributes: nil + ) + } + .wasCalled(exactly: 1) await mockImageDataManager .verify { await $0.load( @@ -938,8 +948,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { await mockCrypto .verify { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } .wasNotCalled() - expect(mockFileManager) - .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) + await mockFileManager + .verify { $0.createFile(atPath: .any, contents: .any, attributes: .any) } + .wasNotCalled() await mockImageDataManager .verify { await $0.load(.any) } .wasNotCalled(timeout: .milliseconds(50)) @@ -964,8 +975,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { await mockCrypto .verify { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } .wasNotCalled() - expect(mockFileManager) - .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) + await mockFileManager + .verify { $0.createFile(atPath: .any, contents: .any, attributes: .any) } + .wasNotCalled() await mockImageDataManager .verify { await $0.load(.any) } .wasNotCalled(timeout: .milliseconds(50)) @@ -1004,8 +1016,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { await mockCrypto .verify { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any)) } .wasNotCalled() - expect(mockFileManager) - .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) + await mockFileManager + .verify { $0.createFile(atPath: .any, contents: .any, attributes: .any) } + .wasNotCalled() await mockImageDataManager .verify { await $0.load(.any) } .wasNotCalled(timeout: .milliseconds(50)) @@ -1111,8 +1124,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: -------- does not save the picture it("does not save the picture") { - expect(mockFileManager) - .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) + await mockFileManager + .verify { $0.createFile(atPath: .any, contents: .any, attributes: .any) } + .wasNotCalled() await mockImageDataManager .verify { await $0.load(.any) } .wasNotCalled(timeout: .milliseconds(50)) @@ -1134,8 +1148,9 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: -------- does not save the picture it("does not save the picture") { - expect(mockFileManager) - .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) + await mockFileManager + .verify { $0.createFile(atPath: .any, contents: .any, attributes: .any) } + .wasNotCalled() await mockImageDataManager .verify { await $0.load(.any) } .wasNotCalled(timeout: .milliseconds(50)) @@ -1169,13 +1184,15 @@ class DisplayPictureDownloadJobSpec: AsyncSpec { // MARK: -------- saves the picture it("saves the picture") { - expect(mockFileManager).to(call(.exactly(times: 1), matchingParameters: .all) { - $0.createFile( - atPath: "/test/DisplayPictures/5465737448617368", - contents: imageData, - attributes: nil - ) - }) + await mockFileManager + .verify { + $0.createFile( + atPath: "/test/DisplayPictures/5465737448617368", + contents: imageData, + attributes: nil + ) + } + .wasCalled(exactly: 1) await mockImageDataManager .verify { await $0.load( diff --git a/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift b/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift index ca27aa187f..50e121957b 100644 --- a/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift @@ -2,6 +2,7 @@ import Foundation import GRDB +import TestUtilities import Quick import Nimble @@ -9,10 +10,6 @@ import Nimble @testable import SessionMessagingKit @testable import SessionUtilitiesKit -extension Job: @retroactive MutableIdentifiable { - public mutating func setId(_ id: Int64?) { self.id = id } -} - class MessageSendJobSpec: AsyncSpec { override class func spec() { // MARK: Configuration @@ -30,38 +27,15 @@ class MessageSendJobSpec: AsyncSpec { @TestState var dependencies: TestDependencies! = TestDependencies { dependencies in dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) } - @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() - @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( + @TestState var mockLibSessionCache: MockLibSessionCache! = .create(using: dependencies) + @TestState var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( - initialSetup: { jobRunner in - jobRunner - .when { - $0.jobInfoFor( - jobs: nil, - state: .running, - variant: .attachmentUpload - ) - } - .thenReturn([:]) - jobRunner - .when { $0.insert(.any, job: .any, before: .any) } - .then { args, untrackedArgs in - let db: ObservingDatabase = untrackedArgs[0] as! ObservingDatabase - var job: Job = args[0] as! Job - job.id = 1000 - - try! job.insert(db) - } - .thenReturn((1000, Job(variant: .messageSend))) - } - ) + @TestState var mockJobRunner: MockJobRunner! = .create(using: dependencies) beforeEach { - /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead - mockLibSessionCache.defaultInitialSetup() + try await mockLibSessionCache.defaultInitialSetup() dependencies.set(cache: .libSession, to: mockLibSessionCache) try await mockStorage.perform(migrations: SNMessagingKit.migrations) @@ -78,6 +52,28 @@ class MessageSendJobSpec: AsyncSpec { using: dependencies ) } + dependencies.set(singleton: .storage, to: mockStorage) + + try await mockJobRunner + .when { + $0.jobInfoFor( + jobs: nil, + state: .anyState, + variant: .attachmentUpload + ) + } + .thenReturn([:]) + try await mockJobRunner + .when { $0.insert(.any, job: .any, before: .any) } + .then { args in + let db: ObservingDatabase = args[0] as! ObservingDatabase + var job: Job = args[1] as! Job + job.id = 1000 + + try! job.insert(db) + } + .thenReturn((1000, Job(variant: .messageSend))) + dependencies.set(singleton: .jobRunner, to: mockJobRunner) } // MARK: - a MessageSendJob @@ -207,7 +203,8 @@ class MessageSendJobSpec: AsyncSpec { mockStorage.write { db in try interaction.insert(db) - try job.insert(db, withRowId: 54321) + job.id = 54321 + try job.insert(db) } } @@ -289,7 +286,10 @@ class MessageSendJobSpec: AsyncSpec { ) ) ) - mockStorage.write { db in try job.insert(db, withRowId: 54321) } + mockStorage.write { db in + job.id = 54321 + try job.insert(db) + } var error: Error? = nil var permanentFailure: Bool = false @@ -406,7 +406,7 @@ class MessageSendJobSpec: AsyncSpec { // MARK: -------- inserts an attachment upload job before the message send job it("inserts an attachment upload job before the message send job") { - mockJobRunner + try await mockJobRunner .when { $0.jobInfoFor( jobs: nil, @@ -425,8 +425,8 @@ class MessageSendJobSpec: AsyncSpec { using: dependencies ) - expect(mockJobRunner) - .to(call(.exactly(times: 1), matchingParameters: .all) { + await mockJobRunner + .verify { $0.insert( .any, job: Job( @@ -443,7 +443,8 @@ class MessageSendJobSpec: AsyncSpec { ), before: job ) - }) + } + .wasCalled(exactly: 1) } // MARK: -------- creates a dependency between the new job and the existing one diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index cebaf36ec0..d39236b2f1 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -19,26 +19,14 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { dependencies.forceSynchronous = true dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) } - @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( + @TestState var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), using: dependencies ) @TestState var mockUserDefaults: MockUserDefaults! = .create(using: dependencies) @TestState var mockNetwork: MockNetwork! = .create(using: dependencies) - @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( - initialSetup: { jobRunner in - jobRunner - .when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) } - .thenReturn(nil) - jobRunner - .when { $0.upsert(.any, job: .any, canStartJob: .any) } - .thenReturn(nil) - jobRunner - .when { $0.jobInfoFor(jobs: .any, state: .any, variant: .any) } - .thenReturn([:]) - } - ) - @TestState var mockOGMCache: MockOGMCache! = MockOGMCache() + @TestState var mockJobRunner: MockJobRunner! = .create(using: dependencies) + @TestState var mockOGMCache: MockOGMCache! = .create(using: dependencies) @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) @TestState var job: Job! = Job(variant: .retrieveDefaultOpenGroupRooms) @TestState var error: Error? = nil @@ -46,11 +34,10 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { @TestState var wasDeferred: Bool! = false beforeEach { - /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) - mockOGMCache.when { $0.setDefaultRoomInfo(.any) }.thenReturn(()) + try await mockOGMCache.when { $0.setDefaultRoomInfo(.any) }.thenReturn(()) dependencies.set(cache: .openGroupManager, to: mockOGMCache) try await mockStorage.perform(migrations: SNMessagingKit.migrations) @@ -60,6 +47,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) } + dependencies.set(singleton: .storage, to: mockStorage) try await mockUserDefaults.defaultInitialSetup() try await mockUserDefaults.when { $0.bool(forKey: .any) }.thenReturn(true) @@ -103,6 +91,17 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { ) ) dependencies.set(singleton: .network, to: mockNetwork) + + try await mockJobRunner + .when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) } + .thenReturn(nil) + try await mockJobRunner + .when { $0.upsert(.any, job: .any, canStartJob: .any) } + .thenReturn(nil) + try await mockJobRunner + .when { $0.jobInfoFor(jobs: .any, state: .any, variant: .any) } + .thenReturn([:]) + dependencies.set(singleton: .jobRunner, to: mockJobRunner) } // MARK: - a RetrieveDefaultOpenGroupRoomsJob @@ -145,7 +144,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { // MARK: -- defers the job if there is an existing job running it("defers the job if there is an existing job running") { - mockJobRunner + try await mockJobRunner .when { $0.jobInfoFor(jobs: .any, state: .running, variant: .retrieveDefaultOpenGroupRooms) } .thenReturn([ 101: JobRunner.JobInfo( @@ -170,7 +169,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { // MARK: -- does not defer the job when there is no existing job it("does not defer the job when there is no existing job") { - mockJobRunner + try await mockJobRunner .when { $0.jobInfoFor(jobs: .any, state: .running, variant: .retrieveDefaultOpenGroupRooms) } .thenReturn([:]) @@ -496,8 +495,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { using: dependencies ) - expect(mockJobRunner) - .to(call(matchingParameters: .all) { + await mockJobRunner + .verify { $0.add( .any, job: Job( @@ -515,7 +514,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { dependantJob: nil, canStartJob: true ) - }) + } + .wasCalled(exactly: 1, timeout: .milliseconds(100)) } // MARK: -- schedules a display picture download if the imageId has changed @@ -543,8 +543,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { using: dependencies ) - expect(mockJobRunner) - .to(call(matchingParameters: .all) { + await mockJobRunner + .verify { $0.add( .any, job: Job( @@ -562,7 +562,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { dependantJob: nil, canStartJob: true ) - }) + } + .wasCalled(exactly: 1, timeout: .milliseconds(100)) } // MARK: -- does not schedule a display picture download if there is no imageId @@ -611,8 +612,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { using: dependencies ) - expect(mockJobRunner) - .toNot(call { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) }) + await mockJobRunner + .verify { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) } + .wasNotCalled() } // MARK: -- does not schedule a display picture download if the imageId matches and the image has already been downloaded @@ -641,8 +643,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { using: dependencies ) - expect(mockJobRunner) - .toNot(call { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) }) + await mockJobRunner + .verify { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) } + .wasNotCalled() } // MARK: -- updates the cache with the default rooms @@ -656,8 +659,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { using: dependencies ) - expect(mockOGMCache) - .toNot(call(matchingParameters: .all) { + await mockOGMCache + .verify { $0.setDefaultRoomInfo([ ( room: OpenGroupAPI.Room.mock.with( @@ -693,7 +696,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: AsyncSpec { ) ) ]) - }) + } + .wasNotCalled() } } } diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift index e11514e57e..e1da914ebd 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift @@ -22,29 +22,16 @@ class LibSessionGroupInfoSpec: AsyncSpec { dependencies.forceSynchronous = true } @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) - @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( + @TestState var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), using: dependencies ) @TestState var mockNetwork: MockNetwork! = .create(using: dependencies) - @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( - initialSetup: { jobRunner in - jobRunner - .when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) } - .thenReturn(nil) - jobRunner - .when { $0.upsert(.any, job: .any, canStartJob: .any) } - .thenReturn(nil) - jobRunner - .when { $0.jobInfoFor(jobs: .any, state: .any, variant: .any) } - .thenReturn([:]) - } - ) + @TestState var mockJobRunner: MockJobRunner! = .create(using: dependencies) @TestState var createGroupOutput: LibSession.CreatedGroupInfo! - @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() + @TestState var mockLibSessionCache: MockLibSessionCache! = .create(using: dependencies) beforeEach { - /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) @@ -65,12 +52,24 @@ class LibSessionGroupInfoSpec: AsyncSpec { using: dependencies ) } + dependencies.set(singleton: .storage, to: mockStorage) + + try await mockJobRunner + .when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) } + .thenReturn(nil) + try await mockJobRunner + .when { $0.upsert(.any, job: .any, canStartJob: .any) } + .thenReturn(nil) + try await mockJobRunner + .when { $0.jobInfoFor(jobs: .any, state: .any, variant: .any) } + .thenReturn([:]) + dependencies.set(singleton: .jobRunner, to: mockJobRunner) var conf: UnsafeMutablePointer! var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) _ = user_groups_init(&conf, &secretKey, nil, 0, nil) - mockLibSessionCache.defaultInitialSetup( + try await mockLibSessionCache.defaultInitialSetup( configs: [ .userGroups: .userGroups(conf), .groupInfo: createGroupOutput.groupState[.groupInfo], @@ -78,7 +77,7 @@ class LibSessionGroupInfoSpec: AsyncSpec { .groupKeys: createGroupOutput.groupState[.groupKeys] ] ) - mockLibSessionCache.when { $0.configNeedsDump(.any) }.thenReturn(true) + try await mockLibSessionCache.when { $0.configNeedsDump(.any) }.thenReturn(true) dependencies.set(cache: .libSession, to: mockLibSessionCache) try await mockNetwork @@ -128,7 +127,7 @@ class LibSessionGroupInfoSpec: AsyncSpec { // MARK: ---- does nothing if there are no changes it("does nothing if there are no changes") { - mockLibSessionCache.when { $0.configNeedsDump(.any) }.thenReturn(false) + try await mockLibSessionCache.when { $0.configNeedsDump(.any) }.thenReturn(false) mockStorage.write { db in try mockLibSessionCache.handleGroupInfoUpdate( @@ -301,9 +300,9 @@ class LibSessionGroupInfoSpec: AsyncSpec { ) } - expect(mockJobRunner) - .to(call(.exactly(times: 1), matchingParameters: .all) { jobRunner in - jobRunner.add( + await mockJobRunner + .verify { + $0.add( .any, job: Job( variant: .displayPictureDownload, @@ -325,7 +324,8 @@ class LibSessionGroupInfoSpec: AsyncSpec { ), canStartJob: true ) - }) + } + .wasCalled(exactly: 1) } } @@ -626,9 +626,9 @@ class LibSessionGroupInfoSpec: AsyncSpec { ) } - expect(mockJobRunner) - .to(call(.exactly(times: 1), matchingParameters: .all) { jobRunner in - jobRunner.add( + await mockJobRunner + .verify { + $0.add( .any, job: Job( variant: .garbageCollection, @@ -642,7 +642,8 @@ class LibSessionGroupInfoSpec: AsyncSpec { ), canStartJob: true ) - }) + } + .wasCalled(exactly: 1) } // MARK: ------ does not delete messages with attachments after the timestamp @@ -843,7 +844,7 @@ class LibSessionGroupInfoSpec: AsyncSpec { // MARK: ---- deletes from the server after deleting messages before a given timestamp it("deletes from the server after deleting messages before a given timestamp") { - mockLibSessionCache + try await mockLibSessionCache .when { $0.authData(groupSessionId: .any) } .thenReturn( GroupAuthData( diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift index 9d90a75744..2e8d9a5a0c 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift @@ -21,29 +21,16 @@ class LibSessionGroupMembersSpec: AsyncSpec { dependencies.forceSynchronous = true } @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) - @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( + @TestState var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), using: dependencies ) @TestState var mockNetwork: MockNetwork! = .create(using: dependencies) - @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( - initialSetup: { jobRunner in - jobRunner - .when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) } - .thenReturn(nil) - jobRunner - .when { $0.upsert(.any, job: .any, canStartJob: .any) } - .thenReturn(nil) - jobRunner - .when { $0.jobInfoFor(jobs: .any, state: .any, variant: .any) } - .thenReturn([:]) - } - ) + @TestState var mockJobRunner: MockJobRunner! = .create(using: dependencies) @TestState var createGroupOutput: LibSession.CreatedGroupInfo! - @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() + @TestState var mockLibSessionCache: MockLibSessionCache! = .create(using: dependencies) beforeEach { - /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) @@ -64,12 +51,24 @@ class LibSessionGroupMembersSpec: AsyncSpec { using: dependencies ) } + dependencies.set(singleton: .storage, to: mockStorage) + + try await mockJobRunner + .when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) } + .thenReturn(nil) + try await mockJobRunner + .when { $0.upsert(.any, job: .any, canStartJob: .any) } + .thenReturn(nil) + try await mockJobRunner + .when { $0.jobInfoFor(jobs: .any, state: .any, variant: .any) } + .thenReturn([:]) + dependencies.set(singleton: .jobRunner, to: mockJobRunner) var conf: UnsafeMutablePointer! var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) _ = user_groups_init(&conf, &secretKey, nil, 0, nil) - mockLibSessionCache.defaultInitialSetup( + try await mockLibSessionCache.defaultInitialSetup( configs: [ .userGroups: .userGroups(conf), .groupInfo: createGroupOutput.groupState[.groupInfo], @@ -115,7 +114,7 @@ class LibSessionGroupMembersSpec: AsyncSpec { try createGroupOutput.group.insert(db) try createGroupOutput.members.forEach { try $0.insert(db) } } - mockLibSessionCache.when { $0.configNeedsDump(.any) }.thenReturn(true) + try await mockLibSessionCache.when { $0.configNeedsDump(.any) }.thenReturn(true) createGroupOutput.groupState[.groupMembers]?.conf.map { var cMemberId: [CChar] = "05\(TestConstants.publicKey)".cString(using: .utf8)! var member: config_group_member = config_group_member() @@ -130,7 +129,7 @@ class LibSessionGroupMembersSpec: AsyncSpec { // MARK: ---- does nothing if there are no changes it("does nothing if there are no changes") { - mockLibSessionCache.when { $0.configNeedsDump(.any) }.thenReturn(false) + try await mockLibSessionCache.when { $0.configNeedsDump(.any) }.thenReturn(false) mockStorage.write { db in try mockLibSessionCache.handleGroupMembersUpdate( diff --git a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift index 82842b2ec3..4ab693561d 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift @@ -21,17 +21,16 @@ class LibSessionSpec: AsyncSpec { dependencies.forceSynchronous = true } @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) - @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( + @TestState var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create(using: dependencies) + @TestState var mockCrypto: MockCrypto! = .create(using: dependencies) @TestState var createGroupOutput: LibSession.CreatedGroupInfo! - @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() + @TestState var mockLibSessionCache: MockLibSessionCache! = .create(using: dependencies) @TestState var userGroupsConfig: LibSession.Config! beforeEach { - /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) @@ -62,6 +61,7 @@ class LibSessionSpec: AsyncSpec { .thenReturn( Authentication.Signature.standard(signature: Array("TestSignature".data(using: .utf8)!)) ) + dependencies.set(singleton: .crypto, to: mockCrypto) try await mockStorage.perform(migrations: SNMessagingKit.migrations) try await mockStorage.writeAsync { db in @@ -80,12 +80,13 @@ class LibSessionSpec: AsyncSpec { using: dependencies ) } + dependencies.set(singleton: .storage, to: mockStorage) var conf: UnsafeMutablePointer! var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) _ = user_groups_init(&conf, &secretKey, nil, 0, nil) - mockLibSessionCache.defaultInitialSetup( + try await mockLibSessionCache.defaultInitialSetup( configs: [ .userGroups: .userGroups(conf), .groupInfo: createGroupOutput.groupState[.groupInfo], @@ -313,7 +314,7 @@ class LibSessionSpec: AsyncSpec { _ = user_groups_init(&userGroupsConf, &secretKey, nil, 0, nil) userGroupsConfig = .userGroups(userGroupsConf) - mockLibSessionCache + try await mockLibSessionCache .when { $0.config(for: .userGroups, sessionId: .any) } .thenReturn(userGroupsConfig) } @@ -558,11 +559,11 @@ class LibSessionSpec: AsyncSpec { ) } - expect(mockLibSessionCache).to(call(.exactly(times: 3)) { - $0.setConfig(for: .any, sessionId: .any, to: .any) - }) - expect(mockLibSessionCache) - .to(call(matchingParameters: .atLeast(2)) { + await mockLibSessionCache + .verify { $0.setConfig(for: .any, sessionId: .any, to: .any) } + .wasCalled(exactly: 3) + await mockLibSessionCache + .verify { $0.setConfig( for: .groupInfo, sessionId: SessionId( @@ -571,9 +572,10 @@ class LibSessionSpec: AsyncSpec { ), to: .any ) - }) - expect(mockLibSessionCache) - .to(call(matchingParameters: .atLeast(2)) { + } + .wasCalled(exactly: 1) + await mockLibSessionCache + .verify { $0.setConfig( for: .groupMembers, sessionId: SessionId( @@ -582,9 +584,10 @@ class LibSessionSpec: AsyncSpec { ), to: .any ) - }) - expect(mockLibSessionCache) - .to(call(matchingParameters: .atLeast(2)) { + } + .wasCalled(exactly: 1) + await mockLibSessionCache + .verify { $0.setConfig( for: .groupKeys, sessionId: SessionId( @@ -593,15 +596,16 @@ class LibSessionSpec: AsyncSpec { ), to: .any ) - }) + } + .wasCalled(exactly: 1) } } // MARK: -- when saving a created a group context("when saving a created a group") { beforeEach { - mockLibSessionCache.when { $0.configNeedsDump(.any) }.thenReturn(true) - mockLibSessionCache + try await mockLibSessionCache.when { $0.configNeedsDump(.any) }.thenReturn(true) + try await mockLibSessionCache .when { try $0.createDump(config: .any, for: .any, sessionId: .any, timestampMs: .any) } .then { args in mockStorage.write { db in @@ -680,15 +684,16 @@ class LibSessionSpec: AsyncSpec { ) } - expect(mockLibSessionCache) - .to(call(.exactly(times: 1), matchingParameters: .all) { + await mockLibSessionCache + .verify { try $0.performAndPushChange( .any, for: .userGroups, sessionId: SessionId(.standard, hex: TestConstants.publicKey), change: { _ in } ) - }) + } + .wasCalled(exactly: 1) } } } diff --git a/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift index e0abeb86a6..8e56dfa00f 100644 --- a/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift @@ -14,11 +14,12 @@ class CryptoOpenGroupAPISpec: AsyncSpec { // MARK: Configuration @TestState var dependencies: TestDependencies! = TestDependencies() - @TestState(singleton: .crypto, in: dependencies) var crypto: Crypto! = Crypto(using: dependencies) + @TestState var crypto: Crypto! = Crypto(using: dependencies) @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) beforeEach { - /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead + dependencies.set(singleton: .crypto, to: crypto) + try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) } diff --git a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift index e6c4e224ec..5f28ae773b 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift @@ -29,9 +29,13 @@ class SOGSMessageSpec: AsyncSpec { """ @TestState var messageData: Data! = messageJson.data(using: .utf8)! @TestState var dependencies: TestDependencies! = TestDependencies() - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create(using: dependencies) + @TestState var mockCrypto: MockCrypto! = .create(using: dependencies) @TestState var decoder: JSONDecoder! = JSONDecoder(using: dependencies) + beforeEach { + dependencies.set(singleton: .crypto, to: mockCrypto) + } + // MARK: - a SOGSMessage describe("a SOGSMessage") { // MARK: -- when decoding diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index 57b43b1534..fc4e419f70 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -21,18 +21,17 @@ class OpenGroupAPISpec: AsyncSpec { dependencies.forceSynchronous = true } @TestState var mockNetwork: MockNetwork! = .create(using: dependencies) - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create(using: dependencies) + @TestState var mockCrypto: MockCrypto! = .create(using: dependencies) @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) - @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() + @TestState var mockLibSessionCache: MockLibSessionCache! = .create(using: dependencies) @TestState var disposables: [AnyCancellable]! = [] @TestState var error: Error? beforeEach { - /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) - mockLibSessionCache.defaultInitialSetup() + try await mockLibSessionCache.defaultInitialSetup() dependencies.set(cache: .libSession, to: mockLibSessionCache) try await mockCrypto @@ -75,6 +74,7 @@ class OpenGroupAPISpec: AsyncSpec { try await mockCrypto .when { $0.generate(.x25519(ed25519Seckey: .any)) } .thenReturn(Array(Data(hex: TestConstants.privateKey))) + dependencies.set(singleton: .crypto, to: mockCrypto) dependencies.set(singleton: .network, to: mockNetwork) } @@ -290,7 +290,9 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ---- when blinded and checking for message requests context("when blinded and checking for message requests") { beforeEach { - mockLibSessionCache.when { $0.get(.checkForCommunityMessageRequests) }.thenReturn(true) + try await mockLibSessionCache + .when { $0.get(.checkForCommunityMessageRequests) } + .thenReturn(true) } // MARK: ------ includes the inbox and outbox endpoints @@ -453,7 +455,9 @@ class OpenGroupAPISpec: AsyncSpec { // MARK: ---- when blinded and not checking for message requests context("when blinded and not checking for message requests") { beforeEach { - mockLibSessionCache.when { $0.get(.checkForCommunityMessageRequests) }.thenReturn(false) + try await mockLibSessionCache + .when { $0.get(.checkForCommunityMessageRequests) } + .thenReturn(false) } // MARK: ------ includes the inbox and outbox endpoints diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 21ee08e906..d83482093c 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -109,36 +109,22 @@ class OpenGroupManagerSpec: AsyncSpec { base64EncodedMessage: try! proto.build().serializedData().base64EncodedString() ) }() - @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( + @TestState var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), using: dependencies ) - @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner( - initialSetup: { jobRunner in - jobRunner - .when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) } - .thenReturn(nil) - jobRunner - .when { $0.upsert(.any, job: .any, canStartJob: .any) } - .thenReturn(nil) - jobRunner - .when { $0.jobInfoFor(jobs: .any, state: .any, variant: .any) } - .thenReturn([:]) - } - ) + @TestState var mockJobRunner: MockJobRunner! = .create(using: dependencies) @TestState var mockNetwork: MockNetwork! = .create(using: dependencies) - @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = .create(using: dependencies) + @TestState var mockCrypto: MockCrypto! = .create(using: dependencies) @TestState var mockUserDefaults: MockUserDefaults! = .create(using: dependencies) @TestState var mockAppGroupDefaults: MockUserDefaults! = .create(using: dependencies) @TestState var mockGeneralCache: MockGeneralCache! = .create(using: dependencies) - @TestState var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache() - @TestState var mockOGMCache: MockOGMCache! = MockOGMCache() + @TestState var mockLibSessionCache: MockLibSessionCache! = .create(using: dependencies) + @TestState var mockOGMCache: MockOGMCache! = .create(using: dependencies) @TestState var mockPoller: MockPoller! = .create(using: dependencies) - @TestState(singleton: .communityPollerManager, in: dependencies) var mockCommunityPollerManager: MockCommunityPollerManager! = .create(using: dependencies) - @TestState(singleton: .keychain, in: dependencies) var mockKeychain: MockKeychain! = .create(using: dependencies) - @TestState(singleton: .fileManager, in: dependencies) var mockFileManager: MockFileManager! = MockFileManager( - initialSetup: { $0.defaultInitialSetup() } - ) + @TestState var mockCommunityPollerManager: MockCommunityPollerManager! = .create(using: dependencies) + @TestState var mockKeychain: MockKeychain! = .create(using: dependencies) + @TestState var mockFileManager: MockFileManager! = .create(using: dependencies) @TestState var userGroupsConf: UnsafeMutablePointer! @TestState var userGroupsInitResult: Int32! = { var secretKey: [UInt8] = Array(Data(hex: TestConstants.edSecretKey)) @@ -151,17 +137,19 @@ class OpenGroupManagerSpec: AsyncSpec { @TestState var openGroupManager: OpenGroupManager! = OpenGroupManager(using: dependencies) beforeEach { - /// The compiler kept crashing when doing this via `@TestState` so need to do it here instead try await mockGeneralCache.defaultInitialSetup() dependencies.set(cache: .general, to: mockGeneralCache) - mockLibSessionCache.defaultInitialSetup() + try await mockLibSessionCache.defaultInitialSetup() dependencies.set(cache: .libSession, to: mockLibSessionCache) - mockOGMCache.when { $0.pendingChanges }.thenReturn([]) - mockOGMCache.when { $0.pendingChanges = .any }.thenReturn(()) - mockOGMCache.when { $0.getLastSuccessfulCommunityPollTimestamp() }.thenReturn(0) - mockOGMCache.when { $0.setDefaultRoomInfo(.any) }.thenReturn(()) + try await mockFileManager.defaultInitialSetup() + dependencies.set(singleton: .fileManager, to: mockFileManager) + + try await mockOGMCache.when { $0.pendingChanges }.thenReturn([]) + try await mockOGMCache.when { $0.pendingChanges = .any }.thenReturn(()) + try await mockOGMCache.when { $0.getLastSuccessfulCommunityPollTimestamp() }.thenReturn(0) + try await mockOGMCache.when { $0.setDefaultRoomInfo(.any) }.thenReturn(()) dependencies.set(cache: .openGroupManager, to: mockOGMCache) try await mockStorage.perform(migrations: SNMessagingKit.migrations) @@ -175,6 +163,7 @@ class OpenGroupManagerSpec: AsyncSpec { try testOpenGroup.insert(db) try Capability(openGroupServer: testOpenGroup.server, variant: .sogs, isMissing: false).insert(db) } + dependencies.set(singleton: .storage, to: mockStorage) try await mockCrypto.when { $0.generate(.hash(message: .any, length: .any)) }.thenReturn([]) try await mockCrypto @@ -216,6 +205,7 @@ class OpenGroupManagerSpec: AsyncSpec { try await mockCrypto .when { $0.generate(.ciphertextWithXChaCha20(plaintext: .any, encKey: .any)) } .thenReturn(Data([1, 2, 3])) + dependencies.set(singleton: .crypto, to: mockCrypto) try await mockPoller.when { await $0.startIfNeeded() }.thenReturn(()) try await mockPoller.when { await $0.stop() }.thenReturn(()) @@ -230,6 +220,7 @@ class OpenGroupManagerSpec: AsyncSpec { try await mockCommunityPollerManager .when { $0.syncState } .thenReturn(CommunityPollerManagerSyncState()) + dependencies.set(singleton: .communityPollerManager, to: mockCommunityPollerManager) try await mockKeychain .when { @@ -242,6 +233,7 @@ class OpenGroupManagerSpec: AsyncSpec { ) } .thenReturn(Data([1, 2, 3])) + dependencies.set(singleton: .keychain, to: mockKeychain) try await mockUserDefaults.defaultInitialSetup() try await mockUserDefaults.when { $0.integer(forKey: .any) }.thenReturn(0) @@ -251,6 +243,17 @@ class OpenGroupManagerSpec: AsyncSpec { try await mockAppGroupDefaults.when { $0.bool(forKey: .any) }.thenReturn(false) dependencies.set(defaults: .appGroup, to: mockAppGroupDefaults) + try await mockJobRunner + .when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) } + .thenReturn(nil) + try await mockJobRunner + .when { $0.upsert(.any, job: .any, canStartJob: .any) } + .thenReturn(nil) + try await mockJobRunner + .when { $0.jobInfoFor(jobs: .any, state: .any, variant: .any) } + .thenReturn([:]) + dependencies.set(singleton: .jobRunner, to: mockJobRunner) + try await mockNetwork.when { $0.networkStatus }.thenReturn(.singleValue(value: .connected)) try await mockNetwork .when { @@ -552,8 +555,10 @@ class OpenGroupManagerSpec: AsyncSpec { // MARK: ------ returns true it("returns true") { try await mockCommunityPollerManager - .when { await $0.serversBeingPolled } - .thenReturn(["http://116.203.70.33"]) + .when { $0.syncState } + .thenReturn(CommunityPollerManagerSyncState( + serversBeingPolled: ["http://116.203.70.33"] + )) mockStorage.write { db in try SessionThread( id: OpenGroup.idFor(roomToken: "testRoom", server: "http://116.203.70.33"), @@ -586,8 +591,10 @@ class OpenGroupManagerSpec: AsyncSpec { // MARK: ------ returns true it("returns true") { try await mockCommunityPollerManager - .when { await $0.serversBeingPolled } - .thenReturn(["http://open.getsession.org"]) + .when { $0.syncState } + .thenReturn(CommunityPollerManagerSyncState( + serversBeingPolled: ["http://open.getsession.org"] + )) mockStorage.write { db in try SessionThread( id: OpenGroup.idFor(roomToken: "testRoom", server: "http://open.getsession.org"), @@ -768,15 +775,17 @@ class OpenGroupManagerSpec: AsyncSpec { context("an existing room") { beforeEach { try await mockCommunityPollerManager - .when { await $0.serversBeingPolled } - .thenReturn(["http://127.0.0.1"]) + .when { $0.syncState } + .thenReturn(CommunityPollerManagerSyncState( + serversBeingPolled: ["http://127.0.0.1"] + )) mockStorage.write { db in try testOpenGroup.insert(db) } } - // MARK: ------ does not reset the sequence number or update the public key - it("does not reset the sequence number or update the public key") { + // MARK: ------ does not reset the sequence number + it("does not reset the sequence number") { mockStorage .writePublisher { db -> Bool in openGroupManager.add( @@ -802,22 +811,14 @@ class OpenGroupManagerSpec: AsyncSpec { } .sinkAndStore(in: &disposables) - expect( + await expect( mockStorage.read { db in try OpenGroup .select(.sequenceNumber) .asRequest(of: Int64.self) .fetchOne(db) } - ).to(equal(5)) - expect( - mockStorage.read { db in - try OpenGroup - .select(.publicKey) - .asRequest(of: String.self) - .fetchOne(db) - } - ).to(equal(TestConstants.publicKey)) + ).toEventually(equal(5)) } } @@ -1132,8 +1133,8 @@ class OpenGroupManagerSpec: AsyncSpec { ) } - expect(mockJobRunner) - .toNot(call(matchingParameters: .all) { + await mockJobRunner + .verify { $0.add( .any, job: Job( @@ -1151,7 +1152,8 @@ class OpenGroupManagerSpec: AsyncSpec { dependantJob: nil, canStartJob: true ) - }) + } + .wasNotCalled() } // MARK: ---- schedules the displayPictureDownload job if there is an image @@ -1181,8 +1183,8 @@ class OpenGroupManagerSpec: AsyncSpec { ) } - expect(mockJobRunner) - .to(call(matchingParameters: .all) { + await mockJobRunner + .verify { $0.add( .any, job: Job( @@ -1200,7 +1202,8 @@ class OpenGroupManagerSpec: AsyncSpec { dependantJob: nil, canStartJob: true ) - }) + } + .wasCalled(exactly: 1) } // MARK: ---- and updating the moderator list @@ -1531,8 +1534,8 @@ class OpenGroupManagerSpec: AsyncSpec { .fetchOne(db) } ).to(equal("10")) - expect(mockJobRunner) - .to(call(.exactly(times: 1), matchingParameters: .all) { + await mockJobRunner + .verify { $0.add( .any, job: Job( @@ -1550,7 +1553,8 @@ class OpenGroupManagerSpec: AsyncSpec { dependantJob: nil, canStartJob: true ) - }) + } + .wasCalled(exactly: 1) } // MARK: ------ uses the existing room image id if none is provided @@ -1603,7 +1607,9 @@ class OpenGroupManagerSpec: AsyncSpec { .fetchOne(db) } ).toNot(beNil()) - expect(mockJobRunner).toNot(call { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) }) + await mockJobRunner + .verify { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) } + .wasNotCalled() } // MARK: ------ uses the new room image id if there is an existing one @@ -1661,8 +1667,8 @@ class OpenGroupManagerSpec: AsyncSpec { .fetchOne(db) } ).toNot(beNil()) - expect(mockJobRunner) - .to(call(.exactly(times: 1), matchingParameters: .all) { + await mockJobRunner + .verify { $0.add( .any, job: Job( @@ -1680,7 +1686,8 @@ class OpenGroupManagerSpec: AsyncSpec { dependantJob: nil, canStartJob: true ) - }) + } + .wasCalled(exactly: 1) } // MARK: ------ does nothing if there is no room image @@ -2590,7 +2597,7 @@ class OpenGroupManagerSpec: AsyncSpec { overallTimeout: expectedRequest.overallTimeout ) } - .wasCalled(exactly: 1) + .wasCalled(exactly: 1, timeout: .milliseconds(100)) } // MARK: ---- does not start a job to retrieve the default rooms if we already have rooms diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index 386c9b7afa..267cc29b22 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -94,8 +94,8 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockJobRunner) - .to(call(.exactly(times: 1), matchingParameters: .all) { + await fixture.mockJobRunner + .verify { $0.add( .any, job: Job( @@ -113,7 +113,8 @@ class MessageReceiverGroupsSpec: AsyncSpec { ), canStartJob: true ) - }) + } + .wasCalled(exactly: 1) } // MARK: ------ schedules but does not start a displayPictureDownload job when not the main app @@ -137,8 +138,8 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockJobRunner) - .to(call(.exactly(times: 1), matchingParameters: .all) { + await fixture.mockJobRunner + .verify { $0.add( .any, job: Job( @@ -156,7 +157,8 @@ class MessageReceiverGroupsSpec: AsyncSpec { ), canStartJob: false ) - }) + } + .wasCalled(exactly: 1) } } } @@ -220,7 +222,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ---- from a sender that is not approved context("from a sender that is not approved") { beforeEach { - fixture.mockLibSessionCache + try await fixture.mockLibSessionCache .when { $0.isMessageRequest(threadId: .any, threadVariant: .any) } .thenReturn(true) fixture.mockStorage.write { db in @@ -300,7 +302,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { try await fixture.mockUserDefaults .when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) } .thenReturn(true) - fixture.mockLibSessionCache + try await fixture.mockLibSessionCache .when { $0.isMessageRequest(threadId: .any, threadVariant: .any) } .thenReturn(true) @@ -345,7 +347,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ---- from a sender that is approved context("from a sender that is approved") { beforeEach { - fixture.mockLibSessionCache + try await fixture.mockLibSessionCache .when { $0.isMessageRequest(threadId: .any, threadVariant: .any) } .thenReturn(false) fixture.mockStorage.write { db in @@ -379,7 +381,7 @@ class MessageReceiverGroupsSpec: AsyncSpec { // MARK: ------ creates the group state it("creates the group state") { - fixture.mockLibSessionCache + try await fixture.mockLibSessionCache .when { $0.hasConfig(for: .any, sessionId: .any) } .thenReturn(false) fixture.mockStorage.write { db in @@ -394,18 +396,15 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockLibSessionCache) - .to(call(.exactly(times: 1), matchingParameters: .atLeast(2)) { - $0.setConfig(for: .groupInfo, sessionId: fixture.groupId, to: .any) - }) - expect(fixture.mockLibSessionCache) - .to(call(.exactly(times: 1), matchingParameters: .atLeast(2)) { - $0.setConfig(for: .groupMembers, sessionId: fixture.groupId, to: .any) - }) - expect(fixture.mockLibSessionCache) - .to(call(.exactly(times: 1), matchingParameters: .atLeast(2)) { - $0.setConfig(for: .groupKeys, sessionId: fixture.groupId, to: .any) - }) + await fixture.mockLibSessionCache + .verify { $0.setConfig(for: .groupInfo, sessionId: fixture.groupId, to: .any) } + .wasCalled(exactly: 1) + await fixture.mockLibSessionCache + .verify { $0.setConfig(for: .groupMembers, sessionId: fixture.groupId, to: .any) } + .wasCalled(exactly: 1) + await fixture.mockLibSessionCache + .verify { $0.setConfig(for: .groupKeys, sessionId: fixture.groupId, to: .any) } + .wasCalled(exactly: 1) } // MARK: ------ adds the group to USER_GROUPS with the invited flag set to false @@ -833,13 +832,14 @@ class MessageReceiverGroupsSpec: AsyncSpec { using: fixture.dependencies ) } - - expect(fixture.mockLibSessionCache).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.loadAdminKey( - groupIdentitySeed: fixture.groupSeed, - groupSessionId: SessionId(.group, publicKey: [1, 2, 3]) - ) - }) + await fixture.mockLibSessionCache + .verify { + try $0.loadAdminKey( + groupIdentitySeed: fixture.groupSeed, + groupSessionId: SessionId(.group, publicKey: [1, 2, 3]) + ) + } + .wasCalled(exactly: 1) } // MARK: ---- replaces the memberAuthData with the admin key in the database @@ -1660,8 +1660,8 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockJobRunner) - .to(call(matchingParameters: .all) { + await fixture.mockJobRunner + .verify { $0.add( .any, job: Job( @@ -1673,7 +1673,8 @@ class MessageReceiverGroupsSpec: AsyncSpec { ), canStartJob: true ) - }) + } + .wasCalled(exactly: 1) } // MARK: ------ does not schedule a member change control message to be sent @@ -1690,8 +1691,8 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockJobRunner) - .toNot(call(.exactly(times: 1), matchingParameters: .all) { + await fixture.mockJobRunner + .verify { $0.add( .any, job: Job( @@ -1717,7 +1718,8 @@ class MessageReceiverGroupsSpec: AsyncSpec { ), canStartJob: true ) - }) + } + .wasNotCalled() } } } @@ -2800,10 +2802,9 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockLibSessionCache) - .to(call(.exactly(times: 1), matchingParameters: .all) { - $0.removeConfigs(for: fixture.groupId) - }) + await fixture.mockLibSessionCache + .verify { $0.removeConfigs(for: fixture.groupId) } + .wasCalled(exactly: 1) } // MARK: ---- removes the cached libSession state dumps @@ -2817,10 +2818,9 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockLibSessionCache) - .to(call(.exactly(times: 1), matchingParameters: .all) { - $0.removeConfigs(for: fixture.groupId) - }) + await fixture.mockLibSessionCache + .verify { $0.removeConfigs(for: fixture.groupId) } + .wasCalled(exactly: 1) let dumps: [ConfigDump]? = fixture.mockStorage.read { db in try ConfigDump @@ -3027,9 +3027,9 @@ class MessageReceiverGroupsSpec: AsyncSpec { ) } - expect(fixture.mockLibSessionCache).to(call(.exactly(times: 1), matchingParameters: .all) { - try $0.markAsKicked(groupSessionIds: [fixture.groupId.hexString]) - }) + await fixture.mockLibSessionCache + .verify { try $0.markAsKicked(groupSessionIds: [fixture.groupId.hexString]) } + .wasCalled(exactly: 1) } } @@ -3251,17 +3251,17 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { } } var mockNetwork: MockNetwork { mock(for: .network) } - var mockJobRunner: MockJobRunner { mock(for: .jobRunner) { MockJobRunner() } } + var mockJobRunner: MockJobRunner { mock(for: .jobRunner) } var mockAppContext: MockAppContext { mock(for: .appContext) } var mockUserDefaults: MockUserDefaults { mock(defaults: .standard) } var mockCrypto: MockCrypto { mock(for: .crypto) } var mockKeychain: MockKeychain { mock(for: .keychain) } - var mockFileManager: MockFileManager { mock(for: .fileManager) { MockFileManager() } } + var mockFileManager: MockFileManager { mock(for: .fileManager) } var mockExtensionHelper: MockExtensionHelper { mock(for: .extensionHelper) } var mockGroupPollerManager: MockGroupPollerManager { mock(for: .groupPollerManager) } var mockNotificationsManager: MockNotificationsManager { mock(for: .notificationsManager) } var mockGeneralCache: MockGeneralCache { mock(cache: .general) } - var mockLibSessionCache: MockLibSessionCache { mock(cache: .libSession) { MockLibSessionCache() } } + var mockLibSessionCache: MockLibSessionCache { mock(cache: .libSession) } var mockSnodeAPICache: MockSnodeAPICache { mock(cache: .snodeAPI) } var mockPoller: MockPoller { mock() } @@ -3536,17 +3536,17 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { private func applyBaselineStubs() async throws { try await applyBaselineStorage() try await applyBaselineNetwork() - await applyBaselineJobRunner() + try await applyBaselineJobRunner() try await applyBaselineAppContext() try await applyBaselineUserDefaults() try await applyBaselineCrypto() try await applyBaselineKeychain() - await applyBaselineFileManager() + try await applyBaselineFileManager() try await applyBaselineExtensionHelper() try await applyBaselineGroupPollerManager() try await applyBaselineNotificationsManager() try await applyBaselineGeneralCache() - await applyBaselineLibSessionCache() + try await applyBaselineLibSessionCache() try await applyBaselineSnodeAPICache() try await applyBaselinePoller() } @@ -3609,11 +3609,19 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { ]) } - private func applyBaselineJobRunner() async { - mockJobRunner.when { $0.jobInfoFor(jobs: .any, state: .any, variant: .any) }.thenReturn([:]) - mockJobRunner.when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) }.thenReturn(nil) - mockJobRunner.when { $0.upsert(.any, job: .any, canStartJob: .any) }.thenReturn(nil) - mockJobRunner.when { $0.manuallyTriggerResult(.any, result: .any) }.thenReturn(()) + private func applyBaselineJobRunner() async throws { + try await mockJobRunner + .when { $0.jobInfoFor(jobs: .any, state: .any, variant: .any) } + .thenReturn([:]) + try await mockJobRunner + .when { $0.add(.any, job: .any, dependantJob: .any, canStartJob: .any) } + .thenReturn(nil) + try await mockJobRunner + .when { $0.upsert(.any, job: .any, canStartJob: .any) } + .thenReturn(nil) + try await mockJobRunner + .when { $0.manuallyTriggerResult(.any, result: .any) } + .thenReturn(()) } private func applyBaselineAppContext() async throws { @@ -3674,8 +3682,8 @@ private class MessageReceiverGroupsTestFixture: FixtureBase { .thenReturn(Data((0..