Started working on the integration
# Conflicts: # Session.xcodeproj/project.pbxproj # SessionMessagingKit/Configuration.swift # SessionMessagingKit/Database/Migrations/_012_AddClosedGroupInfo.swift # SessionMessagingKit/Database/Migrations/_013_AutoDownloadAttachments.swift # SessionMessagingKit/Open Groups/Models/SOGSBatchRequest.swift # SessionSnodeKit/OnionRequestAPI.swift # SessionSnodeKit/SSKDependencies.swift # SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift # SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift # SessionTests/Settings/NotificationContentViewModelSpec.swift # SessionUtilitiesKit/Networking/BatchResponse.swift
This commit is contained in:
parent
ff65c84504
commit
edf3bde573
|
@ -642,8 +642,6 @@
|
|||
FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; };
|
||||
FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */; };
|
||||
FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; };
|
||||
FD3C906227E411AF00CD579F /* HeaderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906127E411AF00CD579F /* HeaderSpec.swift */; };
|
||||
FD3C906427E4122F00CD579F /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906327E4122F00CD579F /* RequestSpec.swift */; };
|
||||
FD3C906727E416AF00CD579F /* BlindedIdLookupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */; };
|
||||
FD3C906A27E417CE00CD579F /* SodiumUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */; };
|
||||
FD3C906D27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */; };
|
||||
|
@ -753,6 +751,40 @@
|
|||
FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */; };
|
||||
FD87DD0028B820F200AF0F98 /* BlockedContactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */; };
|
||||
FD87DD0428B8727D00AF0F98 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DD0328B8727D00AF0F98 /* Configuration.swift */; };
|
||||
FD8ECF40292AF07900C0D1BB /* Poller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF3F292AF07900C0D1BB /* Poller.swift */; };
|
||||
FD8ECF42292B340D00C0D1BB /* SOGSBatchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF41292B340D00C0D1BB /* SOGSBatchRequest.swift */; };
|
||||
FD8ECF44292B397E00C0D1BB /* SendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF43292B397E00C0D1BB /* SendMessageRequest.swift */; };
|
||||
FD8ECF46292B4BD500C0D1BB /* SendMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF45292B4BD500C0D1BB /* SendMessageResponse.swift */; };
|
||||
FD8ECF48292C287500C0D1BB /* UpdateExpiryRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF47292C287500C0D1BB /* UpdateExpiryRequest.swift */; };
|
||||
FD8ECF4A292C2A7300C0D1BB /* DeleteMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF49292C2A7300C0D1BB /* DeleteMessagesRequest.swift */; };
|
||||
FD8ECF4C292C2AC200C0D1BB /* RevokeSubkeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF4B292C2AC200C0D1BB /* RevokeSubkeyRequest.swift */; };
|
||||
FD8ECF4E292C2AF800C0D1BB /* DeleteAllMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF4D292C2AF800C0D1BB /* DeleteAllMessagesRequest.swift */; };
|
||||
FD8ECF50292C2B2B00C0D1BB /* DeleteAllBeforeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF4F292C2B2B00C0D1BB /* DeleteAllBeforeRequest.swift */; };
|
||||
FD8ECF52292C2CAE00C0D1BB /* UpdateExpiryAllRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF51292C2CAE00C0D1BB /* UpdateExpiryAllRequest.swift */; };
|
||||
FD8ECF54292C2DB000C0D1BB /* SnodeAuthenticatedRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF53292C2DB000C0D1BB /* SnodeAuthenticatedRequestBody.swift */; };
|
||||
FD8ECF56292C327700C0D1BB /* LegacyGetMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF55292C327700C0D1BB /* LegacyGetMessagesRequest.swift */; };
|
||||
FD8ECF58292C350500C0D1BB /* LegacySendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF57292C350500C0D1BB /* LegacySendMessageRequest.swift */; };
|
||||
FD8ECF5A292C431B00C0D1BB /* GetSwarmRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF59292C431B00C0D1BB /* GetSwarmRequest.swift */; };
|
||||
FD8ECF5C292C469100C0D1BB /* UpdateExpiryResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF5B292C469100C0D1BB /* UpdateExpiryResponse.swift */; };
|
||||
FD8ECF5E292C478900C0D1BB /* SnodeRecursiveResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF5D292C478900C0D1BB /* SnodeRecursiveResponse.swift */; };
|
||||
FD8ECF60292C4B2400C0D1BB /* SnodeSwarmItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF5F292C4B2400C0D1BB /* SnodeSwarmItem.swift */; };
|
||||
FD8ECF62292C4C8200C0D1BB /* DeleteMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF61292C4C8200C0D1BB /* DeleteMessagesResponse.swift */; };
|
||||
FD8ECF64292C4D6600C0D1BB /* DeleteAllMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF63292C4D6600C0D1BB /* DeleteAllMessagesResponse.swift */; };
|
||||
FD8ECF66292C6F8200C0D1BB /* DeleteAllBeforeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF65292C6F8200C0D1BB /* DeleteAllBeforeResponse.swift */; };
|
||||
FD8ECF68292C72BA00C0D1BB /* UpdateExpiryAllResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF67292C72B900C0D1BB /* UpdateExpiryAllResponse.swift */; };
|
||||
FD8ECF6A292C74A000C0D1BB /* RevokeSubkeyResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF69292C74A000C0D1BB /* RevokeSubkeyResponse.swift */; };
|
||||
FD8ECF6C292C9B6400C0D1BB /* GetNetworkTimestampResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF6B292C9B6400C0D1BB /* GetNetworkTimestampResponse.swift */; };
|
||||
FD8ECF6E292C9EA100C0D1BB /* GetServiceNodesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF6D292C9EA100C0D1BB /* GetServiceNodesRequest.swift */; };
|
||||
FD8ECF72292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF71292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift */; };
|
||||
FD8ECF74292DDB4A00C0D1BB /* Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF73292DDB4A00C0D1BB /* Format.swift */; };
|
||||
FD8ECF7929340F7200C0D1BB /* libsession-util.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */; };
|
||||
FD8ECF7B29340FFD00C0D1BB /* SessionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */; };
|
||||
FD8ECF7D2934293A00C0D1BB /* _011_SharedUtilChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7C2934293A00C0D1BB /* _011_SharedUtilChanges.swift */; };
|
||||
FD8ECF7F2934298100C0D1BB /* SharedConfigDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7E2934298100C0D1BB /* SharedConfigDump.swift */; };
|
||||
FD8ECF822934387A00C0D1BB /* ConfigUserProfileSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */; };
|
||||
FD8ECF852934508B00C0D1BB /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF842934508B00C0D1BB /* BatchResponseSpec.swift */; };
|
||||
FD8ECF8629346DA100C0D1BB /* HeaderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906127E411AF00CD579F /* HeaderSpec.swift */; };
|
||||
FD8ECF8729346DB500C0D1BB /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906327E4122F00CD579F /* RequestSpec.swift */; };
|
||||
FD90040F2818AB6D00ABAAF6 /* GetSnodePoolJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */; };
|
||||
FD9004122818ABDC00ABAAF6 /* Job.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73F280402C4004C14C5 /* Job.swift */; };
|
||||
FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */; };
|
||||
|
@ -835,6 +867,8 @@
|
|||
FDF0B75A2807F3A3004C14C5 /* MessageSenderError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7592807F3A3004C14C5 /* MessageSenderError.swift */; };
|
||||
FDF0B75C2807F41D004C14C5 /* MessageSender+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B75B2807F41D004C14C5 /* MessageSender+Convenience.swift */; };
|
||||
FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B75D280AAF35004C14C5 /* Preferences.swift */; };
|
||||
FDF1AD5F28FF5F930080A701 /* EditGroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF1AD5E28FF5F930080A701 /* EditGroupViewModel.swift */; };
|
||||
FDF1AD6128FF61110080A701 /* _012_AddClosedGroupInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF1AD6028FF61110080A701 /* _012_AddClosedGroupInfo.swift */; };
|
||||
FDF222072818CECF000A4995 /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF222062818CECF000A4995 /* ConversationViewModel.swift */; };
|
||||
FDF222092818D2B0000A4995 /* NSAttributedString+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */; };
|
||||
FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF2220A2818F38D000A4995 /* SessionApp.swift */; };
|
||||
|
@ -1721,6 +1755,13 @@
|
|||
FD37EA1628AC5605003AE748 /* NotificationContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentViewModel.swift; sourceTree = "<group>"; };
|
||||
FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundViewModel.swift; sourceTree = "<group>"; };
|
||||
FD37EA1A28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = "<group>"; };
|
||||
FD37F5572908F5C3005A5E92 /* RemoveUsersModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveUsersModal.swift; sourceTree = "<group>"; };
|
||||
FD37F559290F9E9A005A5E92 /* GroupMembersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMembersViewModel.swift; sourceTree = "<group>"; };
|
||||
FD37F55F291867A8005A5E92 /* ImagePickerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerHandler.swift; sourceTree = "<group>"; };
|
||||
FD37F5612918C471005A5E92 /* GroupMemberLeftMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberLeftMessage.swift; sourceTree = "<group>"; };
|
||||
FD37F5632918C58D005A5E92 /* GroupInviteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInviteMessage.swift; sourceTree = "<group>"; };
|
||||
FD37F5652918C697005A5E92 /* GroupPromoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupPromoteMessage.swift; sourceTree = "<group>"; };
|
||||
FD37F5672918D458005A5E92 /* MessageReceiver+ClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ClosedGroups.swift"; sourceTree = "<group>"; };
|
||||
FD39352B28F382920084DADA /* VersionFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionFooterView.swift; sourceTree = "<group>"; };
|
||||
FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = "<group>"; };
|
||||
FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -1832,6 +1873,38 @@
|
|||
FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactsViewModel.swift; sourceTree = "<group>"; };
|
||||
FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactCell.swift; sourceTree = "<group>"; };
|
||||
FD87DD0328B8727D00AF0F98 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
|
||||
FD8ECF3F292AF07900C0D1BB /* Poller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poller.swift; sourceTree = "<group>"; };
|
||||
FD8ECF41292B340D00C0D1BB /* SOGSBatchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSBatchRequest.swift; sourceTree = "<group>"; };
|
||||
FD8ECF43292B397E00C0D1BB /* SendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequest.swift; sourceTree = "<group>"; };
|
||||
FD8ECF45292B4BD500C0D1BB /* SendMessageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageResponse.swift; sourceTree = "<group>"; };
|
||||
FD8ECF47292C287500C0D1BB /* UpdateExpiryRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExpiryRequest.swift; sourceTree = "<group>"; };
|
||||
FD8ECF49292C2A7300C0D1BB /* DeleteMessagesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteMessagesRequest.swift; sourceTree = "<group>"; };
|
||||
FD8ECF4B292C2AC200C0D1BB /* RevokeSubkeyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokeSubkeyRequest.swift; sourceTree = "<group>"; };
|
||||
FD8ECF4D292C2AF800C0D1BB /* DeleteAllMessagesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllMessagesRequest.swift; sourceTree = "<group>"; };
|
||||
FD8ECF4F292C2B2B00C0D1BB /* DeleteAllBeforeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllBeforeRequest.swift; sourceTree = "<group>"; };
|
||||
FD8ECF51292C2CAE00C0D1BB /* UpdateExpiryAllRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExpiryAllRequest.swift; sourceTree = "<group>"; };
|
||||
FD8ECF53292C2DB000C0D1BB /* SnodeAuthenticatedRequestBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeAuthenticatedRequestBody.swift; sourceTree = "<group>"; };
|
||||
FD8ECF55292C327700C0D1BB /* LegacyGetMessagesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGetMessagesRequest.swift; sourceTree = "<group>"; };
|
||||
FD8ECF57292C350500C0D1BB /* LegacySendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySendMessageRequest.swift; sourceTree = "<group>"; };
|
||||
FD8ECF59292C431B00C0D1BB /* GetSwarmRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSwarmRequest.swift; sourceTree = "<group>"; };
|
||||
FD8ECF5B292C469100C0D1BB /* UpdateExpiryResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExpiryResponse.swift; sourceTree = "<group>"; };
|
||||
FD8ECF5D292C478900C0D1BB /* SnodeRecursiveResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeRecursiveResponse.swift; sourceTree = "<group>"; };
|
||||
FD8ECF5F292C4B2400C0D1BB /* SnodeSwarmItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeSwarmItem.swift; sourceTree = "<group>"; };
|
||||
FD8ECF61292C4C8200C0D1BB /* DeleteMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteMessagesResponse.swift; sourceTree = "<group>"; };
|
||||
FD8ECF63292C4D6600C0D1BB /* DeleteAllMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllMessagesResponse.swift; sourceTree = "<group>"; };
|
||||
FD8ECF65292C6F8200C0D1BB /* DeleteAllBeforeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllBeforeResponse.swift; sourceTree = "<group>"; };
|
||||
FD8ECF67292C72B900C0D1BB /* UpdateExpiryAllResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExpiryAllResponse.swift; sourceTree = "<group>"; };
|
||||
FD8ECF69292C74A000C0D1BB /* RevokeSubkeyResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokeSubkeyResponse.swift; sourceTree = "<group>"; };
|
||||
FD8ECF6B292C9B6400C0D1BB /* GetNetworkTimestampResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetNetworkTimestampResponse.swift; sourceTree = "<group>"; };
|
||||
FD8ECF6D292C9EA100C0D1BB /* GetServiceNodesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetServiceNodesRequest.swift; sourceTree = "<group>"; };
|
||||
FD8ECF71292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _013_AutoDownloadAttachments.swift; sourceTree = "<group>"; };
|
||||
FD8ECF73292DDB4A00C0D1BB /* Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Format.swift; sourceTree = "<group>"; };
|
||||
FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "libsession-util.xcframework"; sourceTree = "<group>"; };
|
||||
FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionUtil.swift; sourceTree = "<group>"; };
|
||||
FD8ECF7C2934293A00C0D1BB /* _011_SharedUtilChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _011_SharedUtilChanges.swift; sourceTree = "<group>"; };
|
||||
FD8ECF7E2934298100C0D1BB /* SharedConfigDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedConfigDump.swift; sourceTree = "<group>"; };
|
||||
FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigUserProfileSpec.swift; sourceTree = "<group>"; };
|
||||
FD8ECF842934508B00C0D1BB /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = "<group>"; };
|
||||
FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSnodePoolJob.swift; sourceTree = "<group>"; };
|
||||
FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = "<group>"; };
|
||||
FDA8EAFD280E8B78002B68E5 /* FailedMessageSendsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedMessageSendsJob.swift; sourceTree = "<group>"; };
|
||||
|
@ -1915,6 +1988,8 @@
|
|||
FDF0B7592807F3A3004C14C5 /* MessageSenderError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderError.swift; sourceTree = "<group>"; };
|
||||
FDF0B75B2807F41D004C14C5 /* MessageSender+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageSender+Convenience.swift"; sourceTree = "<group>"; };
|
||||
FDF0B75D280AAF35004C14C5 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||
FDF1AD5E28FF5F930080A701 /* EditGroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditGroupViewModel.swift; sourceTree = "<group>"; };
|
||||
FDF1AD6028FF61110080A701 /* _012_AddClosedGroupInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _012_AddClosedGroupInfo.swift; sourceTree = "<group>"; };
|
||||
FDF222062818CECF000A4995 /* ConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = "<group>"; };
|
||||
FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FDF2220A2818F38D000A4995 /* SessionApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionApp.swift; sourceTree = "<group>"; };
|
||||
|
@ -1998,6 +2073,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FD8ECF7929340F7200C0D1BB /* libsession-util.xcframework in Frameworks */,
|
||||
FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */,
|
||||
C3C2A70B25539E1E00C340D1 /* SessionSnodeKit.framework in Frameworks */,
|
||||
CEE449BA3596483519120D91 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework in Frameworks */,
|
||||
|
@ -2241,14 +2317,6 @@
|
|||
path = "Views & Modals";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7B81682428B30BEC0069F315 /* Recovered References */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD37EA1A28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7B8C44C328B49DA900FBE25F /* New Conversation */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -3232,6 +3300,8 @@
|
|||
C3A721332558BDDF0043A11F /* Open Groups */,
|
||||
FD3E0C82283B581F002A425C /* Shared Models */,
|
||||
C3BBE0B32554F0D30050F1E3 /* Utilities */,
|
||||
FD8ECF7529340F4800C0D1BB /* LibSessionUtil */,
|
||||
FDC438C027BB4E6800C60D73 /* SMKDependencies.swift */,
|
||||
FD245C612850664300B966DD /* Configuration.swift */,
|
||||
);
|
||||
path = SessionMessagingKit;
|
||||
|
@ -3391,7 +3461,6 @@
|
|||
D221A08C169C9E5E00537ABF /* Frameworks */,
|
||||
D221A08A169C9E5E00537ABF /* Products */,
|
||||
2BADBA206E0B8D297E313FBA /* Pods */,
|
||||
7B81682428B30BEC0069F315 /* Recovered References */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
@ -3524,6 +3593,7 @@
|
|||
FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */,
|
||||
FD5C7308285007920029977D /* BlindedIdLookup.swift */,
|
||||
FD09B7E6288670FD00ED0B66 /* Reaction.swift */,
|
||||
FD8ECF7E2934298100C0D1BB /* SharedConfigDump.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3541,6 +3611,9 @@
|
|||
FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */,
|
||||
7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */,
|
||||
FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */,
|
||||
FD8ECF7C2934293A00C0D1BB /* _011_SharedUtilChanges.swift */,
|
||||
FDF1AD6028FF61110080A701 /* _012_AddClosedGroupInfo.swift */,
|
||||
FD8ECF71292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift */,
|
||||
);
|
||||
path = Migrations;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3728,8 +3801,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
FD3C905E27E410EE00CD579F /* Models */,
|
||||
FD3C906127E411AF00CD579F /* HeaderSpec.swift */,
|
||||
FD3C906327E4122F00CD579F /* RequestSpec.swift */,
|
||||
);
|
||||
path = "Common Networking";
|
||||
sourceTree = "<group>";
|
||||
|
@ -3899,6 +3970,7 @@
|
|||
children = (
|
||||
FD37EA1228AB3F60003AE748 /* Database */,
|
||||
FD83B9B927CF20A5005E1583 /* General */,
|
||||
FD8ECF832934507500C0D1BB /* Networking */,
|
||||
);
|
||||
path = SessionUtilitiesKitTests;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3949,6 +4021,33 @@
|
|||
path = Types;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD8ECF7529340F4800C0D1BB /* LibSessionUtil */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */,
|
||||
FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */,
|
||||
);
|
||||
path = LibSessionUtil;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD8ECF802934385900C0D1BB /* LibSessionUtil */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */,
|
||||
);
|
||||
path = LibSessionUtil;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD8ECF832934507500C0D1BB /* Networking */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD3C906127E411AF00CD579F /* HeaderSpec.swift */,
|
||||
FD3C906327E4122F00CD579F /* RequestSpec.swift */,
|
||||
FD8ECF842934508B00C0D1BB /* BatchResponseSpec.swift */,
|
||||
);
|
||||
path = Networking;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD9004102818ABB000ABAAF6 /* JobRunner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -4050,6 +4149,7 @@
|
|||
FDC4389827BA001800C60D73 /* Open Groups */,
|
||||
FD3C906B27E43C2400CD579F /* Sending & Receiving */,
|
||||
FD3C906827E417B100CD579F /* Utilities */,
|
||||
FD8ECF802934385900C0D1BB /* LibSessionUtil */,
|
||||
);
|
||||
path = SessionMessagingKitTests;
|
||||
sourceTree = "<group>";
|
||||
|
@ -5390,6 +5490,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FD8ECF7B29340FFD00C0D1BB /* SessionUtil.swift in Sources */,
|
||||
7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */,
|
||||
FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */,
|
||||
B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */,
|
||||
|
@ -5474,6 +5575,7 @@
|
|||
FDF0B74F28079E5E004C14C5 /* SendReadReceiptsJob.swift in Sources */,
|
||||
FDF0B7422804EA4F004C14C5 /* _002_SetupStandardJobs.swift in Sources */,
|
||||
B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */,
|
||||
FD8ECF7D2934293A00C0D1BB /* _011_SharedUtilChanges.swift in Sources */,
|
||||
FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */,
|
||||
FD245C5D2850660F00B966DD /* OWSAudioPlayer.m in Sources */,
|
||||
FDF0B7582807F368004C14C5 /* MessageReceiverError.swift in Sources */,
|
||||
|
@ -5520,15 +5622,18 @@
|
|||
FD83B9CE27D17A04005E1583 /* Request.swift in Sources */,
|
||||
C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */,
|
||||
FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */,
|
||||
FD8ECF7F2934298100C0D1BB /* SharedConfigDump.swift in Sources */,
|
||||
C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */,
|
||||
FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */,
|
||||
B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */,
|
||||
FD8ECF72292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift in Sources */,
|
||||
C3DB66C3260ACCE6001EFC55 /* OpenGroupPoller.swift in Sources */,
|
||||
FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */,
|
||||
FD245C5C2850660A00B966DD /* ConfigurationMessage.swift in Sources */,
|
||||
FD5C7301284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift in Sources */,
|
||||
C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */,
|
||||
FD245C642850664F00B966DD /* Threading.swift in Sources */,
|
||||
FDF1AD6128FF61110080A701 /* _012_AddClosedGroupInfo.swift in Sources */,
|
||||
FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */,
|
||||
C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */,
|
||||
FD09799B27FFC82D00936362 /* Quote.swift in Sources */,
|
||||
|
@ -5783,12 +5888,15 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FD8ECF8729346DB500C0D1BB /* RequestSpec.swift in Sources */,
|
||||
FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */,
|
||||
FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */,
|
||||
FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */,
|
||||
FD8ECF8629346DA100C0D1BB /* HeaderSpec.swift in Sources */,
|
||||
FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */,
|
||||
FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
|
||||
FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */,
|
||||
FD8ECF852934508B00C0D1BB /* BatchResponseSpec.swift in Sources */,
|
||||
FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */,
|
||||
FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */,
|
||||
FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */,
|
||||
|
@ -5814,7 +5922,6 @@
|
|||
FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */,
|
||||
FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */,
|
||||
FDC2909A27D71376005DAE71 /* NonceGeneratorSpec.swift in Sources */,
|
||||
FD3C906427E4122F00CD579F /* RequestSpec.swift in Sources */,
|
||||
FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */,
|
||||
FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */,
|
||||
FD859EF827C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift in Sources */,
|
||||
|
@ -5830,8 +5937,8 @@
|
|||
FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */,
|
||||
FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */,
|
||||
FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */,
|
||||
FD8ECF822934387A00C0D1BB /* ConfigUserProfileSpec.swift in Sources */,
|
||||
FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
|
||||
FD3C906227E411AF00CD579F /* HeaderSpec.swift in Sources */,
|
||||
FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */,
|
||||
FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */,
|
||||
FD859EF627C2F52C00510D0C /* MockSign.swift in Sources */,
|
||||
|
@ -7037,7 +7144,7 @@
|
|||
ENABLE_BITCODE = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = "";
|
||||
FRAMEWORK_SEARCH_PATHS = "\"$(SRCROOT)/SessionMessagingKit/LibSessionUtil\"";
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
|
@ -7078,6 +7185,7 @@
|
|||
);
|
||||
"OTHER_SWIFT_FLAGS[arch=*]" = "-D DEBUG";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_INCLUDE_PATHS = "\"$(SRCROOT)/SessionMessagingKit/LibSessionUtil\"";
|
||||
SWIFT_VERSION = 4.0;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
|
@ -7113,7 +7221,7 @@
|
|||
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = "";
|
||||
FRAMEWORK_SEARCH_PATHS = "\"$(SRCROOT)/SessionMessagingKit/LibSessionUtil\"";
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
|
||||
GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES;
|
||||
|
@ -7151,6 +7259,7 @@
|
|||
);
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_INCLUDE_PATHS = "\"$(SRCROOT)/SessionMessagingKit/LibSessionUtil\"";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 4.0;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
|
|
|
@ -27,6 +27,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
// MARK: - Lifecycle
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
SessionUtil.loadState() // TODO: Remove this (move to 'Configuration'? Or call directly as part of 'AppSetup'?)
|
||||
// These should be the first things we do (the startup process can fail without them)
|
||||
SetCurrentAppContext(MainAppContext())
|
||||
verifyDBKeysAvailableBeforeBackgroundLaunch()
|
||||
|
|
|
@ -24,7 +24,8 @@ public enum SNMessagingKit { // Just to make the external API nice
|
|||
[
|
||||
_008_EmojiReacts.self,
|
||||
_009_OpenGroupPermission.self,
|
||||
_010_AddThreadIdToFTS.self
|
||||
_010_AddThreadIdToFTS.self,
|
||||
_011_SharedUtilChanges.self
|
||||
]
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
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 _011_SharedUtilChanges: Migration {
|
||||
static let target: TargetMigrations.Identifier = .messagingKit
|
||||
static let identifier: String = "SharedUtilChanges"
|
||||
static let needsConfigSync: Bool = false
|
||||
static let minExpectedRunDuration: TimeInterval = 0.1
|
||||
|
||||
static func migrate(_ db: Database) throws {
|
||||
try db.create(table: ConfigDump.self) { t in
|
||||
t.column(.variant, .text)
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
t.column(.data, .blob)
|
||||
.notNull()
|
||||
}
|
||||
|
||||
// TODO: Create dumps for current data
|
||||
|
||||
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
internal struct ConfigDump: Codable, Equatable, Hashable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "configDump" }
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
case variant
|
||||
case data
|
||||
}
|
||||
|
||||
enum Variant: String, Codable, DatabaseValueConvertible {
|
||||
case userProfile
|
||||
}
|
||||
|
||||
var id: Variant { variant }
|
||||
|
||||
/// The type of config this dump is for
|
||||
public let variant: Variant
|
||||
|
||||
/// The data for this dump
|
||||
public let data: Data
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtil
|
||||
import SessionUtilitiesKit
|
||||
|
||||
/*internal*/public enum SessionUtil { // TODO: Rename this to be cleaner?
|
||||
/*internal*/public static func loadState() {
|
||||
let storedDump: Data? = Storage.shared
|
||||
.read { db in try ConfigDump.fetchOne(db, id: .userProfile) }?
|
||||
.data
|
||||
var dump: UnsafePointer<CChar>? = nil // TODO: Load from DB/Disk
|
||||
let dumpLen: size_t = 0
|
||||
var conf: UnsafeMutablePointer<config_object>? = nil
|
||||
// var confSetup: UnsafeMutablePointer<UnsafeMutablePointer<config_object>?>? = nil
|
||||
var error: UnsafeMutablePointer<CChar>? = nil
|
||||
// TODO: Will need to manually release any unsafe pointers
|
||||
let result = user_profile_init(&conf, dump, dumpLen, error)
|
||||
|
||||
guard result == 0 else { return } // TODO: Throw error
|
||||
|
||||
// var conf: UnsafeMutablePointer<config_object>? = confSetup?.pointee
|
||||
|
||||
user_profile_set_name(conf, "TestName") // TODO: Confirm success
|
||||
|
||||
let profileUrl: [CChar] = "http://example.org/omg-pic-123.bmp".bytes.map { CChar(bitPattern: $0) }
|
||||
let profileKey: [CChar] = "secretNOTSECRET".bytes.map { CChar(bitPattern: $0) }
|
||||
let profilePic: user_profile_pic = profileUrl.withUnsafeBufferPointer { profileUrlPtr in
|
||||
profileKey.withUnsafeBufferPointer { profileKeyPtr in
|
||||
user_profile_pic(
|
||||
url: profileUrlPtr.baseAddress,
|
||||
key: profileKeyPtr.baseAddress,
|
||||
keylen: profileKey.count
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
user_profile_set_pic(conf, profilePic) // TODO: Confirm success
|
||||
|
||||
if config_needs_push(conf) {
|
||||
print("Needs Push!!!")
|
||||
}
|
||||
|
||||
if config_needs_dump(conf) {
|
||||
print("Needs Dump!!!")
|
||||
}
|
||||
|
||||
var toPush: UnsafeMutablePointer<CChar>? = nil
|
||||
var pushLen: Int = 0
|
||||
let seqNo = config_push(conf, &toPush, &pushLen)
|
||||
|
||||
//var remoteAddr: [CChar] = remote.bytes.map { CChar(bitPattern: $0) }
|
||||
//config_dump(conf, &dump1, &dump1len);
|
||||
|
||||
free(toPush) // TODO: Confirm
|
||||
|
||||
var dumpResult: UnsafeMutablePointer<CChar>? = nil
|
||||
var dumpResultLen: Int = 0
|
||||
|
||||
config_dump(conf, &dumpResult, &dumpResultLen)
|
||||
|
||||
print("RAWR")
|
||||
let str = String(cString: dumpResult!)
|
||||
let stryBytes = str.bytes
|
||||
let hexStr = stryBytes.toHexString()
|
||||
let data = Data(bytes: dumpResult!, count: dumpResultLen)
|
||||
// dumpResult.
|
||||
// Storage.shared.write { db in
|
||||
// try ConfigDump(variant: .userProfile, data: <#T##Data#>)
|
||||
// .save(db)
|
||||
// }
|
||||
//
|
||||
print("RAWR2")
|
||||
|
||||
//String(cString: dumpResult!)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AvailableLibraries</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>libsession-util.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64_x86_64-simulator</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>libsession-util.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
<string>x86_64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
<key>SupportedPlatformVariant</key>
|
||||
<string>simulator</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XFWK</string>
|
||||
<key>XCFrameworkFormatVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,9 @@
|
|||
module SessionUtil {
|
||||
module capi {
|
||||
header "session/config/error.h"
|
||||
header "session/config/user_profile.h"
|
||||
header "session/config/base.h"
|
||||
header "session/xed25519.h"
|
||||
export *
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
#pragma once
|
||||
#include <oxenc/bt_value.h>
|
||||
|
||||
#include <cassert>
|
||||
#ifndef NDEBUG
|
||||
#include <algorithm>
|
||||
#endif
|
||||
|
||||
namespace session::bt {
|
||||
|
||||
using oxenc::bt_dict;
|
||||
using oxenc::bt_list;
|
||||
|
||||
/// Merges two bt dicts together: the returned dict includes all keys in a or b. Keys in *both*
|
||||
/// dicts get their value from `a`, otherwise the value is that of the dict that contains the key.
|
||||
bt_dict merge(const bt_dict& a, const bt_dict& b);
|
||||
|
||||
/// Merges two ordered bt_lists together using a predicate to determine order. The input lists must
|
||||
/// be sorted to begin with. `cmp` must be callable with a pair of `const bt_value&` arguments and
|
||||
/// must return true if the first argument should be considered less than the second argument. By
|
||||
/// default this skips elements from b that compare equal to a value of a, but you can include all
|
||||
/// the duplicates by specifying the `duplicates` parameter as true.
|
||||
template <typename Compare>
|
||||
bt_list merge_sorted(const bt_list& a, const bt_list& b, Compare cmp, bool duplicates = false) {
|
||||
bt_list result;
|
||||
auto it_a = a.begin();
|
||||
auto it_b = b.begin();
|
||||
|
||||
assert(std::is_sorted(it_a, a.end(), cmp));
|
||||
assert(std::is_sorted(it_b, b.end(), cmp));
|
||||
|
||||
if (duplicates) {
|
||||
while (it_a != a.end() && it_b != b.end()) {
|
||||
if (!cmp(*it_a, *it_b)) // *b <= *a
|
||||
result.push_back(*it_b++);
|
||||
else // *a < *b
|
||||
result.push_back(*it_a++);
|
||||
}
|
||||
} else {
|
||||
while (it_a != a.end() && it_b != b.end()) {
|
||||
if (cmp(*it_b, *it_a)) // *b < *a
|
||||
result.push_back(*it_b++);
|
||||
else if (cmp(*it_a, *it_b)) // *a < *b
|
||||
result.push_back(*it_a++);
|
||||
else // *a == *b
|
||||
++it_b; // skip it
|
||||
}
|
||||
}
|
||||
|
||||
if (it_a != a.end())
|
||||
result.insert(result.end(), it_a, a.end());
|
||||
else if (it_b != b.end())
|
||||
result.insert(result.end(), it_b, b.end());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace session::bt
|
|
@ -0,0 +1,369 @@
|
|||
#pragma once
|
||||
|
||||
#include <oxenc/bt_serialize.h>
|
||||
#include <oxenc/bt_value.h>
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace session::config {
|
||||
|
||||
inline constexpr int MAX_MESSAGE_SIZE = 76800; // 76.8kB = Storage server's limit
|
||||
|
||||
// Application data data types:
|
||||
using scalar = std::variant<int64_t, std::string>;
|
||||
|
||||
using set = std::set<scalar>;
|
||||
struct dict_value;
|
||||
using dict = std::map<std::string, dict_value>;
|
||||
using dict_variant = std::variant<dict, set, scalar>;
|
||||
struct dict_value : dict_variant {
|
||||
using dict_variant::dict_variant;
|
||||
using dict_variant::operator=;
|
||||
};
|
||||
|
||||
// Helpers for gcc-10 and earlier which don't like visiting a std::variant subtype:
|
||||
constexpr inline dict_variant& unwrap(dict_value& v) {
|
||||
return static_cast<dict_variant&>(v);
|
||||
}
|
||||
constexpr inline const dict_variant& unwrap(const dict_value& v) {
|
||||
return static_cast<const dict_variant&>(v);
|
||||
}
|
||||
|
||||
using seqno_t = std::int64_t;
|
||||
using hash_t = std::array<unsigned char, 32>;
|
||||
using seqno_hash_t = std::pair<seqno_t, hash_t>;
|
||||
|
||||
using ustring = std::basic_string<unsigned char>;
|
||||
using ustring_view = std::basic_string_view<unsigned char>;
|
||||
|
||||
class MutableConfigMessage;
|
||||
|
||||
/// Base type for all errors that can happen during config parsing
|
||||
struct config_error : std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
/// Type thrown for bad signatures (bad or missing signature).
|
||||
struct signature_error : config_error {
|
||||
using config_error::config_error;
|
||||
};
|
||||
/// Type thrown for a missing signature when a signature is required.
|
||||
struct missing_signature : signature_error {
|
||||
using signature_error::signature_error;
|
||||
};
|
||||
/// Type thrown for an unparseable config (e.g. keys with invalid types, or keys before "#" or after
|
||||
/// "~").
|
||||
struct config_parse_error : config_error {
|
||||
using config_error::config_error;
|
||||
};
|
||||
|
||||
/// Class for a parsed, read-only config message; also serves as the base class of a
|
||||
/// MutableConfigMessage which allows setting values.
|
||||
class ConfigMessage {
|
||||
public:
|
||||
using lagged_diffs_t = std::map<seqno_hash_t, oxenc::bt_dict>;
|
||||
|
||||
#ifndef SESSION_TESTING_EXPOSE_INTERNALS
|
||||
protected:
|
||||
#endif
|
||||
dict data_;
|
||||
|
||||
// diff data for *this* message, parsed during construction. Subclasses may use this for
|
||||
// managing their own diff in the `diff()` method.
|
||||
oxenc::bt_dict diff_;
|
||||
|
||||
// diffs of previous messages that are included in this message.
|
||||
lagged_diffs_t lagged_diffs_;
|
||||
|
||||
// Unknown top-level config keys which we preserve even though we don't understand what they
|
||||
// mean.
|
||||
oxenc::bt_dict unknown_;
|
||||
|
||||
/// Seqno and hash of the message; we calculate this when loading. Subclasses put the hash here
|
||||
/// (so that they can return a reference to it).
|
||||
seqno_hash_t seqno_hash_{0, {0}};
|
||||
|
||||
bool verified_signature_ = false;
|
||||
|
||||
bool merged_ = false;
|
||||
|
||||
public:
|
||||
constexpr static int DEFAULT_DIFF_LAGS = 5;
|
||||
|
||||
/// Verification function: this is passed the data that should have been signed and the 64-byte
|
||||
/// signature. Should return true to accept the signature, false to reject it and skip the
|
||||
/// message. It can also throw to abort message construction (that is: returning false skips
|
||||
/// the message when loading multiple messages, but can still continue with other messages;
|
||||
/// throwing aborts the entire construction).
|
||||
using verify_callable = std::function<bool(ustring_view data, ustring_view signature)>;
|
||||
|
||||
/// Signing function: this is passed the data to be signed and returns the 64-byte signature.
|
||||
using sign_callable = std::function<std::string(ustring_view data)>;
|
||||
|
||||
ConfigMessage();
|
||||
ConfigMessage(const ConfigMessage&) = default;
|
||||
ConfigMessage& operator=(const ConfigMessage&) = default;
|
||||
ConfigMessage(ConfigMessage&&) = default;
|
||||
ConfigMessage& operator=(ConfigMessage&&) = default;
|
||||
|
||||
virtual ~ConfigMessage() = default;
|
||||
|
||||
/// Initializes a config message by parsing a serialized message. Throws on any error. See the
|
||||
/// vector version below for argument descriptions.
|
||||
explicit ConfigMessage(
|
||||
std::string_view serialized,
|
||||
verify_callable verifier = nullptr,
|
||||
sign_callable signer = nullptr,
|
||||
int lag = DEFAULT_DIFF_LAGS,
|
||||
bool signature_optional = false);
|
||||
|
||||
/// Constructs a new ConfigMessage by loading and potentially merging multiple serialized
|
||||
/// ConfigMessages together, according to the config conflict resolution rules. The result
|
||||
/// of this call can either be one of the config messages directly (if one is found that
|
||||
/// includes all the others), or can be a new config message that merges multiple configs
|
||||
/// together. You can check `.merged()` to see which happened.
|
||||
///
|
||||
/// This constructor always requires at least one valid config from the given inputs; if all are
|
||||
/// empty,
|
||||
///
|
||||
/// verifier - a signature verification function. If provided and not nullptr this will be
|
||||
/// called to verify each signature in the provided messages: any that are missing a signature
|
||||
/// or for which the verifier returns false will be dropped from consideration for merging. If
|
||||
/// *all* messages fail verification an exception is raised.
|
||||
///
|
||||
/// signer - a signature generation function. This is not used directly by the ConfigMessage,
|
||||
/// but providing it will allow it to be passed automatically to any MutableConfigMessage
|
||||
/// derived from this ConfigMessage.
|
||||
///
|
||||
/// lag - the lag setting controlling the config merging rules. Any config message with lagged
|
||||
/// diffs that exceeding this lag value will have those early lagged diffs dropping during
|
||||
/// loading.
|
||||
///
|
||||
/// signature_optional - if true then accept a message with no signature even when a verifier is
|
||||
/// set, thus allowing unsigned messages (though messages with an invalid signature are still
|
||||
/// not allowed). This option is ignored when verifier is not set.
|
||||
///
|
||||
/// error_callback - if set then any config message parsing error will be passed to this
|
||||
/// function for handling: the callback typically warns and, if the overall construction should
|
||||
/// abort, rethrows the error. If this function is omitted then the default skips (without
|
||||
/// failing) individual parse errors and only aborts construction if *all* messages fail to
|
||||
/// parse. A simple handler such as `[](const auto& e) { throw e; }` can be used to make any
|
||||
/// parse error of any message fatal.
|
||||
explicit ConfigMessage(
|
||||
const std::vector<std::string_view>& configs,
|
||||
verify_callable verifier = nullptr,
|
||||
sign_callable signer = nullptr,
|
||||
int lag = DEFAULT_DIFF_LAGS,
|
||||
bool signature_optional = false,
|
||||
std::function<void(const config_error&)> error_handler = nullptr);
|
||||
|
||||
/// Returns a read-only reference to the contained data. (To get a mutable config object use
|
||||
/// MutableConfigMessage).
|
||||
const dict& data() const { return data_; }
|
||||
|
||||
/// The verify function; if loading a message with a signature and this is set then it will
|
||||
/// be called to verify the signature of the message. Takes a pointer to the signing data,
|
||||
/// the data length, and a pointer to the 64-byte signature.
|
||||
verify_callable verifier;
|
||||
|
||||
/// The signing function; this is not directly used by the non-mutable base class, but will be
|
||||
/// propagated to mutable config messages that are derived e.g. by calling `.increment()`. This
|
||||
/// is called when serializing a config message to add a signature. If it is nullptr then no
|
||||
/// signature is added to the serialized data.
|
||||
sign_callable signer;
|
||||
|
||||
/// How many lagged config diffs that should be carried forward to resolve conflicts,
|
||||
/// including this message. If 0 then config messages won't have any diffs and will not be
|
||||
/// mergeable.
|
||||
int lag = DEFAULT_DIFF_LAGS;
|
||||
|
||||
/// The diff structure for changes in *this* config message. Subclasses that need to override
|
||||
/// should populate into `diff_` and return a reference to it (internal code assumes `diff_` is
|
||||
/// correct immediately after a call to this).
|
||||
virtual const oxenc::bt_dict& diff();
|
||||
|
||||
/// Returns the seqno of this message
|
||||
const seqno_t& seqno() const { return seqno_hash_.first; }
|
||||
|
||||
/// Calculates the hash of the current message. For a ConfigMessage this is calculated when the
|
||||
/// message is first loaded; for a MutableConfigMessage this serializes the current value to
|
||||
/// properly compute the current hash. Subclasses must ensure that seqno_hash_.second is set to
|
||||
/// the correct value when this is called (and typically return a reference to it).
|
||||
virtual const hash_t& hash() { return seqno_hash_.second; }
|
||||
|
||||
/// After loading multiple config files this flag indicates whether or not we had to produce a
|
||||
/// new, merged configuration message (true) or did not need to merge (false). (For config
|
||||
/// messages that were not loaded from serialized data this is always true).
|
||||
bool merged() const { return merged_; }
|
||||
|
||||
/// Returns true if this message contained a valid, verified signature when it was parsed.
|
||||
/// Returns false otherwise (e.g. not loaded from verification at all; loaded without a
|
||||
/// verification function; or had no signature and a signature wasn't required).
|
||||
bool verified_signature() const { return verified_signature_; }
|
||||
|
||||
/// Constructs a new MutableConfigMessage from this config message with an incremented seqno.
|
||||
/// The new config message's diff will reflect changes made after this construction.
|
||||
virtual MutableConfigMessage increment() const;
|
||||
|
||||
/// Serializes this config's data. Note that if the ConfigMessage was constructed from signed,
|
||||
/// serialized input, this will only produce an exact copy of the original serialized input if
|
||||
/// it uses the identical, deterministic signing function used to construct the original.
|
||||
///
|
||||
/// The optional `enable_signing` argument can be specified as false to disable signing (this is
|
||||
/// typically for a local serialization value that isn't being pushed to the server). Note that
|
||||
/// signing is always disabled if there is no signing callback set, regardless of the value of
|
||||
/// this argument.
|
||||
virtual std::string serialize(bool enable_signing = true);
|
||||
|
||||
protected:
|
||||
std::string serialize_impl(const oxenc::bt_dict& diff, bool enable_signing = true);
|
||||
};
|
||||
|
||||
// Constructor tag
|
||||
struct increment_seqno_t {};
|
||||
inline constexpr increment_seqno_t increment_seqno{};
|
||||
|
||||
class MutableConfigMessage : public ConfigMessage {
|
||||
protected:
|
||||
dict orig_data_{data_};
|
||||
|
||||
friend class ConfigMessage;
|
||||
|
||||
public:
|
||||
MutableConfigMessage(const MutableConfigMessage&) = default;
|
||||
MutableConfigMessage& operator=(const MutableConfigMessage&) = default;
|
||||
MutableConfigMessage(MutableConfigMessage&&) = default;
|
||||
MutableConfigMessage& operator=(MutableConfigMessage&&) = default;
|
||||
|
||||
/// Constructs a new, empty config message. Takes various fields to pre-fill the various
|
||||
/// properties during construction (these are for convenience and equivalent to setting them via
|
||||
/// properties/methods after construction).
|
||||
///
|
||||
/// seqno -- the message's seqno, default 0
|
||||
/// lags -- number of lags to keep (when deriving messages, e.g. via increment())
|
||||
/// signer -- if specified and not nullptr then this message will be signed when serialized
|
||||
/// using the given signing function. If omitted no signing takes place.
|
||||
explicit MutableConfigMessage(
|
||||
seqno_t seqno = 0, int lag = DEFAULT_DIFF_LAGS, sign_callable signer = nullptr) {
|
||||
this->lag = lag;
|
||||
this->seqno(seqno);
|
||||
this->signer = signer;
|
||||
}
|
||||
|
||||
/// Wraps the ConfigMessage constructor with the same arguments but always produces a
|
||||
/// MutableConfigMessage. In particular this means that if the base constructor performed a
|
||||
/// merge (and thus incremented seqno) then the config stays as is, but contained in a Mutable
|
||||
/// message that can be changed. If it did *not* merge (i.e. the highest seqno message it found
|
||||
/// did not conflict with any other messages) then this construction is equivalent to doing a
|
||||
/// base load followed by a .increment() call. In other words: this constructor *always* gives
|
||||
/// you an incremented seqno value from the highest valid input config message.
|
||||
///
|
||||
/// This is almost equivalent to ConfigMessage{args...}.increment(), except that this
|
||||
/// constructor only increments seqno once while the indirect version would increment twice in
|
||||
/// the case of a required merge conflict resolution.
|
||||
explicit MutableConfigMessage(
|
||||
const std::vector<std::string_view>& configs,
|
||||
verify_callable verifier = nullptr,
|
||||
sign_callable signer = nullptr,
|
||||
int lag = DEFAULT_DIFF_LAGS,
|
||||
bool signature_optional = false,
|
||||
std::function<void(const config_error&)> error_handler = nullptr);
|
||||
|
||||
/// Wrapper around the above that takes a single string view to load a single message, doesn't
|
||||
/// take an error handler and instead always throws on parse errors (the above also throws for
|
||||
/// an erroneous single message, but with a less specific "no valid config messages" error).
|
||||
explicit MutableConfigMessage(
|
||||
std::string_view config,
|
||||
verify_callable verifier = nullptr,
|
||||
sign_callable signer = nullptr,
|
||||
int lag = DEFAULT_DIFF_LAGS,
|
||||
bool signature_optional = false);
|
||||
|
||||
/// Does the same as the base incrementing, but also records any diff info from the current
|
||||
/// MutableConfigMessage. *this* object gets pruned and signed as part of this call. If the
|
||||
/// sign argument is omitted/nullptr then the current object's `sign` callback gets copied into
|
||||
/// the new object. After this call you typically do not want to further modify *this (because
|
||||
/// any modifications will change the hash, making *this no longer a parent of the new object).
|
||||
MutableConfigMessage increment() const override;
|
||||
|
||||
/// Constructor that does the same thing as the `m.increment()` factory method. The second
|
||||
/// value should be the literal `increment_seqno` value (to select this constructor).
|
||||
explicit MutableConfigMessage(const ConfigMessage& m, increment_seqno_t);
|
||||
|
||||
using ConfigMessage::data;
|
||||
/// Returns a mutable reference to the underlying config data.
|
||||
dict& data() { return data_; }
|
||||
|
||||
using ConfigMessage::seqno;
|
||||
|
||||
/// Sets the seqno of the message to a specific value. You usually want to use `.increment()`
|
||||
/// from an existing config message rather than manually adjusting the seqno.
|
||||
void seqno(seqno_t new_seqno) { seqno_hash_.first = new_seqno; }
|
||||
|
||||
/// Returns the current diff for this data relative to its original data. The data is pruned
|
||||
/// implicitly by this call.
|
||||
const oxenc::bt_dict& diff() override;
|
||||
|
||||
/// Prunes empty dicts/sets from data. This is called automatically when serializing or
|
||||
/// calculating a diff. Returns true if the data was actually changed, false if nothing needed
|
||||
/// pruning.
|
||||
bool prune();
|
||||
|
||||
/// Calculates the hash of the current message. Can optionally be given the already-serialized
|
||||
/// value, if available; if empty/omitted, `serialize()` will be called to compute it.
|
||||
const hash_t& hash() override;
|
||||
|
||||
protected:
|
||||
const hash_t& hash(std::string_view serialized);
|
||||
void increment_impl();
|
||||
};
|
||||
|
||||
/// Encrypts a config message using XChaCha20-Poly1305, using a blake2b keyed hash of the message
|
||||
/// for the nonce (rather than pure random) so that different clients will encrypt the same data to
|
||||
/// the same encrypted value (thus allowing for server-side deduplication of identical messages).
|
||||
///
|
||||
/// `key_base` must be 32 bytes. This value is a fixed key that all clients that might receive this
|
||||
/// message can calculate independently (for instance a value derived from a secret key, or a shared
|
||||
/// random key). This key will be hashed with the message size and domain suffix (see below) to
|
||||
/// determine the actual encryption key.
|
||||
///
|
||||
/// `domain` is a short string (1-24 chars) used for the keyed hash. Typically this is the type of
|
||||
/// config, e.g. "closed-group" or "contacts". The full key will be
|
||||
/// "session-config-encrypted-message-[domain]". This value is also used for the encrypted key (see
|
||||
/// above).
|
||||
///
|
||||
/// The returned result will consist of encrypted data with authentication tag and appended nonce,
|
||||
/// suitable for being passed to decrypt() to authenticate and decrypt.
|
||||
///
|
||||
/// Throw std::invalid_argument on bad input (i.e. from invalid key_base or domain).
|
||||
ustring encrypt(ustring_view message, ustring_view key_base, std::string_view domain);
|
||||
|
||||
/// Same as above but works with strings/string_views instead of ustring/ustring_view
|
||||
std::string encrypt(std::string_view message, std::string_view key_base, std::string_view domain);
|
||||
|
||||
/// Thrown if decrypt() fails.
|
||||
struct decrypt_error : std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
/// Takes a value produced by `encrypt()` and decrypts it. `key_base` and `domain` must be the same
|
||||
/// given to encrypt or else decryption fails. Upon decryption failure a std::
|
||||
ustring decrypt(ustring_view ciphertext, ustring_view key_base, std::string_view domain);
|
||||
|
||||
/// Same as above but using std::string/string_view
|
||||
std::string decrypt(
|
||||
std::string_view ciphertext, std::string_view key_base, std::string_view domain);
|
||||
|
||||
} // namespace session::config
|
||||
|
||||
namespace oxenc::detail {
|
||||
|
||||
template <>
|
||||
struct bt_serialize<session::config::dict_value> : bt_serialize<session::config::dict_variant> {};
|
||||
|
||||
} // namespace oxenc::detail
|
|
@ -0,0 +1,85 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined(_WIN32) || defined(WIN32)
|
||||
#define LIBSESSION_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
#define LIBSESSION_EXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
#define LIBSESSION_C_API extern "C" LIBSESSION_EXPORT
|
||||
|
||||
typedef int64_t seqno_t;
|
||||
|
||||
// Config object base type: this type holds the internal object and is initialized by the various
|
||||
// config-dependent settings (e.g. config_user_profile_init) then passed to the various functions.
|
||||
typedef struct config_object {
|
||||
// Internal opaque object pointer; calling code should leave this alone.
|
||||
void* internals;
|
||||
// When an error occurs in the C API this string will be set to the specific error message. May
|
||||
// be NULL.
|
||||
const char* last_error;
|
||||
} config_object;
|
||||
|
||||
// Common functions callable on any config instance:
|
||||
|
||||
/// Frees a config object created with one of the config-dependent ..._init functions (e.g.
|
||||
/// user_profile_init).
|
||||
void config_free(config_object* conf);
|
||||
|
||||
/// Returns the numeric namespace in which config messages of this type should be stored.
|
||||
int16_t config_storage_namespace(const config_object* conf);
|
||||
|
||||
/// Merges the config object with one or more remotely obtained config strings. After this call the
|
||||
/// config object may be unchanged, complete replaced, or updated and needing a push, depending on
|
||||
/// the messages that are merged; the caller should check config_needs_push().
|
||||
///
|
||||
/// `configs` is an array of pointers to the start of the strings; `lengths` is an array of string
|
||||
/// lengths; `count` is the length of those two arrays.
|
||||
void config_merge(config_object* conf, const char** configs, const size_t* lengths, size_t count);
|
||||
|
||||
/// Returns true if this config object contains updated data that has not yet been confirmed stored
|
||||
/// on the server.
|
||||
bool config_needs_push(const config_object* conf);
|
||||
|
||||
/// Obtains the configuration data that needs to be pushed to the server. A new buffer of the
|
||||
/// appropriate size is malloc'd and set to `out` The output is written to a new malloc'ed buffer of
|
||||
/// the appropriate size; the buffer and the output length are set in the `out` and `outlen`
|
||||
/// parameters. Note that this is binary data, *not* a null-terminated C string.
|
||||
///
|
||||
/// Generally this call should be guarded by a call to `config_needs_push`, however it can be used
|
||||
/// to re-obtain the current serialized config even if no push is needed (for example, if the client
|
||||
/// wants to re-submit it after a network error).
|
||||
///
|
||||
/// NB: The returned buffer belongs to the caller: that is, the caller *MUST* free() it when done
|
||||
/// with it.
|
||||
seqno_t config_push(config_object* conf, char** out, size_t* outlen);
|
||||
|
||||
/// Reports that data obtained from `config_push` has been successfully stored on the server. The
|
||||
/// seqno value is the one returned by the config_push call that yielded the config data.
|
||||
void config_confirm_pushed(config_object* conf, seqno_t seqno);
|
||||
|
||||
/// Returns a binary dump of the current state of the config object. This dump can be used to
|
||||
/// resurrect the object at a later point (e.g. after a restart). Allocates a new buffer and sets
|
||||
/// it in `out` and the length in `outlen`. Note that this is binary data, *not* a null-terminated
|
||||
/// C string.
|
||||
///
|
||||
/// NB: It is the caller's responsibility to `free()` the buffer when done with it.
|
||||
///
|
||||
/// Immediately after this is called `config_needs_dump` will start returning true (until the
|
||||
/// configuration is next modified).
|
||||
void config_dump(config_object* conf, char** out, size_t* outlen);
|
||||
|
||||
/// Returns true if something has changed since the last call to `dump()` that requires calling
|
||||
/// and saving the `config_dump()` data again.
|
||||
bool config_needs_dump(const config_object* conf);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
|
@ -0,0 +1,503 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <session/config.hpp>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
|
||||
#include "base.h"
|
||||
#include "namespaces.hpp"
|
||||
|
||||
namespace session::config {
|
||||
|
||||
template <typename T, typename... U>
|
||||
static constexpr bool is_one_of = (std::is_same_v<T, U> || ...);
|
||||
|
||||
/// True for a dict_value direct subtype, but not scalar sub-subtypes.
|
||||
template <typename T>
|
||||
static constexpr bool is_dict_subtype = is_one_of<T, config::scalar, config::set, config::dict>;
|
||||
|
||||
/// True for a dict_value or any of the types containable within a dict value
|
||||
template <typename T>
|
||||
static constexpr bool is_dict_value =
|
||||
is_dict_subtype<T> || is_one_of<T, dict_value, int64_t, std::string>;
|
||||
|
||||
// Levels for the logging callback
|
||||
enum class LogLevel { debug, info, warning, error };
|
||||
|
||||
/// Our current config state
|
||||
enum class ConfigState : int {
|
||||
/// Clean means the config is confirmed stored on the server and we haven't changed anything.
|
||||
Clean = 0,
|
||||
|
||||
/// Dirty means we have local changes, and the changes haven't been serialized yet for sending
|
||||
/// to the server.
|
||||
Dirty = 1,
|
||||
|
||||
/// Waiting is halfway in-between clean and dirty: the caller has serialized the data, but
|
||||
/// hasn't yet reported back that the data has been stored, *and* we haven't made any changes
|
||||
/// since the data was serialize.
|
||||
Waiting = 2,
|
||||
};
|
||||
|
||||
/// Base config type for client-side configs containing common functionality needed by all config
|
||||
/// sub-types.
|
||||
class ConfigBase {
|
||||
private:
|
||||
// The object (either base config message or MutableConfigMessage) that stores the current
|
||||
// config message. Subclasses do not directly access this: instead they call `dirty()` if they
|
||||
// intend to make changes, or the `set_config_field` wrapper.
|
||||
std::unique_ptr<ConfigMessage> _config;
|
||||
|
||||
// Tracks our current state
|
||||
ConfigState _state = ConfigState::Clean;
|
||||
|
||||
protected:
|
||||
// Constructs an empty base config with no config settings and seqno set to 0.
|
||||
ConfigBase();
|
||||
|
||||
// Constructs a base config by loading the data from a dump as produced by `dump()`.
|
||||
explicit ConfigBase(std::string_view dump);
|
||||
|
||||
// Tracks whether we need to dump again; most mutating methods should set this to true (unless
|
||||
// calling set_state, which sets to to true implicitly).
|
||||
bool _needs_dump = false;
|
||||
|
||||
// Sets the current state; this also sets _needs_dump to true.
|
||||
void set_state(ConfigState s) {
|
||||
_state = s;
|
||||
_needs_dump = true;
|
||||
}
|
||||
|
||||
// If set then we log things by calling this callback
|
||||
std::function<void(LogLevel lvl, std::string msg)> logger;
|
||||
|
||||
// Invokes the above if set, does nothing if there is no logger.
|
||||
void log(LogLevel lvl, std::string msg) {
|
||||
if (logger)
|
||||
logger(lvl, std::move(msg));
|
||||
}
|
||||
|
||||
// Returns a reference to the current MutableConfigMessage. If the current message is not
|
||||
// already dirty (i.e. Clean or Waiting) then calling this increments the seqno counter.
|
||||
MutableConfigMessage& dirty();
|
||||
|
||||
// class for proxying subfield access; this class should never be stored but only used
|
||||
// ephemerally (most of its methods are rvalue-qualified). This lets constructs such as
|
||||
// foo["abc"]["def"]["ghi"] = 12;
|
||||
// work, auto-vivifying (or trampling, if not a dict) subdicts to reach the target. It also
|
||||
// allows non-vivifying value retrieval via .string(), .integer(), etc. methods.
|
||||
class DictFieldProxy {
|
||||
private:
|
||||
ConfigBase& _conf;
|
||||
std::vector<std::string> _inter_keys;
|
||||
std::string _last_key;
|
||||
|
||||
// See if we can find the key without needing to create anything, so that we can attempt to
|
||||
// access values without mutating anything (which allows, among other things, for assigning
|
||||
// of the existing value to not dirty anything). Returns nullptr if the value or something
|
||||
// along its path would need to be created, or has the wrong type; otherwise a const pointer
|
||||
// to the value. The templated type, if provided, can be one of the types a dict_value can
|
||||
// hold to also check that the returned value has a particular type; if omitted you get back
|
||||
// the dict_value pointer itself.
|
||||
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
|
||||
const T* get_clean() const {
|
||||
const config::dict* data = &_conf._config->data();
|
||||
// All but the last need to be dicts:
|
||||
for (const auto& key : _inter_keys) {
|
||||
auto it = data->find(key);
|
||||
data = it != data->end() ? std::get_if<config::dict>(&it->second) : nullptr;
|
||||
if (!data)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const dict_value* val;
|
||||
// The last can be any value type:
|
||||
if (auto it = data->find(_last_key); it != data->end())
|
||||
val = &it->second;
|
||||
else
|
||||
return nullptr;
|
||||
|
||||
if constexpr (std::is_same_v<T, dict_value>)
|
||||
return val;
|
||||
else if constexpr (is_dict_subtype<T>) {
|
||||
if (auto* v = std::get_if<T>(val))
|
||||
return v;
|
||||
} else { // int64 or std::string, i.e. the config::scalar sub-types.
|
||||
if (auto* scalar = std::get_if<config::scalar>(val))
|
||||
return std::get_if<T>(scalar);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Returns a lvalue reference to the value, stomping its way through the dict as it goes to
|
||||
// create subdicts as needed to reach the target value. If given a template type then we
|
||||
// also cast the final dict_value variant into the given type (and replace if with a
|
||||
// default-constructed value if it has the wrong type) then return a reference to that.
|
||||
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
|
||||
T& get_dirty() {
|
||||
config::dict* data = &_conf.dirty().data();
|
||||
for (const auto& key : _inter_keys) {
|
||||
auto& val = (*data)[key];
|
||||
data = std::get_if<config::dict>(&val);
|
||||
if (!data)
|
||||
data = &val.emplace<config::dict>();
|
||||
}
|
||||
auto& val = (*data)[_last_key];
|
||||
|
||||
if constexpr (std::is_same_v<T, dict_value>)
|
||||
return val;
|
||||
else if constexpr (is_dict_subtype<T>) {
|
||||
if (auto* v = std::get_if<T>(&val))
|
||||
return *v;
|
||||
return val.emplace<T>();
|
||||
} else { // int64 or std::string, i.e. the config::scalar sub-types.
|
||||
if (auto* scalar = std::get_if<config::scalar>(&val)) {
|
||||
if (auto* v = std::get_if<T>(scalar))
|
||||
return *v;
|
||||
return scalar->emplace<T>();
|
||||
}
|
||||
return val.emplace<scalar>().emplace<T>();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void assign_if_changed(T value) {
|
||||
// Try to avoiding dirtying the config if this assignment isn't changing anything
|
||||
if (!_conf.is_dirty())
|
||||
if (auto current = get_clean<T>(); current && *current == value)
|
||||
return;
|
||||
|
||||
get_dirty<T>() = std::move(value);
|
||||
}
|
||||
|
||||
void insert_if_missing(config::scalar&& value) {
|
||||
if (!_conf.is_dirty())
|
||||
if (auto current = get_clean<config::set>(); current && current->count(value))
|
||||
return;
|
||||
|
||||
get_dirty<config::set>().insert(std::move(value));
|
||||
}
|
||||
|
||||
void set_erase_impl(const config::scalar& value) {
|
||||
if (!_conf.is_dirty())
|
||||
if (auto current = get_clean<config::set>(); current && !current->count(value))
|
||||
return;
|
||||
|
||||
config::dict* data = &_conf.dirty().data();
|
||||
|
||||
for (const auto& key : _inter_keys) {
|
||||
auto it = data->find(key);
|
||||
data = it != data->end() ? std::get_if<config::dict>(&it->second) : nullptr;
|
||||
if (!data)
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = data->find(_last_key);
|
||||
if (it == data->end())
|
||||
return;
|
||||
auto& val = it->second;
|
||||
if (auto* current = std::get_if<config::set>(&val))
|
||||
current->erase(value);
|
||||
else
|
||||
val.emplace<config::set>();
|
||||
}
|
||||
|
||||
public:
|
||||
DictFieldProxy(ConfigBase& b, std::string key) : _conf{b}, _last_key{std::move(key)} {}
|
||||
|
||||
/// Descends into a dict, returning a copied proxy object for the path to the requested
|
||||
/// field. Nothing is created by doing this unless you actually assign to a value.
|
||||
DictFieldProxy operator[](std::string subkey) const& {
|
||||
DictFieldProxy subfield{_conf, std::move(subkey)};
|
||||
subfield._inter_keys.reserve(_inter_keys.size() + 1);
|
||||
subfield._inter_keys.insert(
|
||||
subfield._inter_keys.end(), _inter_keys.begin(), _inter_keys.end());
|
||||
subfield._inter_keys.push_back(_last_key);
|
||||
return subfield;
|
||||
}
|
||||
|
||||
// Same as above, but when called on an rvalue reference we just mutate the current proxy to
|
||||
// the new dict path.
|
||||
DictFieldProxy&& operator[](std::string subkey) && {
|
||||
_inter_keys.push_back(std::move(_last_key));
|
||||
_last_key = std::move(subkey);
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
/// Returns a const pointer to the string if one exists at the given location, nullptr
|
||||
/// otherwise.
|
||||
const std::string* string() const { return get_clean<std::string>(); }
|
||||
|
||||
/// returns the value as a string_view or a fallback if the value doesn't exist (or isn't a
|
||||
/// string). The returned view is directly into the value (or fallback) and so mustn't be
|
||||
/// used beyond the validity of either.
|
||||
std::string_view string_view_or(std::string_view fallback) const {
|
||||
if (auto* s = string())
|
||||
return {*s};
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/// Returns a copy of the value as a string, if it exists and is a string; returns
|
||||
/// `fallback` otherwise.
|
||||
std::string string_or(std::string fallback) const {
|
||||
if (auto* s = string())
|
||||
return *s;
|
||||
return std::move(fallback);
|
||||
}
|
||||
|
||||
/// Returns a const pointer to the integer if one exists at the given location, nullptr
|
||||
/// otherwise.
|
||||
const int64_t* integer() const { return get_clean<int64_t>(); }
|
||||
|
||||
/// Returns the value as an integer or a fallback if the value doesn't exist (or isn't an
|
||||
/// integer).
|
||||
int64_t integer_or(int64_t fallback) const {
|
||||
if (auto* i = integer())
|
||||
return *i;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/// Returns a const pointer to the set if one exists at the given location, nullptr
|
||||
/// otherwise.
|
||||
const config::set* set() const { return get_clean<config::set>(); }
|
||||
/// Returns a const pointer to the dict if one exists at the given location, nullptr
|
||||
/// otherwise. (You typically don't need to use this but can rather just use [] to descend
|
||||
/// into the dict).
|
||||
const config::dict* dict() const { return get_clean<config::dict>(); }
|
||||
|
||||
/// Replaces the current value with the given string. This also auto-vivifies any
|
||||
/// intermediate dicts needed to reach the given key, including replacing non-dict values if
|
||||
/// they currently exist along the path.
|
||||
void operator=(std::string value) { assign_if_changed(std::move(value)); }
|
||||
/// Same as above, but takes a string_view for convenience.
|
||||
void operator=(std::string_view value) { *this = std::string{value}; }
|
||||
/// Replace the current value with the given integer. See above.
|
||||
void operator=(int64_t value) { assign_if_changed(value); }
|
||||
/// Replace the current value with the given set. See above.
|
||||
void operator=(config::set value) { assign_if_changed(std::move(value)); }
|
||||
/// Replace the current value with the given dict. See above. This often isn't needed
|
||||
/// because of how other assignment operations work.
|
||||
void operator=(config::dict value) { assign_if_changed(std::move(value)); }
|
||||
|
||||
/// Returns true if there is a value at the current key. If a template type T is given, it
|
||||
/// only returns true if that value also is a `T`.
|
||||
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
|
||||
bool exists() const {
|
||||
return get_clean<T>() != nullptr;
|
||||
}
|
||||
|
||||
// Alias for `exists<T>()`
|
||||
template <typename T>
|
||||
bool is() const {
|
||||
return exists<T>();
|
||||
}
|
||||
|
||||
/// Removes the value at the current location, regardless of what it currently is. This
|
||||
/// does nothing if the current location does not have a value.
|
||||
void erase() {
|
||||
if (!_conf.is_dirty() && !get_clean())
|
||||
return;
|
||||
|
||||
config::dict* data = &_conf.dirty().data();
|
||||
for (const auto& key : _inter_keys) {
|
||||
auto it = data->find(key);
|
||||
data = it != data->end() ? std::get_if<config::dict>(&it->second) : nullptr;
|
||||
if (!data)
|
||||
return;
|
||||
}
|
||||
data->erase(_last_key);
|
||||
}
|
||||
|
||||
/// Adds a value to the set at the current location. If the current value is not a set or
|
||||
/// does not exist then dicts will be created to reach it and a new set will be created.
|
||||
void set_insert(std::string_view value) {
|
||||
insert_if_missing(config::scalar{std::string{value}});
|
||||
}
|
||||
void set_insert(int64_t value) { insert_if_missing(config::scalar{value}); }
|
||||
|
||||
/// Removes a value from the set at the current location. If the current value does not
|
||||
/// exist then nothing happens. If it does exist, but is not a set, it will be replaced
|
||||
/// with an empty set. Otherwise the given value will be removed from the set, if present.
|
||||
void set_erase(std::string_view value) {
|
||||
set_erase_impl(config::scalar{std::string{value}});
|
||||
}
|
||||
void set_erase(int64_t value) { set_erase_impl(scalar{value}); }
|
||||
|
||||
/// Emplaces a value at the current location. As with assignment, this creates dicts as
|
||||
/// needed along the keys to reach the target. The existing value (if present) is destroyed
|
||||
/// to make room for the new one.
|
||||
template <
|
||||
typename T,
|
||||
typename... Args,
|
||||
typename = std::enable_if_t<
|
||||
is_one_of<T, config::set, config::dict, int64_t, std::string>>>
|
||||
T& emplace(Args&&... args) {
|
||||
if constexpr (is_one_of<T, int64_t, std::string>)
|
||||
return get_dirty<scalar>().emplace<T>(std::forward<Args>(args)...);
|
||||
|
||||
return get_dirty().emplace<T>(std::forward<Args>(args)...);
|
||||
}
|
||||
};
|
||||
|
||||
/// Wrapper for the ConfigBase's root `data` field to provide data access. Only provides a []
|
||||
/// that gets you into a DictFieldProxy.
|
||||
class DictFieldRoot {
|
||||
ConfigBase& _conf;
|
||||
DictFieldRoot(DictFieldRoot&&) = delete;
|
||||
DictFieldRoot(const DictFieldRoot&) = delete;
|
||||
DictFieldRoot& operator=(DictFieldRoot&&) = delete;
|
||||
DictFieldRoot& operator=(const DictFieldRoot&) = delete;
|
||||
|
||||
public:
|
||||
DictFieldRoot(ConfigBase& b) : _conf{b} {}
|
||||
|
||||
/// Access a dict element. This returns a proxy object for accessing the value, but does
|
||||
/// *not* auto-vivify the path (unless/until you assign to it).
|
||||
DictFieldProxy operator[](std::string key) const& {
|
||||
return DictFieldProxy{_conf, std::move(key)};
|
||||
}
|
||||
};
|
||||
|
||||
// Called when dumping to obtain any extra data that a subclass needs to store to reconstitute
|
||||
// the object. The base implementation does nothing. The counterpart to this,
|
||||
// `load_extra_data()`, is called when loading from a dump that has extra data; a subclass
|
||||
// should either override both (if it needs to serialize extra data) or neither (if it needs no
|
||||
// extra data). Internally this extra data (if non-empty) is stored in the "+" key of the dump.
|
||||
virtual oxenc::bt_dict extra_data() const { return {}; }
|
||||
|
||||
// Called when constructing from a dump that has extra data. The base implementation does
|
||||
// nothing.
|
||||
virtual void load_extra_data(oxenc::bt_dict extra) {}
|
||||
|
||||
public:
|
||||
virtual ~ConfigBase() = default;
|
||||
|
||||
// Proxy class providing read and write access to the contained config data.
|
||||
const DictFieldRoot data{*this};
|
||||
|
||||
// Accesses the storage namespace where this config type is to be stored/loaded from. See
|
||||
// namespaces.hpp for the underlying integer values.
|
||||
virtual Namespace storage_namespace() const = 0;
|
||||
|
||||
// How many config lags should be used for this object; default to 5. Implementing subclasses
|
||||
// can override to return a different constant if desired. More lags require more "diff"
|
||||
// storage in the config messages, but also allow for a higher tolerance of simultaneous message
|
||||
// conflicts.
|
||||
virtual int config_lags() const { return 5; }
|
||||
|
||||
// This takes all of the messages pulled down from the server and does whatever is necessary to
|
||||
// merge (or replace) the current values.
|
||||
//
|
||||
// After this call the caller should check `needs_push()` to see if the data on hand was updated
|
||||
// and needs to be pushed to the server again.
|
||||
//
|
||||
// Will throw on serious error (i.e. if neither the current nor any of the given configs are
|
||||
// parseable).
|
||||
virtual void merge(const std::vector<std::string_view>& configs);
|
||||
|
||||
// Returns true if we are currently dirty (i.e. have made changes that haven't been serialized
|
||||
// yet).
|
||||
bool is_dirty() const { return _state == ConfigState::Dirty; }
|
||||
|
||||
// Returns true if we are curently clean (i.e. our current config is stored on the server and
|
||||
// unmodified).
|
||||
bool is_clean() const { return _state == ConfigState::Clean; }
|
||||
|
||||
// Returns true if this object contains updated data that has not yet been confirmed stored on
|
||||
// the server. This will be true whenever `is_clean()` is false: that is, if we are currently
|
||||
// "dirty" (i.e. have changes that haven't been pushed) or are still awaiting confirmation of
|
||||
// storage of the most recent serialized push data.
|
||||
virtual bool needs_push() const;
|
||||
|
||||
// Returns the data to push to the server along with the seqno value of the data. If the config
|
||||
// is currently dirty (i.e. has previously unsent modifications) then this marks it as
|
||||
// awaiting-confirmation instead of dirty so that any future change immediately increments the
|
||||
// seqno.
|
||||
virtual std::pair<std::string, seqno_t> push();
|
||||
|
||||
// Should be called after the push is confirmed stored on the storage server swarm to let the
|
||||
// object know the data is stored. (Once this is called `needs_push` will start returning false
|
||||
// until something changes). Takes the seqno that was pushed so that the object can ensure that
|
||||
// the latest version was pushed (i.e. in case there have been other changes since the `push()`
|
||||
// call that returned this seqno).
|
||||
//
|
||||
// It is safe to call this multiple times with the same seqno value, and with out-of-order
|
||||
// seqnos (e.g. calling with seqno 122 after having called with 123; the duplicates and earlier
|
||||
// ones will just be ignored).
|
||||
virtual void confirm_pushed(seqno_t seqno);
|
||||
|
||||
// Returns a dump of the current state for storage in the database; this value would get passed
|
||||
// into the constructor to reconstitute the object (including the push/not pushed status). This
|
||||
// method is *not* virtual: if subclasses need to store extra data they should set it in the
|
||||
// `subclass_data` field.
|
||||
std::string dump();
|
||||
|
||||
// Returns true if something has changed since the last call to `dump()` that requires calling
|
||||
// and saving the `dump()` data again.
|
||||
virtual bool needs_dump() const { return _needs_dump; }
|
||||
};
|
||||
|
||||
// The C++ struct we hold opaquely inside the C internals struct. This is designed so that any
|
||||
// internals<T> has the same layout so that it doesn't matter whether we unbox to an
|
||||
// internals<ConfigBase> or internals<SubType>.
|
||||
template <
|
||||
typename ConfigT = ConfigBase,
|
||||
std::enable_if_t<std::is_base_of_v<ConfigBase, ConfigT>, int> = 0>
|
||||
struct internals final {
|
||||
std::unique_ptr<ConfigBase> config;
|
||||
std::string error;
|
||||
|
||||
// Dereferencing falls through to the ConfigBase object
|
||||
ConfigT* operator->() {
|
||||
if constexpr (std::is_same_v<ConfigT, ConfigBase>)
|
||||
return config.get();
|
||||
else {
|
||||
auto* c = dynamic_cast<ConfigT*>(config.get());
|
||||
assert(c);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
const ConfigT* operator->() const {
|
||||
if constexpr (std::is_same_v<ConfigT, ConfigBase>)
|
||||
return config.get();
|
||||
else {
|
||||
auto* c = dynamic_cast<ConfigT*>(config.get());
|
||||
assert(c);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
ConfigT& operator*() { return *operator->(); }
|
||||
const ConfigT& operator*() const { return *operator->(); }
|
||||
};
|
||||
|
||||
template <typename T = ConfigBase, std::enable_if_t<std::is_base_of_v<ConfigBase, T>, int> = 0>
|
||||
inline internals<T>& unbox(config_object* conf) {
|
||||
return *static_cast<internals<T>*>(conf->internals);
|
||||
}
|
||||
template <typename T = ConfigBase, std::enable_if_t<std::is_base_of_v<ConfigBase, T>, int> = 0>
|
||||
inline const internals<T>& unbox(const config_object* conf) {
|
||||
return *static_cast<const internals<T>*>(conf->internals);
|
||||
}
|
||||
|
||||
// Sets an error message in the internals.error string and updates the last_error pointer in the
|
||||
// outer (C) config_object struct to point at it.
|
||||
void set_error(config_object* conf, std::string e);
|
||||
|
||||
// Same as above, but gets the error string out of an exception and passed through a return value.
|
||||
// Intended to simplify catch-and-return-error such as:
|
||||
// try {
|
||||
// whatever();
|
||||
// } catch (const std::exception& e) {
|
||||
// return set_error(conf, LIB_SESSION_ERR_OHNOES, e);
|
||||
// }
|
||||
inline int set_error(config_object* conf, int errcode, const std::exception& e) {
|
||||
set_error(conf, e.what());
|
||||
return errcode;
|
||||
}
|
||||
|
||||
// Copies a value contained in a string into a new malloced char buffer, returning the buffer and
|
||||
// size via the two pointer arguments.
|
||||
void copy_out(const std::string& data, char** out, size_t* outlen);
|
||||
|
||||
} // namespace session::config
|
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
enum config_error {
|
||||
/// Value returned for no error
|
||||
SESSION_ERR_NONE = 0,
|
||||
/// Error indicating that initialization failed because the dumped data being loaded is invalid.
|
||||
SESSION_ERR_INVALID_DUMP = 1,
|
||||
/// Error indicated a bad value, e.g. if trying to set something invalid in a config field.
|
||||
SESSION_ERR_BAD_VALUE = 2,
|
||||
};
|
||||
|
||||
// Returns a generic string for a given integer error code as returned by some functions. Depending
|
||||
// on the call, a more details error string may be available in the config_object's `last_error`
|
||||
// field.
|
||||
const char* config_errstr(int err);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace session::config {
|
||||
|
||||
enum class Namespace : std::int16_t {
|
||||
UserProfile = 2,
|
||||
ClosedGroupInfo = 11,
|
||||
};
|
||||
|
||||
} // namespace session::config
|
|
@ -0,0 +1,51 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "base.h"
|
||||
|
||||
/// Constructs a user profile config object and sets a pointer to it in `conf`. To restore an
|
||||
/// existing dump produced by a past instantiation's call to `dump()` pass the dump value via `dump`
|
||||
/// and `dumplen`; to construct a new, empty profile pass NULL and 0.
|
||||
///
|
||||
/// `error` must either be NULL or a pointer to a buffer of at least 256 bytes.
|
||||
///
|
||||
/// Returns 0 on success; returns a non-zero error code and sets error (if not NULL) to the
|
||||
/// exception message on failure.
|
||||
///
|
||||
/// When done with the object the `config_object` must be destroyed by passing the pointer to
|
||||
/// config_free() (in `session/config/base.h`).
|
||||
int user_profile_init(config_object** conf, const char* dump, size_t dumplen, char* error)
|
||||
__attribute__((warn_unused_result));
|
||||
|
||||
/// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at
|
||||
/// all. Should be copied right away as the pointer may not remain valid beyond other API calls.
|
||||
const char* user_profile_get_name(const config_object* conf);
|
||||
|
||||
/// Sets the user profile name to the null-terminated C string. Returns 0 on success, non-zero on
|
||||
/// error (and sets the config_object's error string).
|
||||
int user_profile_set_name(config_object* conf, const char* name);
|
||||
|
||||
typedef struct user_profile_pic {
|
||||
// Null-terminated C string containing the uploaded URL of the pic. Will be NULL if there is no
|
||||
// profile pic.
|
||||
const char* url;
|
||||
// The profile pic decryption key, in bytes. This is a byte buffer of length `keylen`, *not* a
|
||||
// null-terminated C string. Will be NULL if there is no profile pic.
|
||||
const char* key;
|
||||
size_t keylen;
|
||||
} user_profile_pic;
|
||||
|
||||
// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile
|
||||
// pic is not currently set, and otherwise should be copied right away (they will not be valid
|
||||
// beyond other API calls on this config object).
|
||||
user_profile_pic user_profile_get_pic(const config_object* conf);
|
||||
|
||||
// Sets a user profile
|
||||
int user_profile_set_pic(config_object* conf, user_profile_pic pic);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
|
@ -0,0 +1,43 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <session/config.hpp>
|
||||
|
||||
#include "base.hpp"
|
||||
#include "namespaces.hpp"
|
||||
|
||||
namespace session::config {
|
||||
|
||||
/// keys used in this config, either currently or in the past (so that we don't reuse):
|
||||
///
|
||||
/// n - user profile name
|
||||
/// p - user profile url
|
||||
/// q - user profile decryption key (binary)
|
||||
|
||||
class UserProfile final : public ConfigBase {
|
||||
|
||||
public:
|
||||
/// Constructs a new, blank user profile.
|
||||
UserProfile() = default;
|
||||
|
||||
/// Constructs a user profile from existing data
|
||||
explicit UserProfile(std::string_view dumped) : ConfigBase{dumped} {}
|
||||
|
||||
Namespace storage_namespace() const override { return Namespace::UserProfile; }
|
||||
|
||||
/// Returns the user profile name, or nullptr if there is no profile name set.
|
||||
const std::string* get_name() const;
|
||||
|
||||
/// Sets the user profile name
|
||||
void set_name(std::string_view new_name);
|
||||
|
||||
/// Gets the user's current profile pic URL and decryption key. Returns nullptr for *both*
|
||||
/// values if *either* value is unset or empty in the config data.
|
||||
std::pair<const std::string*, const std::string*> get_profile_pic() const;
|
||||
|
||||
/// Sets the user's current profile pic to a new URL and decryption key. Clears both if either
|
||||
/// one is empty.
|
||||
void set_profile_pic(std::string url, std::string key);
|
||||
};
|
||||
|
||||
} // namespace session::config
|
|
@ -0,0 +1,43 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace session {
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
/// An uploaded file is its URL + decryption key
|
||||
struct Uploaded {
|
||||
std::string url;
|
||||
std::string key;
|
||||
};
|
||||
|
||||
/// A conversation disappearing messages setting
|
||||
struct Disappearing {
|
||||
/// The possible modes of a disappearing messages setting.
|
||||
enum class Mode : int { None = 0, AfterSend = 1, AfterRead = 2 };
|
||||
|
||||
/// The mode itself
|
||||
Mode mode = Mode::None;
|
||||
|
||||
/// The timer value; this is only used when mode is not None.
|
||||
std::chrono::seconds timer = 0s;
|
||||
};
|
||||
|
||||
/// A Session ID: an x25519 pubkey, with a 05 identifying prefix. On the wire we send just the
|
||||
/// 32-byte pubkey value (i.e. not hex, without the prefix).
|
||||
struct SessionID {
|
||||
/// The fixed session netid, 0x05
|
||||
static constexpr unsigned char netid = 0x05;
|
||||
|
||||
/// The raw x25519 pubkey, as bytes
|
||||
std::array<unsigned char, 32> pubkey;
|
||||
|
||||
/// Returns the full pubkey in hex, including the netid prefix.
|
||||
std::string hex() const;
|
||||
};
|
||||
|
||||
} // namespace session
|
|
@ -0,0 +1,34 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/// XEd25519-signed a message given a curve25519 privkey and message. Writes the 64-byte signature
|
||||
/// to `sig` on success and returns 0. Returns non-zero on failure.
|
||||
__attribute__((warn_unused_result)) int session_xed25519_sign(
|
||||
unsigned char* signature /* 64 byte buffer */,
|
||||
const unsigned char* curve25519_privkey /* 32 bytes */,
|
||||
const unsigned char* msg,
|
||||
const unsigned int msg_len);
|
||||
|
||||
/// Verifies an XEd25519-signed message given a 64-byte signature, 32-byte curve25519 pubkey, and
|
||||
/// message. Returns 0 if the signature verifies successfully, non-zero on failure.
|
||||
__attribute__((warn_unused_result)) int session_xed25519_verify(
|
||||
const unsigned char* signature /* 64 bytes */,
|
||||
const unsigned char* pubkey /* 32-bytes */,
|
||||
const unsigned char* msg,
|
||||
const unsigned int msg_len);
|
||||
|
||||
/// Given a curve25519 pubkey, this writes the associated XEd25519-derived Ed25519 pubkey into
|
||||
/// ed25519_pubkey. Note, however, that there are *two* possible Ed25519 pubkeys that could result
|
||||
/// in a given curve25519 pubkey: this always returns the positive value. You can get the other
|
||||
/// possibility (the negative) by flipping the sign bit, i.e. `returned_pubkey[31] |= 0x80`.
|
||||
/// Returns 0 on success, non-0 on failure.
|
||||
__attribute__((warn_unused_result)) int session_xed25519_pubkey(
|
||||
unsigned char* ed25519_pubkey /* 32-byte output buffer */,
|
||||
const unsigned char* curve25519_pubkey /* 32 bytes */);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace session::xed25519 {
|
||||
|
||||
using ustring_view = std::basic_string_view<unsigned char>;
|
||||
|
||||
/// XEd25519-signs a message given the curve25519 privkey and message.
|
||||
std::array<unsigned char, 64> sign(
|
||||
ustring_view curve25519_privkey /* 32 bytes */, ustring_view msg);
|
||||
|
||||
/// "Softer" version that takes and returns strings of regular chars
|
||||
std::string sign(std::string_view curve25519_privkey /* 32 bytes */, std::string_view msg);
|
||||
|
||||
/// Verifies a curve25519 message allegedly signed by the given curve25519 pubkey
|
||||
[[nodiscard]] bool verify(
|
||||
ustring_view signature /* 64 bytes */,
|
||||
ustring_view curve25519_pubkey /* 32 bytes */,
|
||||
ustring_view msg);
|
||||
|
||||
/// "Softer" version that takes strings of regular chars
|
||||
[[nodiscard]] bool verify(
|
||||
std::string_view signature /* 64 bytes */,
|
||||
std::string_view curve25519_pubkey /* 32 bytes */,
|
||||
std::string_view msg);
|
||||
|
||||
/// Given a curve25519 pubkey, this returns the associated XEd25519-derived Ed25519 pubkey. Note,
|
||||
/// however, that there are *two* possible Ed25519 pubkeys that could result in a given curve25519
|
||||
/// pubkey: this always returns the positive value. You can get the other possibility (the
|
||||
/// negative) by flipping the sign bit, i.e. `returned_pubkey[31] |= 0x80`.
|
||||
std::array<unsigned char, 32> pubkey(ustring_view curve25519_pubkey);
|
||||
|
||||
/// "Softer" version that takes/returns strings of regular chars
|
||||
std::string pubkey(std::string_view curve25519_pubkey);
|
||||
|
||||
} // namespace session::xed25519
|
|
@ -0,0 +1,121 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
internal extension OpenGroupAPI {
|
||||
struct BatchRequest: Encodable {
|
||||
let requests: [Child]
|
||||
|
||||
init(requests: [Info]) {
|
||||
self.requests = requests.map { $0.child }
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
try container.encode(requests)
|
||||
}
|
||||
|
||||
// MARK: - BatchRequest.Info
|
||||
|
||||
struct Info {
|
||||
public let endpoint: any EndpointType
|
||||
public let responseType: Codable.Type
|
||||
fileprivate let child: Child
|
||||
|
||||
public init<T: Encodable, E: EndpointType, R: Codable>(request: Request<T, E>, responseType: R.Type) {
|
||||
self.endpoint = request.endpoint
|
||||
self.responseType = HTTP.BatchSubResponse<R>.self
|
||||
self.child = Child(request: request)
|
||||
}
|
||||
|
||||
public init<T: Encodable, E: EndpointType>(request: Request<T, E>) {
|
||||
self.init(
|
||||
request: request,
|
||||
responseType: NoResponse.self
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BatchRequest.Child
|
||||
|
||||
struct Child: Encodable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case method
|
||||
case path
|
||||
case headers
|
||||
case json
|
||||
case b64
|
||||
case bytes
|
||||
}
|
||||
|
||||
let method: HTTPMethod
|
||||
let path: String
|
||||
let headers: [String: String]?
|
||||
|
||||
/// The `jsonBodyEncoder` is used to avoid having to make `Child` a generic type (haven't found a good way
|
||||
/// to keep `Child` encodable using protocols unfortunately so need this work around)
|
||||
private let jsonBodyEncoder: ((inout KeyedEncodingContainer<CodingKeys>, CodingKeys) throws -> ())?
|
||||
private let b64: String?
|
||||
private let bytes: [UInt8]?
|
||||
|
||||
internal init<T: Encodable, E: EndpointType>(request: Request<T, E>) {
|
||||
self.method = request.method
|
||||
self.path = request.urlPathAndParamsString
|
||||
self.headers = (request.headers.isEmpty ? nil : request.headers.toHTTPHeaders())
|
||||
|
||||
// Note: Need to differentiate between JSON, b64 string and bytes body values to ensure
|
||||
// they are encoded correctly so the server knows how to handle them
|
||||
switch request.body {
|
||||
case let bodyString as String:
|
||||
self.jsonBodyEncoder = nil
|
||||
self.b64 = bodyString
|
||||
self.bytes = nil
|
||||
|
||||
case let bodyBytes as [UInt8]:
|
||||
self.jsonBodyEncoder = nil
|
||||
self.b64 = nil
|
||||
self.bytes = bodyBytes
|
||||
|
||||
default:
|
||||
self.jsonBodyEncoder = { [body = request.body] container, key in
|
||||
try container.encodeIfPresent(body, forKey: key)
|
||||
}
|
||||
self.b64 = nil
|
||||
self.bytes = nil
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(method, forKey: .method)
|
||||
try container.encode(path, forKey: .path)
|
||||
try container.encodeIfPresent(headers, forKey: .headers)
|
||||
try jsonBodyEncoder?(&container, .json)
|
||||
try container.encodeIfPresent(b64, forKey: .b64)
|
||||
try container.encodeIfPresent(bytes, forKey: .bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
internal extension Promise where T == HTTP.BatchResponse {
|
||||
func map<E: EndpointType>(
|
||||
requests: [OpenGroupAPI.BatchRequest.Info],
|
||||
toHashMapFor endpointType: E.Type
|
||||
) -> Promise<[E: (ResponseInfoType, Codable?)]> {
|
||||
return self.map { result in
|
||||
result.enumerated()
|
||||
.reduce(into: [:]) { prev, next in
|
||||
guard let endpoint: E = requests[next.offset].endpoint as? E else { return }
|
||||
|
||||
prev[endpoint] = next.element
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,327 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SessionUtil
|
||||
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
/// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches
|
||||
class ConfigUserProfileSpec: QuickSpec {
|
||||
// MARK: - Spec
|
||||
|
||||
override func spec() {
|
||||
it("behaves correctly") {
|
||||
// Initialize a brand new, empty config because we have no dump data to deal with.
|
||||
let error: UnsafeMutablePointer<CChar>? = nil
|
||||
var conf: UnsafeMutablePointer<config_object>? = nil
|
||||
expect(user_profile_init(&conf, nil, 0, error)).to(equal(0))
|
||||
|
||||
// We don't need to push anything, since this is an empty config
|
||||
expect(config_needs_push(conf)).to(beFalse())
|
||||
// And we haven't changed anything so don't need to dump to db
|
||||
expect(config_needs_dump(conf)).to(beFalse())
|
||||
|
||||
// Since it's empty there shouldn't be a name.
|
||||
let namePtr = user_profile_get_name(conf)
|
||||
expect(namePtr).to(beNil())
|
||||
|
||||
var toPush: UnsafeMutablePointer<CChar>? = nil
|
||||
var toPushLen: Int = 0
|
||||
// We don't need to push since we haven't changed anything, so this call is mainly just for
|
||||
// testing:
|
||||
let seqno: Int64 = config_push(conf, &toPush, &toPushLen)
|
||||
expect(toPush).toNot(beNil())
|
||||
expect(seqno).to(equal(0))
|
||||
expect(String(cString: toPush!)).to(equal("d1:#i0e1:&de1:<le1:=dee"))
|
||||
|
||||
// This should also be unset:
|
||||
let pic = user_profile_get_pic(conf)
|
||||
expect(pic.url).to(beNil())
|
||||
expect(pic.key).to(beNil())
|
||||
expect(pic.keylen).to(equal(0))
|
||||
|
||||
|
||||
// Now let's go set a profile name and picture:
|
||||
expect(user_profile_set_name(conf, "Kallie")).to(equal(0))
|
||||
let profileUrl: [CChar] = "http://example.org/omg-pic-123.bmp"
|
||||
.bytes
|
||||
.map { CChar(bitPattern: $0) }
|
||||
let profileKey: [CChar] = "secretNOTSECRET"
|
||||
.bytes
|
||||
.map { CChar(bitPattern: $0) }
|
||||
let p: user_profile_pic = profileUrl.withUnsafeBufferPointer { profileUrlPtr in
|
||||
profileKey.withUnsafeBufferPointer { profileKeyPtr in
|
||||
user_profile_pic(
|
||||
url: profileUrlPtr.baseAddress,
|
||||
key: profileKeyPtr.baseAddress,
|
||||
keylen: 6
|
||||
)
|
||||
}
|
||||
}
|
||||
expect(user_profile_set_pic(conf, p)).to(equal(0))
|
||||
|
||||
// Retrieve them just to make sure they set properly:
|
||||
let namePtr2 = user_profile_get_name(conf)
|
||||
expect(namePtr2).toNot(beNil())
|
||||
expect(String(cString: namePtr2!)).to(equal("Kallie"))
|
||||
|
||||
let pic2 = user_profile_get_pic(conf);
|
||||
expect(pic2.url).toNot(beNil())
|
||||
expect(pic2.key).toNot(beNil())
|
||||
expect(pic2.keylen).to(equal(6))
|
||||
expect(String(cString: pic2.url!)).to(equal("http://example.org/omg-pic-123.bmp"))
|
||||
expect(String(cString: pic2.key!)).to(equal("secret"))
|
||||
|
||||
// Since we've made changes, we should need to push new config to the swarm, *and* should need
|
||||
// to dump the updated state:
|
||||
expect(config_needs_push(conf)).to(beTrue())
|
||||
expect(config_needs_dump(conf)).to(beTrue())
|
||||
|
||||
var toPush2: UnsafeMutablePointer<CChar>? = nil
|
||||
var toPush2Len: Int = 0
|
||||
let seqno2 = config_push(conf, &toPush2, &toPush2Len);
|
||||
// incremented since we made changes (this only increments once between
|
||||
// dumps; even though we changed two fields here).
|
||||
expect(seqno2).to(equal(1))
|
||||
|
||||
// Note: This hex value differs from the value in the library tests because
|
||||
// it looks like the library has an "end of cell mark" character added at the
|
||||
// end (0x07 or '0007') so we need to manually add it to work
|
||||
let expHash0: [CChar] = Data(hex: "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965")
|
||||
.bytes
|
||||
.map { CChar(bitPattern: $0) }
|
||||
// .withUnsafeBufferPointer { profileKeyPtr in
|
||||
// String(cString: profileKeyPtr.baseAddress!)
|
||||
// }
|
||||
// The data to be actually pushed, expanded like this to make it somewhat human-readable:
|
||||
let expPush1: [CChar] = ["""
|
||||
d
|
||||
1:#i1e
|
||||
1:& d
|
||||
1:n 6:Kallie
|
||||
1:p 34:http://example.org/omg-pic-123.bmp
|
||||
1:q 6:secret
|
||||
e
|
||||
1:< l
|
||||
l i0e 32:
|
||||
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability
|
||||
.bytes
|
||||
.map { CChar(bitPattern: $0) },
|
||||
expHash0,
|
||||
"""
|
||||
de e
|
||||
e
|
||||
1:= d
|
||||
1:n 0:
|
||||
1:p 0:
|
||||
1:q 0:
|
||||
e
|
||||
e
|
||||
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability
|
||||
.bytes
|
||||
.map { CChar(bitPattern: $0) },
|
||||
// [CChar()] // Need to null-terminate the string or it'll crash
|
||||
]
|
||||
.flatMap { $0 }
|
||||
|
||||
expect(String(cString: toPush2!))
|
||||
// Need to null-terminate the string or it'll crash
|
||||
.to(equal(String(cString: expPush1.appending(CChar()))))
|
||||
|
||||
// We haven't dumped, so still need to dump:
|
||||
expect(config_needs_dump(conf)).to(beTrue())
|
||||
// We did call push, but we haven't confirmed it as stored yet, so this will still return true:
|
||||
expect(config_needs_push(conf)).to(beTrue())
|
||||
|
||||
var dump1: UnsafeMutablePointer<CChar>? = nil
|
||||
var dump1Len: Int = 0
|
||||
|
||||
config_dump(conf, &dump1, &dump1Len)
|
||||
// (in a real client we'd now store this to disk)
|
||||
|
||||
expect(config_needs_dump(conf)).to(beFalse())
|
||||
|
||||
let expDump1: [CChar] = [
|
||||
"""
|
||||
d
|
||||
1:! i2e
|
||||
1:$ \(expPush1.count):
|
||||
"""
|
||||
.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
|
||||
.bytes
|
||||
.map { CChar(bitPattern: $0) },
|
||||
expPush1,
|
||||
"""
|
||||
e
|
||||
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
|
||||
.bytes
|
||||
.map { CChar(bitPattern: $0) }
|
||||
]
|
||||
.flatMap { $0 }
|
||||
expect(String(cString: dump1!))
|
||||
.to(equal(String(cString: expDump1.appending(CChar()))))// Need to null-terminate the string or it'll crash
|
||||
|
||||
// So now imagine we got back confirmation from the swarm that the push has been stored:
|
||||
config_confirm_pushed(conf, seqno2)
|
||||
|
||||
expect(config_needs_push(conf)).to(beFalse())
|
||||
expect(config_needs_dump(conf)).to(beTrue()) // The confirmation changes state, so this makes us need a dump
|
||||
|
||||
var dump2: UnsafeMutablePointer<CChar>? = nil
|
||||
var dump2Len: Int = 0
|
||||
config_dump(conf, &dump2, &dump2Len)
|
||||
|
||||
expect(config_needs_dump(conf)).to(beFalse())
|
||||
|
||||
// Now we're going to set up a second, competing config object (in the real world this would be
|
||||
// another Session client somewhere).
|
||||
|
||||
// Start with an empty config, as above:
|
||||
let error2: UnsafeMutablePointer<CChar>? = nil
|
||||
var conf2: UnsafeMutablePointer<config_object>? = nil
|
||||
expect(user_profile_init(&conf2, nil, 0, error2)).to(equal(0))
|
||||
expect(config_needs_dump(conf2)).to(beFalse())
|
||||
|
||||
// Now imagine we just pulled down the `exp_push1` string from the swarm; we merge it into
|
||||
// conf2:
|
||||
let mergeData: [[CChar]] = [expPush1]
|
||||
var mergeDataPtr = mergeData.map { value in
|
||||
let cStringCopy = UnsafeMutableBufferPointer<CChar>.allocate(capacity: value.count)
|
||||
_ = cStringCopy.initialize(from: value)
|
||||
let ptr = UnsafePointer(cStringCopy.baseAddress)
|
||||
return UnsafePointer(cStringCopy.baseAddress)
|
||||
}
|
||||
var mergeSize: [Int] = [expPush1.count]
|
||||
config_merge(conf2, &mergeDataPtr, &mergeSize, 1)
|
||||
|
||||
// Our state has changed, so we need to dump:
|
||||
expect(config_needs_dump(conf2)).to(beTrue())
|
||||
var dump3: UnsafeMutablePointer<CChar>? = nil
|
||||
var dump3Len: Int = 0
|
||||
config_dump(conf, &dump3, &dump3Len)
|
||||
// (store in db)
|
||||
expect(config_needs_dump(conf2)).to(beFalse())// TODO: This one is broken now!!!
|
||||
|
||||
// We *don't* need to push: even though we updated, all we did is update to the merged data (and
|
||||
// didn't have any sort of merge conflict needed):
|
||||
expect(config_needs_push(conf2)).to(beFalse())
|
||||
|
||||
// Now let's create a conflicting update:
|
||||
|
||||
// Change the name on both clients:
|
||||
user_profile_set_name(conf, "Nibbler")
|
||||
user_profile_set_name(conf2, "Raz")
|
||||
|
||||
// And, on conf2, we're also going to change the profile pic:
|
||||
let profile2Url: [CChar] = "http://new.example.com/pic"
|
||||
.bytes
|
||||
.map { CChar(bitPattern: $0) }
|
||||
let profile2Key: [CChar] = "qwert\0yuio"
|
||||
.bytes
|
||||
.map { CChar(bitPattern: $0) }
|
||||
let p2: user_profile_pic = profile2Url.withUnsafeBufferPointer { profile2UrlPtr in
|
||||
profile2Key.withUnsafeBufferPointer { profile2KeyPtr in
|
||||
user_profile_pic(
|
||||
url: profile2UrlPtr.baseAddress,
|
||||
key: profile2KeyPtr.baseAddress,
|
||||
keylen: 10
|
||||
)
|
||||
}
|
||||
}
|
||||
user_profile_set_pic(conf2, p2)
|
||||
|
||||
// Both have changes, so push need a push
|
||||
expect(config_needs_push(conf)).to(beTrue())
|
||||
expect(config_needs_push(conf2)).to(beTrue())
|
||||
var toPush3: UnsafeMutablePointer<CChar>? = nil
|
||||
var toPush3Len: Int = 0
|
||||
let seqno3 = config_push(conf, &toPush3, &toPush3Len)
|
||||
expect(seqno3).to(equal(2)) // incremented, since we made a field change
|
||||
|
||||
var toPush4: UnsafeMutablePointer<CChar>? = nil
|
||||
var toPush4Len: Int = 0
|
||||
let seqno4 = config_push(conf2, &toPush4, &toPush4Len)
|
||||
expect(seqno4).to(equal(2)) // incremented, since we made a field change
|
||||
|
||||
var dump4: UnsafeMutablePointer<CChar>? = nil
|
||||
var dump4Len: Int = 0
|
||||
config_dump(conf, &dump4, &dump4Len);
|
||||
var dump5: UnsafeMutablePointer<CChar>? = nil
|
||||
var dump5Len: Int = 0
|
||||
config_dump(conf2, &dump5, &dump5Len);
|
||||
// (store in db)
|
||||
|
||||
// Since we set different things, we're going to get back different serialized data to be
|
||||
// pushed:
|
||||
expect(String(cString: toPush3!)).toNot(equal(String(cString: toPush4!)))
|
||||
|
||||
// Now imagine that each client pushed its `seqno=2` config to the swarm, but then each client
|
||||
// also fetches new messages and pulls down the other client's `seqno=2` value.
|
||||
|
||||
// Feed the new config into each other. (This array could hold multiple configs if we pulled
|
||||
// down more than one).
|
||||
let mergeData2: [String] = [String(cString: toPush3!)]
|
||||
var mergeData2Ptr = mergeData2.map { value in
|
||||
value.bytes.map { CChar(bitPattern: $0) }.withUnsafeBufferPointer { valuePtr in
|
||||
valuePtr.baseAddress
|
||||
}
|
||||
}
|
||||
var mergeSize2: [Int] = [toPush3Len]
|
||||
config_merge(conf2, &mergeData2Ptr, &mergeSize2, 1)
|
||||
let mergeData3: [String] = [String(cString: toPush4!)]
|
||||
var mergeData3Ptr = mergeData3.map { value in
|
||||
value.bytes.map { CChar(bitPattern: $0) }.withUnsafeBufferPointer { valuePtr in
|
||||
valuePtr.baseAddress
|
||||
}
|
||||
}
|
||||
var mergeSize3: [Int] = [toPush4Len]
|
||||
config_merge(conf, &mergeData3Ptr, &mergeSize3, 1)
|
||||
|
||||
// Now after the merge we *will* want to push from both client, since both will have generated a
|
||||
// merge conflict update (with seqno = 3).
|
||||
expect(config_needs_push(conf)).to(beTrue())
|
||||
expect(config_needs_push(conf2)).to(beTrue())
|
||||
let seqno5 = config_push(conf, &toPush3, &toPush3Len);
|
||||
let seqno6 = config_push(conf2, &toPush4, &toPush4Len);
|
||||
|
||||
expect(seqno5).to(equal(3))
|
||||
expect(seqno6).to(equal(3))
|
||||
|
||||
// They should have resolved the conflict to the same thing:
|
||||
expect(String(cString: user_profile_get_name(conf)!)).to(equal("Nibbler"))
|
||||
expect(String(cString: user_profile_get_name(conf2)!)).to(equal("Nibbler"))
|
||||
// (Note that they could have also both resolved to "Raz" here, but the hash of the serialized
|
||||
// message just happens to have a higher hash -- and thus gets priority -- for this particular
|
||||
// test).
|
||||
|
||||
// Since only one of them set a profile pic there should be no conflict there:
|
||||
let pic3 = user_profile_get_pic(conf)
|
||||
expect(pic3.url).toNot(beNil())
|
||||
expect(String(cString: pic3.url!)).to(equal("http://new.example.com/pic"))
|
||||
expect(pic3.key).toNot(beNil())
|
||||
expect(String(cString: pic3.key!)).to(equal("qwert\0yuio"))
|
||||
let pic4 = user_profile_get_pic(conf2)
|
||||
expect(pic4.url).toNot(beNil())
|
||||
expect(String(cString: pic4.url!)).to(equal("http://new.example.com/pic"))
|
||||
expect(pic4.key).toNot(beNil())
|
||||
expect(String(cString: pic4.key!)).to(equal("qwert\0yuio"))
|
||||
|
||||
config_confirm_pushed(conf, seqno5)
|
||||
config_confirm_pushed(conf2, seqno6)
|
||||
|
||||
var dump6: UnsafeMutablePointer<CChar>? = nil
|
||||
var dump6Len: Int = 0
|
||||
config_dump(conf, &dump6, &dump6Len);
|
||||
var dump7: UnsafeMutablePointer<CChar>? = nil
|
||||
var dump7Len: Int = 0
|
||||
config_dump(conf2, &dump7, &dump7Len);
|
||||
// (store in db)
|
||||
|
||||
expect(config_needs_dump(conf)).to(beFalse())
|
||||
expect(config_needs_dump(conf2)).to(beFalse())
|
||||
expect(config_needs_push(conf)).to(beFalse())
|
||||
expect(config_needs_push(conf2)).to(beFalse())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,12 +21,12 @@ class BatchRequestInfoSpec: QuickSpec {
|
|||
override func spec() {
|
||||
// MARK: - BatchSubRequest
|
||||
|
||||
describe("a BatchSubRequest") {
|
||||
var subRequest: OpenGroupAPI.BatchSubRequest!
|
||||
describe("a BatchRequest.Child") {
|
||||
var subRequest: OpenGroupAPI.BatchRequest.Child!
|
||||
|
||||
context("when initializing") {
|
||||
it("sets the headers to nil if there aren't any") {
|
||||
subRequest = OpenGroupAPI.BatchSubRequest(
|
||||
subRequest = OpenGroupAPI.BatchRequest.Child(
|
||||
request: Request<NoBody, OpenGroupAPI.Endpoint>(
|
||||
server: "testServer",
|
||||
endpoint: .batch
|
||||
|
@ -37,7 +37,7 @@ class BatchRequestInfoSpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("converts the headers to HTTP headers") {
|
||||
subRequest = OpenGroupAPI.BatchSubRequest(
|
||||
subRequest = OpenGroupAPI.BatchRequest.Child(
|
||||
request: Request<NoBody, OpenGroupAPI.Endpoint>(
|
||||
method: .get,
|
||||
server: "testServer",
|
||||
|
@ -54,7 +54,7 @@ class BatchRequestInfoSpec: QuickSpec {
|
|||
|
||||
context("when encoding") {
|
||||
it("successfully encodes a string body") {
|
||||
subRequest = OpenGroupAPI.BatchSubRequest(
|
||||
subRequest = OpenGroupAPI.BatchRequest.Child(
|
||||
request: Request<String, OpenGroupAPI.Endpoint>(
|
||||
method: .get,
|
||||
server: "testServer",
|
||||
|
@ -72,7 +72,7 @@ class BatchRequestInfoSpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("successfully encodes a byte body") {
|
||||
subRequest = OpenGroupAPI.BatchSubRequest(
|
||||
subRequest = OpenGroupAPI.BatchRequest.Child(
|
||||
request: Request<[UInt8], OpenGroupAPI.Endpoint>(
|
||||
method: .get,
|
||||
server: "testServer",
|
||||
|
@ -90,7 +90,7 @@ class BatchRequestInfoSpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("successfully encodes a JSON body") {
|
||||
subRequest = OpenGroupAPI.BatchSubRequest(
|
||||
subRequest = OpenGroupAPI.BatchRequest.Child(
|
||||
request: Request<TestType, OpenGroupAPI.Endpoint>(
|
||||
method: .get,
|
||||
server: "testServer",
|
||||
|
@ -109,112 +109,9 @@ class BatchRequestInfoSpec: QuickSpec {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - BatchSubResponse<T>
|
||||
|
||||
describe("a BatchSubResponse<T>") {
|
||||
context("when decoding") {
|
||||
it("decodes correctly") {
|
||||
let jsonString: String = """
|
||||
{
|
||||
"code": 200,
|
||||
"headers": {
|
||||
"testKey": "testValue"
|
||||
},
|
||||
"body": {
|
||||
"stringValue": "testValue"
|
||||
}
|
||||
}
|
||||
"""
|
||||
let subResponse: OpenGroupAPI.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
|
||||
OpenGroupAPI.BatchSubResponse<TestType>.self,
|
||||
from: jsonString.data(using: .utf8)!
|
||||
)
|
||||
|
||||
expect(subResponse).toNot(beNil())
|
||||
expect(subResponse?.body).toNot(beNil())
|
||||
}
|
||||
|
||||
it("decodes with invalid body data") {
|
||||
let jsonString: String = """
|
||||
{
|
||||
"code": 200,
|
||||
"headers": {
|
||||
"testKey": "testValue"
|
||||
},
|
||||
"body": "Hello!!!"
|
||||
}
|
||||
"""
|
||||
let subResponse: OpenGroupAPI.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
|
||||
OpenGroupAPI.BatchSubResponse<TestType>.self,
|
||||
from: jsonString.data(using: .utf8)!
|
||||
)
|
||||
|
||||
expect(subResponse).toNot(beNil())
|
||||
}
|
||||
|
||||
it("flags invalid body data as invalid") {
|
||||
let jsonString: String = """
|
||||
{
|
||||
"code": 200,
|
||||
"headers": {
|
||||
"testKey": "testValue"
|
||||
},
|
||||
"body": "Hello!!!"
|
||||
}
|
||||
"""
|
||||
let subResponse: OpenGroupAPI.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
|
||||
OpenGroupAPI.BatchSubResponse<TestType>.self,
|
||||
from: jsonString.data(using: .utf8)!
|
||||
)
|
||||
|
||||
expect(subResponse).toNot(beNil())
|
||||
expect(subResponse?.body).to(beNil())
|
||||
expect(subResponse?.failedToParseBody).to(beTrue())
|
||||
}
|
||||
|
||||
it("does not flag a missing or invalid optional body as invalid") {
|
||||
let jsonString: String = """
|
||||
{
|
||||
"code": 200,
|
||||
"headers": {
|
||||
"testKey": "testValue"
|
||||
},
|
||||
}
|
||||
"""
|
||||
let subResponse: OpenGroupAPI.BatchSubResponse<TestType?>? = try? JSONDecoder().decode(
|
||||
OpenGroupAPI.BatchSubResponse<TestType?>.self,
|
||||
from: jsonString.data(using: .utf8)!
|
||||
)
|
||||
|
||||
expect(subResponse).toNot(beNil())
|
||||
expect(subResponse?.body).to(beNil())
|
||||
expect(subResponse?.failedToParseBody).to(beFalse())
|
||||
}
|
||||
|
||||
it("does not flag a NoResponse body as invalid") {
|
||||
let jsonString: String = """
|
||||
{
|
||||
"code": 200,
|
||||
"headers": {
|
||||
"testKey": "testValue"
|
||||
},
|
||||
}
|
||||
"""
|
||||
let subResponse: OpenGroupAPI.BatchSubResponse<NoResponse>? = try? JSONDecoder().decode(
|
||||
OpenGroupAPI.BatchSubResponse<NoResponse>.self,
|
||||
from: jsonString.data(using: .utf8)!
|
||||
)
|
||||
|
||||
expect(subResponse).toNot(beNil())
|
||||
expect(subResponse?.body).to(beNil())
|
||||
expect(subResponse?.failedToParseBody).to(beFalse())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BatchRequestInfo<T, R>
|
||||
|
||||
describe("a BatchRequestInfo<T, R>") {
|
||||
describe("a BatchRequest.Info") {
|
||||
var request: Request<TestType, OpenGroupAPI.Endpoint>!
|
||||
|
||||
beforeEach {
|
||||
|
@ -229,26 +126,26 @@ class BatchRequestInfoSpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("initializes correctly when given a request") {
|
||||
let requestInfo: OpenGroupAPI.BatchRequestInfo<TestType> = OpenGroupAPI.BatchRequestInfo(
|
||||
let requestInfo: OpenGroupAPI.BatchRequest.Info = OpenGroupAPI.BatchRequest.Info(
|
||||
request: request
|
||||
)
|
||||
|
||||
expect(requestInfo.request).to(equal(request))
|
||||
expect(requestInfo.responseType == OpenGroupAPI.BatchSubResponse<NoResponse>.self).to(beTrue())
|
||||
expect(requestInfo.endpoint.path).to(equal(request.endpoint.path))
|
||||
expect(requestInfo.responseType == HTTP.BatchSubResponse<NoResponse>.self).to(beTrue())
|
||||
}
|
||||
|
||||
it("initializes correctly when given a request and a response type") {
|
||||
let requestInfo: OpenGroupAPI.BatchRequestInfo<TestType> = OpenGroupAPI.BatchRequestInfo(
|
||||
let requestInfo: OpenGroupAPI.BatchRequest.Info = OpenGroupAPI.BatchRequest.Info(
|
||||
request: request,
|
||||
responseType: TestType.self
|
||||
)
|
||||
|
||||
expect(requestInfo.request).to(equal(request))
|
||||
expect(requestInfo.responseType == OpenGroupAPI.BatchSubResponse<TestType>.self).to(beTrue())
|
||||
expect(requestInfo.endpoint.path).to(equal(request.endpoint.path))
|
||||
expect(requestInfo.responseType == HTTP.BatchSubResponse<TestType>.self).to(beTrue())
|
||||
}
|
||||
|
||||
it("exposes the endpoint correctly") {
|
||||
let requestInfo: OpenGroupAPI.BatchRequestInfo<TestType> = OpenGroupAPI.BatchRequestInfo(
|
||||
let requestInfo: OpenGroupAPI.BatchRequest.Info = OpenGroupAPI.BatchRequest.Info(
|
||||
request: request
|
||||
)
|
||||
|
||||
|
@ -256,130 +153,17 @@ class BatchRequestInfoSpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("generates a sub request correctly") {
|
||||
let requestInfo: OpenGroupAPI.BatchRequestInfo<TestType> = OpenGroupAPI.BatchRequestInfo(
|
||||
request: request
|
||||
let batchRequest: OpenGroupAPI.BatchRequest = OpenGroupAPI.BatchRequest(
|
||||
requests: [
|
||||
OpenGroupAPI.BatchRequest.Info(
|
||||
request: request
|
||||
)
|
||||
]
|
||||
)
|
||||
let subRequest: OpenGroupAPI.BatchSubRequest = requestInfo.toSubRequest()
|
||||
|
||||
expect(subRequest.method).to(equal(request.method))
|
||||
expect(subRequest.path).to(equal(request.urlPathAndParamsString))
|
||||
expect(subRequest.headers).to(beNil())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
// MARK: --Decodable
|
||||
|
||||
describe("a Decodable") {
|
||||
it("decodes correctly") {
|
||||
let jsonData: Data = "{\"stringValue\":\"testValue\"}".data(using: .utf8)!
|
||||
let result: TestType? = try? TestType.decoded(from: jsonData)
|
||||
|
||||
expect(result).to(equal(TestType(stringValue: "testValue")))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - --Promise
|
||||
|
||||
describe("an (OnionRequestResponseInfoType, Data?) Promise") {
|
||||
var responseInfo: OnionRequestResponseInfoType!
|
||||
var capabilities: OpenGroupAPI.Capabilities!
|
||||
var pinnedMessage: OpenGroupAPI.PinnedMessage!
|
||||
var data: Data!
|
||||
|
||||
beforeEach {
|
||||
responseInfo = OnionRequestAPI.ResponseInfo(code: 200, headers: [:])
|
||||
capabilities = OpenGroupAPI.Capabilities(capabilities: [], missing: nil)
|
||||
pinnedMessage = OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 123, pinnedBy: "test")
|
||||
data = """
|
||||
[\([
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: capabilities,
|
||||
failedToParseBody: false
|
||||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: pinnedMessage,
|
||||
failedToParseBody: false
|
||||
)
|
||||
)
|
||||
]
|
||||
.map { String(data: $0, encoding: .utf8)! }
|
||||
.joined(separator: ","))]
|
||||
""".data(using: .utf8)!
|
||||
}
|
||||
|
||||
it("decodes valid data correctly") {
|
||||
let result = Promise.value((responseInfo, data))
|
||||
.decoded(as: [
|
||||
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>.self,
|
||||
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>.self
|
||||
])
|
||||
|
||||
expect(result.value).toNot(beNil())
|
||||
expect((result.value?[0].1 as? OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>)?.body)
|
||||
.to(equal(capabilities))
|
||||
expect((result.value?[1].1 as? OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>)?.body)
|
||||
.to(equal(pinnedMessage))
|
||||
}
|
||||
|
||||
it("fails if there is no data") {
|
||||
let result = Promise.value((responseInfo, nil)).decoded(as: [])
|
||||
|
||||
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
|
||||
}
|
||||
|
||||
it("fails if the data is not JSON") {
|
||||
let result = Promise.value((responseInfo, Data([1, 2, 3]))).decoded(as: [])
|
||||
|
||||
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
|
||||
}
|
||||
|
||||
it("fails if the data is not a JSON array") {
|
||||
let result = Promise.value((responseInfo, "{}".data(using: .utf8))).decoded(as: [])
|
||||
|
||||
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
|
||||
}
|
||||
|
||||
it("fails if the JSON array does not have the same number of items as the expected types") {
|
||||
let result = Promise.value((responseInfo, data))
|
||||
.decoded(as: [
|
||||
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>.self,
|
||||
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>.self,
|
||||
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>.self
|
||||
])
|
||||
|
||||
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
|
||||
}
|
||||
|
||||
it("fails if one of the JSON array values fails to decode") {
|
||||
data = """
|
||||
[\([
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: capabilities,
|
||||
failedToParseBody: false
|
||||
)
|
||||
)
|
||||
]
|
||||
.map { String(data: $0, encoding: .utf8)! }
|
||||
.joined(separator: ",")),{"test": "test"}]
|
||||
""".data(using: .utf8)!
|
||||
let result = Promise.value((responseInfo, data))
|
||||
.decoded(as: [
|
||||
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>.self,
|
||||
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>.self
|
||||
])
|
||||
|
||||
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
|
||||
expect(batchRequest.requests[0].method).to(equal(request.method))
|
||||
expect(batchRequest.requests[0].path).to(equal(request.urlPathAndParamsString))
|
||||
expect(batchRequest.requests[0].headers).to(beNil())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@ class SOGSMessageSpec: QuickSpec {
|
|||
expect {
|
||||
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
|
||||
}
|
||||
.to(throwError(HTTP.Error.parsingFailed))
|
||||
.to(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
|
||||
it("errors if the data is not a base64 encoded string") {
|
||||
|
@ -128,7 +128,7 @@ class SOGSMessageSpec: QuickSpec {
|
|||
expect {
|
||||
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
|
||||
}
|
||||
.to(throwError(HTTP.Error.parsingFailed))
|
||||
.to(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
|
||||
it("errors if the signature is not a base64 encoded string") {
|
||||
|
@ -150,7 +150,7 @@ class SOGSMessageSpec: QuickSpec {
|
|||
expect {
|
||||
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
|
||||
}
|
||||
.to(throwError(HTTP.Error.parsingFailed))
|
||||
.to(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
|
||||
it("errors if the dependencies are not provided to the JSONDecoder") {
|
||||
|
@ -159,7 +159,7 @@ class SOGSMessageSpec: QuickSpec {
|
|||
expect {
|
||||
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
|
||||
}
|
||||
.to(throwError(HTTP.Error.parsingFailed))
|
||||
.to(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
|
||||
it("errors if the session_id value is not valid") {
|
||||
|
@ -181,7 +181,7 @@ class SOGSMessageSpec: QuickSpec {
|
|||
expect {
|
||||
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
|
||||
}
|
||||
.to(throwError(HTTP.Error.parsingFailed))
|
||||
.to(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
|
||||
|
||||
|
@ -239,7 +239,7 @@ class SOGSMessageSpec: QuickSpec {
|
|||
expect {
|
||||
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
|
||||
}
|
||||
.to(throwError(HTTP.Error.parsingFailed))
|
||||
.to(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -274,7 +274,7 @@ class SOGSMessageSpec: QuickSpec {
|
|||
expect {
|
||||
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
|
||||
}
|
||||
.to(throwError(HTTP.Error.parsingFailed))
|
||||
.to(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
var mockNonce24Generator: MockNonce24Generator!
|
||||
var dependencies: SMKDependencies!
|
||||
|
||||
var response: (OnionRequestResponseInfoType, Codable)? = nil
|
||||
var pollResponse: [OpenGroupAPI.Endpoint: (OnionRequestResponseInfoType, Codable?)]?
|
||||
var response: (ResponseInfoType, Codable)? = nil
|
||||
var pollResponse: [OpenGroupAPI.Endpoint: (ResponseInfoType, Codable?)]?
|
||||
var error: Error?
|
||||
|
||||
describe("an OpenGroupAPI") {
|
||||
|
@ -136,7 +136,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
override class var mockResponse: Data? {
|
||||
let responses: [Data] = [
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil),
|
||||
|
@ -144,7 +144,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: try! JSONDecoder().decode(
|
||||
|
@ -163,7 +163,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: [OpenGroupAPI.Message](),
|
||||
|
@ -212,7 +212,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
let requestData: TestOnionRequestAPI.RequestData? = (pollResponse?[.capabilities]?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.urlString).to(equal("testserver/batch"))
|
||||
expect(requestData?.httpMethod).to(equal("POST"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.publicKey).to(equal("88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
|
||||
}
|
||||
|
||||
|
@ -369,7 +368,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
override class var mockResponse: Data? {
|
||||
let responses: [Data] = [
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil),
|
||||
|
@ -377,7 +376,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: try! JSONDecoder().decode(
|
||||
|
@ -396,7 +395,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: [OpenGroupAPI.Message](),
|
||||
|
@ -404,7 +403,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: [OpenGroupAPI.DirectMessage](),
|
||||
|
@ -412,7 +411,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: [OpenGroupAPI.DirectMessage](),
|
||||
|
@ -575,7 +574,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
override class var mockResponse: Data? {
|
||||
let responses: [Data] = [
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil),
|
||||
|
@ -583,7 +582,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""),
|
||||
|
@ -591,7 +590,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""),
|
||||
|
@ -626,9 +625,9 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
)
|
||||
expect(error?.localizedDescription).to(beNil())
|
||||
|
||||
let capabilitiesResponse: OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>? = (pollResponse?[.capabilities]?.1 as? OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>)
|
||||
let pollInfoResponse: OpenGroupAPI.BatchSubResponse<OpenGroupAPI.RoomPollInfo>? = (pollResponse?[.roomPollInfo("testRoom", 0)]?.1 as? OpenGroupAPI.BatchSubResponse<OpenGroupAPI.RoomPollInfo>)
|
||||
let messagesResponse: OpenGroupAPI.BatchSubResponse<[Failable<OpenGroupAPI.Message>]>? = (pollResponse?[.roomMessagesRecent("testRoom")]?.1 as? OpenGroupAPI.BatchSubResponse<[Failable<OpenGroupAPI.Message>]>)
|
||||
let capabilitiesResponse: HTTP.BatchSubResponse<OpenGroupAPI.Capabilities>? = (pollResponse?[.capabilities]?.1 as? HTTP.BatchSubResponse<OpenGroupAPI.Capabilities>)
|
||||
let pollInfoResponse: HTTP.BatchSubResponse<OpenGroupAPI.RoomPollInfo>? = (pollResponse?[.roomPollInfo("testRoom", 0)]?.1 as? HTTP.BatchSubResponse<OpenGroupAPI.RoomPollInfo>)
|
||||
let messagesResponse: HTTP.BatchSubResponse<[Failable<OpenGroupAPI.Message>]>? = (pollResponse?[.roomMessagesRecent("testRoom")]?.1 as? HTTP.BatchSubResponse<[Failable<OpenGroupAPI.Message>]>)
|
||||
expect(capabilitiesResponse?.failedToParseBody).to(beFalse())
|
||||
expect(pollInfoResponse?.failedToParseBody).to(beTrue())
|
||||
expect(messagesResponse?.failedToParseBody).to(beTrue())
|
||||
|
@ -651,7 +650,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
|
||||
expect(error?.localizedDescription)
|
||||
.toEventually(
|
||||
equal(HTTP.Error.parsingFailed.localizedDescription),
|
||||
equal(HTTPError.parsingFailed.localizedDescription),
|
||||
timeout: .milliseconds(100)
|
||||
)
|
||||
|
||||
|
@ -680,7 +679,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
|
||||
expect(error?.localizedDescription)
|
||||
.toEventually(
|
||||
equal(HTTP.Error.parsingFailed.localizedDescription),
|
||||
equal(HTTPError.parsingFailed.localizedDescription),
|
||||
timeout: .milliseconds(100)
|
||||
)
|
||||
|
||||
|
@ -709,7 +708,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
|
||||
expect(error?.localizedDescription)
|
||||
.toEventually(
|
||||
equal(HTTP.Error.parsingFailed.localizedDescription),
|
||||
equal(HTTPError.parsingFailed.localizedDescription),
|
||||
timeout: .milliseconds(100)
|
||||
)
|
||||
|
||||
|
@ -738,7 +737,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
|
||||
expect(error?.localizedDescription)
|
||||
.toEventually(
|
||||
equal(HTTP.Error.parsingFailed.localizedDescription),
|
||||
equal(HTTPError.parsingFailed.localizedDescription),
|
||||
timeout: .milliseconds(100)
|
||||
)
|
||||
|
||||
|
@ -750,7 +749,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
override class var mockResponse: Data? {
|
||||
let responses: [Data] = [
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil),
|
||||
|
@ -758,7 +757,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: try! JSONDecoder().decode(
|
||||
|
@ -799,7 +798,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
|
||||
expect(error?.localizedDescription)
|
||||
.toEventually(
|
||||
equal(HTTP.Error.parsingFailed.localizedDescription),
|
||||
equal(HTTPError.parsingFailed.localizedDescription),
|
||||
timeout: .milliseconds(100)
|
||||
)
|
||||
|
||||
|
@ -819,7 +818,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
dependencies = dependencies.with(onionApi: TestApi.self)
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities)?
|
||||
var response: (info: ResponseInfoType, data: OpenGroupAPI.Capabilities)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -846,7 +845,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate request data
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("GET"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/capabilities"))
|
||||
}
|
||||
}
|
||||
|
@ -890,7 +888,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
dependencies = dependencies.with(onionApi: TestApi.self)
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: [OpenGroupAPI.Room])?
|
||||
var response: (info: ResponseInfoType, data: [OpenGroupAPI.Room])?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -917,7 +915,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate request data
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("GET"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/rooms"))
|
||||
}
|
||||
}
|
||||
|
@ -960,7 +957,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
override class var mockResponse: Data? {
|
||||
let responses: [Data] = [
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: capabilitiesData,
|
||||
|
@ -968,7 +965,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: roomData,
|
||||
|
@ -982,7 +979,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
dependencies = dependencies.with(onionApi: TestApi.self)
|
||||
|
||||
var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))?
|
||||
var response: (capabilities: (info: ResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: ResponseInfoType, data: OpenGroupAPI.Room?))?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1011,7 +1008,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate request data
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.capabilities.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("POST"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/sequence"))
|
||||
}
|
||||
}
|
||||
|
@ -1024,7 +1020,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
override class var mockResponse: Data? {
|
||||
let responses: [Data] = [
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: capabilitiesData,
|
||||
|
@ -1038,7 +1034,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
dependencies = dependencies.with(onionApi: TestApi.self)
|
||||
|
||||
var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))?
|
||||
var response: (capabilities: (info: ResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: ResponseInfoType, data: OpenGroupAPI.Room?))?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1056,7 +1052,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
|
||||
expect(error?.localizedDescription)
|
||||
.toEventually(
|
||||
equal(HTTP.Error.parsingFailed.localizedDescription),
|
||||
equal(HTTPError.parsingFailed.localizedDescription),
|
||||
timeout: .milliseconds(100)
|
||||
)
|
||||
|
||||
|
@ -1096,7 +1092,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
override class var mockResponse: Data? {
|
||||
let responses: [Data] = [
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: roomData,
|
||||
|
@ -1110,7 +1106,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
dependencies = dependencies.with(onionApi: TestApi.self)
|
||||
|
||||
var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))?
|
||||
var response: (capabilities: (info: ResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: ResponseInfoType, data: OpenGroupAPI.Room?))?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1128,7 +1124,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
|
||||
expect(error?.localizedDescription)
|
||||
.toEventually(
|
||||
equal(HTTP.Error.parsingFailed.localizedDescription),
|
||||
equal(HTTPError.parsingFailed.localizedDescription),
|
||||
timeout: .milliseconds(100)
|
||||
)
|
||||
|
||||
|
@ -1169,7 +1165,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
override class var mockResponse: Data? {
|
||||
let responses: [Data] = [
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: capabilitiesData,
|
||||
|
@ -1177,7 +1173,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: roomData,
|
||||
|
@ -1185,7 +1181,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""),
|
||||
|
@ -1199,7 +1195,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
dependencies = dependencies.with(onionApi: TestApi.self)
|
||||
|
||||
var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))?
|
||||
var response: (capabilities: (info: ResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: ResponseInfoType, data: OpenGroupAPI.Room?))?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1216,7 +1212,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
|
||||
expect(error?.localizedDescription)
|
||||
.toEventually(
|
||||
equal(HTTP.Error.parsingFailed.localizedDescription),
|
||||
equal(HTTPError.parsingFailed.localizedDescription),
|
||||
timeout: .milliseconds(100)
|
||||
)
|
||||
|
||||
|
@ -1258,7 +1254,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("correctly sends the message") {
|
||||
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1291,7 +1287,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate signature headers
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("POST"))
|
||||
expect(requestData?.server).to(equal("testServer"))
|
||||
expect(requestData?.urlString).to(equal("testServer/room/testRoom/message"))
|
||||
}
|
||||
|
||||
|
@ -1304,7 +1299,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("signs the message correctly") {
|
||||
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1344,7 +1339,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
_ = try OpenGroup.deleteAll(db)
|
||||
}
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1379,7 +1374,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
_ = try Identity.filter(id: .x25519PrivateKey).deleteAll(db)
|
||||
}
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1412,7 +1407,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
mockEd25519.reset() // The 'keyPair' value doesn't equate so have to explicitly reset
|
||||
mockEd25519.when { try $0.sign(data: anyArray(), keyPair: any()) }.thenReturn(nil)
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1452,7 +1447,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("signs the message correctly") {
|
||||
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1492,7 +1487,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
_ = try OpenGroup.deleteAll(db)
|
||||
}
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1527,7 +1522,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
_ = try Identity.filter(id: .ed25519SecretKey).deleteAll(db)
|
||||
}
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1568,7 +1563,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
.thenReturn(nil)
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1621,7 +1616,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
dependencies = dependencies.with(onionApi: TestApi.self)
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1651,7 +1646,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate request data
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("GET"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/room/testRoom/message/123"))
|
||||
}
|
||||
}
|
||||
|
@ -1674,7 +1668,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("correctly sends the update") {
|
||||
var response: (info: OnionRequestResponseInfoType, data: Data?)?
|
||||
var response: (info: ResponseInfoType, data: Data?)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1703,7 +1697,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate signature headers
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("PUT"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/room/testRoom/message/123"))
|
||||
}
|
||||
|
||||
|
@ -1716,7 +1709,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("signs the message correctly") {
|
||||
var response: (info: OnionRequestResponseInfoType, data: Data?)?
|
||||
var response: (info: ResponseInfoType, data: Data?)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1755,7 +1748,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
_ = try OpenGroup.deleteAll(db)
|
||||
}
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: Data?)?
|
||||
var response: (info: ResponseInfoType, data: Data?)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1789,7 +1782,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
_ = try Identity.filter(id: .x25519PrivateKey).deleteAll(db)
|
||||
}
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: Data?)?
|
||||
var response: (info: ResponseInfoType, data: Data?)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1821,7 +1814,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
mockEd25519.reset() // The 'keyPair' value doesn't equate so have to explicitly reset
|
||||
mockEd25519.when { try $0.sign(data: anyArray(), keyPair: any()) }.thenReturn(nil)
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: Data?)?
|
||||
var response: (info: ResponseInfoType, data: Data?)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1860,7 +1853,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("signs the message correctly") {
|
||||
var response: (info: OnionRequestResponseInfoType, data: Data?)?
|
||||
var response: (info: ResponseInfoType, data: Data?)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1899,7 +1892,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
_ = try OpenGroup.deleteAll(db)
|
||||
}
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: Data?)?
|
||||
var response: (info: ResponseInfoType, data: Data?)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1933,7 +1926,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
_ = try Identity.filter(id: .ed25519SecretKey).deleteAll(db)
|
||||
}
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: Data?)?
|
||||
var response: (info: ResponseInfoType, data: Data?)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -1973,7 +1966,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
.thenReturn(nil)
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: Data?)?
|
||||
var response: (info: ResponseInfoType, data: Data?)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -2010,7 +2003,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
dependencies = dependencies.with(onionApi: TestApi.self)
|
||||
|
||||
var response: (info: OnionRequestResponseInfoType, data: Data?)?
|
||||
var response: (info: ResponseInfoType, data: Data?)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -2037,13 +2030,12 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate request data
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("DELETE"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/room/testRoom/message/123"))
|
||||
}
|
||||
}
|
||||
|
||||
context("when deleting all messages for a user") {
|
||||
var response: (info: OnionRequestResponseInfoType, data: Data?)?
|
||||
var response: (info: ResponseInfoType, data: Data?)?
|
||||
|
||||
beforeEach {
|
||||
class TestApi: TestOnionRequestAPI {
|
||||
|
@ -2082,7 +2074,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate request data
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("DELETE"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/room/testRoom/all/testUserId"))
|
||||
}
|
||||
}
|
||||
|
@ -2096,7 +2087,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
dependencies = dependencies.with(onionApi: TestApi.self)
|
||||
|
||||
var response: OnionRequestResponseInfoType?
|
||||
var response: ResponseInfoType?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -2123,7 +2114,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate request data
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("POST"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/room/testRoom/pin/123"))
|
||||
}
|
||||
}
|
||||
|
@ -2135,7 +2125,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
dependencies = dependencies.with(onionApi: TestApi.self)
|
||||
|
||||
var response: OnionRequestResponseInfoType?
|
||||
var response: ResponseInfoType?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -2162,7 +2152,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate request data
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("POST"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/room/testRoom/unpin/123"))
|
||||
}
|
||||
}
|
||||
|
@ -2174,7 +2163,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
dependencies = dependencies.with(onionApi: TestApi.self)
|
||||
|
||||
var response: OnionRequestResponseInfoType?
|
||||
var response: ResponseInfoType?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -2200,7 +2189,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate request data
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("POST"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/room/testRoom/unpin/all"))
|
||||
}
|
||||
}
|
||||
|
@ -2241,7 +2229,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate signature headers
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("POST"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/room/testRoom/file"))
|
||||
}
|
||||
|
||||
|
@ -2277,7 +2264,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
|
||||
// Validate signature headers
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.headers[Header.contentDisposition.rawValue])
|
||||
expect(requestData?.headers[HTTPHeader.contentDisposition])
|
||||
.toNot(contain("filename"))
|
||||
}
|
||||
|
||||
|
@ -2314,7 +2301,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
|
||||
// Validate signature headers
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.headers[Header.contentDisposition.rawValue]).to(contain("TestFileName"))
|
||||
expect(requestData?.headers[HTTPHeader.contentDisposition]).to(contain("TestFileName"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2352,7 +2339,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate signature headers
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("GET"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/room/testRoom/file/1"))
|
||||
}
|
||||
}
|
||||
|
@ -2383,7 +2369,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("correctly sends the message request") {
|
||||
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.SendDirectMessageResponse)?
|
||||
var response: (info: ResponseInfoType, data: OpenGroupAPI.SendDirectMessageResponse)?
|
||||
|
||||
mockStorage
|
||||
.read { db in
|
||||
|
@ -2413,7 +2399,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate signature headers
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("POST"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/inbox/testUserId"))
|
||||
}
|
||||
}
|
||||
|
@ -2421,7 +2406,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// MARK: - Users
|
||||
|
||||
context("when banning a user") {
|
||||
var response: (info: OnionRequestResponseInfoType, data: Data?)?
|
||||
var response: (info: ResponseInfoType, data: Data?)?
|
||||
|
||||
beforeEach {
|
||||
class TestApi: TestOnionRequestAPI {
|
||||
|
@ -2461,7 +2446,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate request data
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("POST"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/user/testUserId/ban"))
|
||||
}
|
||||
|
||||
|
@ -2531,7 +2515,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
|
||||
context("when unbanning a user") {
|
||||
var response: (info: OnionRequestResponseInfoType, data: Data?)?
|
||||
var response: (info: ResponseInfoType, data: Data?)?
|
||||
|
||||
beforeEach {
|
||||
class TestApi: TestOnionRequestAPI {
|
||||
|
@ -2570,7 +2554,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate request data
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("POST"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/user/testUserId/unban"))
|
||||
}
|
||||
|
||||
|
@ -2638,7 +2621,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
|
||||
context("when updating a users permissions") {
|
||||
var response: (info: OnionRequestResponseInfoType, data: Data?)?
|
||||
var response: (info: ResponseInfoType, data: Data?)?
|
||||
|
||||
beforeEach {
|
||||
class TestApi: TestOnionRequestAPI {
|
||||
|
@ -2680,7 +2663,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate request data
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("POST"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/user/testUserId/moderator"))
|
||||
}
|
||||
|
||||
|
@ -2773,7 +2755,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
|
||||
expect(error?.localizedDescription)
|
||||
.toEventually(
|
||||
equal(HTTP.Error.generic.localizedDescription),
|
||||
equal(HTTPError.generic.localizedDescription),
|
||||
timeout: .milliseconds(100)
|
||||
)
|
||||
|
||||
|
@ -2782,14 +2764,14 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
}
|
||||
|
||||
context("when banning and deleting all messages for a user") {
|
||||
var response: [OnionRequestResponseInfoType]?
|
||||
var response: [ResponseInfoType]?
|
||||
|
||||
beforeEach {
|
||||
class TestApi: TestOnionRequestAPI {
|
||||
override class var mockResponse: Data? {
|
||||
let responses: [Data] = [
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse<NoResponse>(
|
||||
HTTP.BatchSubResponse<NoResponse>(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: nil,
|
||||
|
@ -2797,7 +2779,7 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse<NoResponse>(
|
||||
HTTP.BatchSubResponse<NoResponse>(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: nil,
|
||||
|
@ -2842,7 +2824,6 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
// Validate request data
|
||||
let requestData: TestOnionRequestAPI.RequestData? = (response?.first as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.httpMethod).to(equal("POST"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.urlString).to(equal("testserver/sequence"))
|
||||
}
|
||||
|
||||
|
@ -3013,14 +2994,13 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.urlString).to(equal("testserver/rooms"))
|
||||
expect(requestData?.httpMethod).to(equal("GET"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.publicKey).to(equal("88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
|
||||
expect(requestData?.headers).to(haveCount(4))
|
||||
expect(requestData?.headers[Header.sogsPubKey.rawValue])
|
||||
expect(requestData?.headers[HTTPHeader.sogsPubKey])
|
||||
.to(equal("00bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc"))
|
||||
expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890"))
|
||||
expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg=="))
|
||||
expect(requestData?.headers[Header.sogsSignature.rawValue]).to(equal("TestSignature".bytes.toBase64()))
|
||||
expect(requestData?.headers[HTTPHeader.sogsTimestamp]).to(equal("1234567890"))
|
||||
expect(requestData?.headers[HTTPHeader.sogsNonce]).to(equal("pK6YRtQApl4NhECGizF0Cg=="))
|
||||
expect(requestData?.headers[HTTPHeader.sogsSignature]).to(equal("TestSignature".bytes.toBase64()))
|
||||
}
|
||||
|
||||
it("fails when the signature is not generated") {
|
||||
|
@ -3083,13 +3063,12 @@ class OpenGroupAPISpec: QuickSpec {
|
|||
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
|
||||
expect(requestData?.urlString).to(equal("testserver/rooms"))
|
||||
expect(requestData?.httpMethod).to(equal("GET"))
|
||||
expect(requestData?.server).to(equal("testserver"))
|
||||
expect(requestData?.publicKey).to(equal("88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
|
||||
expect(requestData?.headers).to(haveCount(4))
|
||||
expect(requestData?.headers[Header.sogsPubKey.rawValue]).to(equal("1588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
|
||||
expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890"))
|
||||
expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg=="))
|
||||
expect(requestData?.headers[Header.sogsSignature.rawValue]).to(equal("TestSogsSignature".bytes.toBase64()))
|
||||
expect(requestData?.headers[HTTPHeader.sogsPubKey]).to(equal("1588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
|
||||
expect(requestData?.headers[HTTPHeader.sogsTimestamp]).to(equal("1234567890"))
|
||||
expect(requestData?.headers[HTTPHeader.sogsNonce]).to(equal("pK6YRtQApl4NhECGizF0Cg=="))
|
||||
expect(requestData?.headers[HTTPHeader.sogsSignature]).to(equal("TestSogsSignature".bytes.toBase64()))
|
||||
}
|
||||
|
||||
it("fails when the blindedKeyPair is not generated") {
|
||||
|
|
|
@ -47,7 +47,7 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
override class var mockResponse: Data? {
|
||||
let responses: [Data] = [
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: capabilitiesData,
|
||||
|
@ -55,7 +55,7 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: roomData,
|
||||
|
@ -917,7 +917,7 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
|
||||
expect(error?.localizedDescription)
|
||||
.toEventually(
|
||||
equal(HTTP.Error.parsingFailed.localizedDescription),
|
||||
equal(HTTPError.parsingFailed.localizedDescription),
|
||||
timeout: .milliseconds(50)
|
||||
)
|
||||
}
|
||||
|
@ -1953,7 +1953,7 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
var didComplete: Bool = false // Prevent multi-threading test bugs
|
||||
|
||||
mockOGMCache.when { $0.groupImagePromises }
|
||||
.thenReturn([OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Promise(error: HTTP.Error.generic)])
|
||||
.thenReturn([OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Promise(error: HTTPError.generic)])
|
||||
|
||||
testPollInfo = OpenGroupAPI.RoomPollInfo(
|
||||
token: "testRoom",
|
||||
|
@ -3194,7 +3194,7 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
override class var mockResponse: Data? {
|
||||
let responses: [Data] = [
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: capabilitiesData,
|
||||
|
@ -3202,7 +3202,7 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: roomsData,
|
||||
|
@ -3349,7 +3349,7 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
|
||||
expect(error?.localizedDescription)
|
||||
.toEventually(
|
||||
equal(HTTP.Error.invalidResponse.localizedDescription),
|
||||
equal(HTTPError.invalidResponse.localizedDescription),
|
||||
timeout: .milliseconds(50)
|
||||
)
|
||||
expect(TestRoomsApi.callCounter).to(equal(9)) // First attempt + 8 retries
|
||||
|
@ -3369,7 +3369,7 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
|
||||
expect(error?.localizedDescription)
|
||||
.toEventually(
|
||||
equal(HTTP.Error.invalidResponse.localizedDescription),
|
||||
equal(HTTPError.invalidResponse.localizedDescription),
|
||||
timeout: .milliseconds(50)
|
||||
)
|
||||
expect(mockOGMCache)
|
||||
|
@ -3414,7 +3414,7 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
override class var mockResponse: Data? {
|
||||
let responses: [Data] = [
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: capabilitiesData,
|
||||
|
@ -3422,7 +3422,7 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
OpenGroupAPI.BatchSubResponse(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: roomsData,
|
||||
|
@ -3568,12 +3568,12 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
|
||||
it("adds the image retrieval promise to the cache") {
|
||||
class TestNeverReturningApi: OnionRequestAPIType {
|
||||
static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
return Promise<(OnionRequestResponseInfoType, Data?)>.pending().promise
|
||||
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String) -> Promise<(ResponseInfoType, Data?)> {
|
||||
return Promise<(ResponseInfoType, Data?)>.pending().promise
|
||||
}
|
||||
|
||||
static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String?) -> Promise<Data> {
|
||||
return Promise.value(Data())
|
||||
static func sendOnionRequest(_ payload: Data, to snode: Snode) -> Promise<(ResponseInfoType, Data?)> {
|
||||
return Promise.value((HTTP.ResponseInfo(code: 200, headers: [:]), Data()))
|
||||
}
|
||||
}
|
||||
dependencies = dependencies.with(onionApi: TestNeverReturningApi.self)
|
||||
|
|
|
@ -13,14 +13,18 @@ class TestOnionRequestAPI: OnionRequestAPIType {
|
|||
let urlString: String?
|
||||
let httpMethod: String
|
||||
let headers: [String: String]
|
||||
let snodeMethod: String?
|
||||
let body: Data?
|
||||
let destination: OnionRequestAPIDestination
|
||||
|
||||
let server: String
|
||||
let version: OnionRequestAPIVersion
|
||||
let publicKey: String?
|
||||
var publicKey: String? {
|
||||
switch destination {
|
||||
case .snode: return nil
|
||||
case .server(_, _, let x25519PublicKey, _, _): return x25519PublicKey
|
||||
}
|
||||
}
|
||||
}
|
||||
class ResponseInfo: OnionRequestResponseInfoType {
|
||||
|
||||
class ResponseInfo: ResponseInfoType {
|
||||
let requestData: RequestData
|
||||
let code: Int
|
||||
let headers: [String: String]
|
||||
|
@ -34,18 +38,20 @@ class TestOnionRequestAPI: OnionRequestAPIType {
|
|||
|
||||
class var mockResponse: Data? { return nil }
|
||||
|
||||
static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String) -> Promise<(ResponseInfoType, Data?)> {
|
||||
let responseInfo: ResponseInfo = ResponseInfo(
|
||||
requestData: RequestData(
|
||||
urlString: request.url?.absoluteString,
|
||||
httpMethod: (request.httpMethod ?? "GET"),
|
||||
headers: (request.allHTTPHeaderFields ?? [:]),
|
||||
snodeMethod: nil,
|
||||
body: request.httpBody,
|
||||
|
||||
server: server,
|
||||
version: version,
|
||||
publicKey: x25519PublicKey
|
||||
destination: OnionRequestAPIDestination.server(
|
||||
host: request.url!.host!,
|
||||
target: OnionRequestAPIVersion.v4.rawValue,
|
||||
x25519PublicKey: x25519PublicKey,
|
||||
scheme: request.url!.scheme,
|
||||
port: request.url!.port.map { UInt16($0) }
|
||||
)
|
||||
),
|
||||
code: 200,
|
||||
headers: [:]
|
||||
|
@ -54,7 +60,19 @@ class TestOnionRequestAPI: OnionRequestAPIType {
|
|||
return Promise.value((responseInfo, mockResponse))
|
||||
}
|
||||
|
||||
static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String?) -> Promise<Data> {
|
||||
return Promise.value(mockResponse!)
|
||||
static func sendOnionRequest(_ payload: Data, to snode: Snode) -> Promise<(ResponseInfoType, Data?)> {
|
||||
let responseInfo: ResponseInfo = ResponseInfo(
|
||||
requestData: RequestData(
|
||||
urlString: "\(snode.address):\(snode.port)/onion_req/v2",
|
||||
httpMethod: "POST",
|
||||
headers: [:],
|
||||
body: payload,
|
||||
destination: OnionRequestAPIDestination.snode(snode)
|
||||
),
|
||||
code: 200,
|
||||
headers: [:]
|
||||
)
|
||||
|
||||
return Promise.value((responseInfo, mockResponse))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public enum OnionRequestAPIDestination: CustomStringConvertible {
|
||||
public enum OnionRequestAPIDestination: CustomStringConvertible, Codable {
|
||||
case snode(Snode)
|
||||
case server(host: String, target: String, x25519PublicKey: String, scheme: String?, port: UInt16?)
|
||||
|
||||
|
|
|
@ -393,7 +393,11 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
|||
}
|
||||
|
||||
/// Sends an onion request to `server`. Builds new paths as needed.
|
||||
public static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion = .v4, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
public static func sendOnionRequest(
|
||||
_ request: URLRequest,
|
||||
to server: String, // TODO: Remove this 'server' value (unused)
|
||||
with x25519PublicKey: String
|
||||
) -> Promise<(ResponseInfoType, Data?)> {
|
||||
guard let url = request.url, let host = request.url?.host else {
|
||||
return Promise(error: OnionRequestAPIError.invalidURL)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
open class SSKDependencies: Dependencies {
|
||||
public var _onionApi: Atomic<OnionRequestAPIType.Type?>
|
||||
public var onionApi: OnionRequestAPIType.Type {
|
||||
get { Dependencies.getValueSettingIfNull(&_onionApi) { OnionRequestAPI.self } }
|
||||
set { _onionApi.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(
|
||||
onionApi: OnionRequestAPIType.Type? = nil,
|
||||
generalCache: Atomic<GeneralCacheType>? = nil,
|
||||
storage: Storage? = nil,
|
||||
scheduler: ValueObservationScheduler? = nil,
|
||||
standardUserDefaults: UserDefaultsType? = nil,
|
||||
date: Date? = nil
|
||||
) {
|
||||
_onionApi = Atomic(onionApi)
|
||||
|
||||
super.init(
|
||||
generalCache: generalCache,
|
||||
storage: storage,
|
||||
scheduler: scheduler,
|
||||
standardUserDefaults: standardUserDefaults,
|
||||
date: date
|
||||
)
|
||||
}
|
||||
}
|
|
@ -195,7 +195,7 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
|
|||
)
|
||||
)
|
||||
|
||||
viewModel.settingsData.first?.elements.last?.onTap?(nil)
|
||||
viewModel.tableData.first?.elements.last?.onTap?()
|
||||
}
|
||||
|
||||
it("shows the save button") {
|
||||
|
|
|
@ -97,7 +97,7 @@ class ThreadSettingsViewModelSpec: QuickSpec {
|
|||
.first(where: { $0.model == .content })?
|
||||
.elements
|
||||
.first(where: { $0.id == .searchConversation })?
|
||||
.onTap?(nil)
|
||||
.onTap?()
|
||||
|
||||
expect(didTriggerSearchCallbackTriggered).to(beTrue())
|
||||
}
|
||||
|
@ -106,8 +106,8 @@ class ThreadSettingsViewModelSpec: QuickSpec {
|
|||
viewModel.settingsData
|
||||
.first(where: { $0.model == .content })?
|
||||
.elements
|
||||
.first(where: { $0.id == .notificationMute })?
|
||||
.onTap?(nil)
|
||||
.first(where: { $0.id == .notifications })?
|
||||
.onTap?()
|
||||
|
||||
expect(
|
||||
mockStorage
|
||||
|
@ -208,15 +208,11 @@ class ThreadSettingsViewModelSpec: QuickSpec {
|
|||
context("when entering edit mode") {
|
||||
beforeEach {
|
||||
viewModel.rightNavItems.firstValue()??.first?.action?()
|
||||
|
||||
let leftAccessory: SessionCell.Accessory? = viewModel.settingsData.first?
|
||||
.elements.first?
|
||||
.leftAccessory
|
||||
|
||||
switch leftAccessory {
|
||||
case .threadInfo(_, _, _, _, let titleChanged): titleChanged?("TestNew")
|
||||
default: break
|
||||
}
|
||||
viewModel.textChanged("TestNew", for: .nickname)
|
||||
// TODO: Enter edit mode by pressing on the first item
|
||||
// viewModel.tableData.first?
|
||||
// .elements.first?
|
||||
// .onTap?()
|
||||
}
|
||||
|
||||
it("enters the editing state") {
|
||||
|
@ -341,15 +337,12 @@ class ThreadSettingsViewModelSpec: QuickSpec {
|
|||
context("when entering edit mode") {
|
||||
beforeEach {
|
||||
viewModel.rightNavItems.firstValue()??.first?.action?()
|
||||
viewModel.textChanged("TestUserNew", for: .nickname)
|
||||
|
||||
let leftAccessory: SessionCell.Accessory? = viewModel.settingsData.first?
|
||||
.elements.first?
|
||||
.leftAccessory
|
||||
|
||||
switch leftAccessory {
|
||||
case .threadInfo(_, _, _, _, let titleChanged): titleChanged?("TestUserNew")
|
||||
default: break
|
||||
}
|
||||
// TODO: Enter edit mode by pressing on the first item
|
||||
// viewModel.tableData.first?
|
||||
// .elements.first?
|
||||
// .onTap?()
|
||||
}
|
||||
|
||||
it("enters the editing state") {
|
||||
|
|
|
@ -132,7 +132,7 @@ class NotificationContentViewModelSpec: QuickSpec {
|
|||
|
||||
context("when tapping an item") {
|
||||
it("updates the saved preference") {
|
||||
viewModel.settingsData.first?.elements.last?.onTap?(nil)
|
||||
viewModel.tableData.first?.elements.last?.onTap?()
|
||||
|
||||
expect(mockStorage[.preferencesNotificationPreviewType])
|
||||
.to(equal(Preferences.NotificationPreviewType.noNameNoPreview))
|
||||
|
@ -147,7 +147,7 @@ class NotificationContentViewModelSpec: QuickSpec {
|
|||
receiveCompletion: { _ in },
|
||||
receiveValue: { _ in didDismissScreen = true }
|
||||
)
|
||||
viewModel.settingsData.first?.elements.last?.onTap?(nil)
|
||||
viewModel.tableData.first?.elements.last?.onTap?()
|
||||
|
||||
expect(didDismissScreen).to(beTrue())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
public extension HTTP {
|
||||
// MARK: - Convenience Aliases
|
||||
|
||||
typealias BatchResponse = [(ResponseInfoType, Codable?)]
|
||||
typealias BatchResponseTypes = [Codable.Type]
|
||||
|
||||
// MARK: - BatchSubResponse<T>
|
||||
|
||||
struct BatchSubResponse<T: Codable>: Codable {
|
||||
/// The numeric http response code (e.g. 200 for success)
|
||||
public let code: Int32
|
||||
|
||||
/// Any headers returned by the request
|
||||
public let headers: [String: String]
|
||||
|
||||
/// The body of the request; will be plain json if content-type is `application/json`, otherwise it will be base64 encoded data
|
||||
public let body: T?
|
||||
|
||||
/// A flag to indicate that there was a body but it failed to parse
|
||||
public let failedToParseBody: Bool
|
||||
|
||||
public init(
|
||||
code: Int32,
|
||||
headers: [String: String] = [:],
|
||||
body: T? = nil,
|
||||
failedToParseBody: Bool = false
|
||||
) {
|
||||
self.code = code
|
||||
self.headers = headers
|
||||
self.body = body
|
||||
self.failedToParseBody = failedToParseBody
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension HTTP.BatchSubResponse {
|
||||
init(from decoder: Decoder) throws {
|
||||
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let body: T? = try? container.decode(T.self, forKey: .body)
|
||||
|
||||
self = HTTP.BatchSubResponse(
|
||||
code: try container.decode(Int32.self, forKey: .code),
|
||||
headers: ((try? container.decode([String: String].self, forKey: .headers)) ?? [:]),
|
||||
body: body,
|
||||
failedToParseBody: (
|
||||
body == nil &&
|
||||
T.self != NoResponse.self &&
|
||||
!(T.self is ExpressibleByNilLiteral.Type)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
public extension Decodable {
|
||||
static func decoded(from data: Data, using dependencies: Dependencies = Dependencies()) throws -> Self {
|
||||
return try data.decoded(as: Self.self, using: dependencies)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Promise where T == (ResponseInfoType, Data?) {
|
||||
func decoded(as types: HTTP.BatchResponseTypes, on queue: DispatchQueue? = nil, using dependencies: Dependencies = Dependencies()) -> Promise<HTTP.BatchResponse> {
|
||||
self.map(on: queue) { responseInfo, maybeData -> HTTP.BatchResponse in
|
||||
// Need to split the data into an array of data so each item can be Decoded correctly
|
||||
guard let data: Data = maybeData else { throw HTTPError.parsingFailed }
|
||||
guard let jsonObject: Any = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) else {
|
||||
throw HTTPError.parsingFailed
|
||||
}
|
||||
|
||||
let dataArray: [Data]
|
||||
|
||||
switch jsonObject {
|
||||
case let anyArray as [Any]:
|
||||
dataArray = anyArray.compactMap { try? JSONSerialization.data(withJSONObject: $0) }
|
||||
|
||||
guard dataArray.count == types.count else { throw HTTPError.parsingFailed }
|
||||
|
||||
case let anyDict as [String: Any]:
|
||||
guard
|
||||
let resultsArray: [Data] = (anyDict["results"] as? [Any])?
|
||||
.compactMap({ try? JSONSerialization.data(withJSONObject: $0) }),
|
||||
resultsArray.count == types.count
|
||||
else { throw HTTPError.parsingFailed }
|
||||
|
||||
dataArray = resultsArray
|
||||
|
||||
default: throw HTTPError.parsingFailed
|
||||
}
|
||||
|
||||
do {
|
||||
return try zip(dataArray, types)
|
||||
.map { data, type in try type.decoded(from: data, using: dependencies) }
|
||||
.map { data in (responseInfo, data) }
|
||||
}
|
||||
catch {
|
||||
throw HTTPError.parsingFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
@testable import SessionUtilitiesKit
|
||||
|
||||
class BatchRequestInfoSpec: QuickSpec {
|
||||
struct TestType: Codable, Equatable {
|
||||
let stringValue: String
|
||||
}
|
||||
|
||||
struct TestType2: Codable, Equatable {
|
||||
let intValue: Int
|
||||
let stringValue2: String
|
||||
}
|
||||
|
||||
// MARK: - Spec
|
||||
|
||||
override func spec() {
|
||||
// MARK: - HTTP.BatchSubResponse<T>
|
||||
|
||||
describe("an HTTP.BatchSubResponse<T>") {
|
||||
context("when decoding") {
|
||||
it("decodes correctly") {
|
||||
let jsonString: String = """
|
||||
{
|
||||
"code": 200,
|
||||
"headers": {
|
||||
"testKey": "testValue"
|
||||
},
|
||||
"body": {
|
||||
"stringValue": "testValue"
|
||||
}
|
||||
}
|
||||
"""
|
||||
let subResponse: HTTP.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
|
||||
HTTP.BatchSubResponse<TestType>.self,
|
||||
from: jsonString.data(using: .utf8)!
|
||||
)
|
||||
|
||||
expect(subResponse).toNot(beNil())
|
||||
expect(subResponse?.body).toNot(beNil())
|
||||
}
|
||||
|
||||
it("decodes with invalid body data") {
|
||||
let jsonString: String = """
|
||||
{
|
||||
"code": 200,
|
||||
"headers": {
|
||||
"testKey": "testValue"
|
||||
},
|
||||
"body": "Hello!!!"
|
||||
}
|
||||
"""
|
||||
let subResponse: HTTP.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
|
||||
HTTP.BatchSubResponse<TestType>.self,
|
||||
from: jsonString.data(using: .utf8)!
|
||||
)
|
||||
|
||||
expect(subResponse).toNot(beNil())
|
||||
}
|
||||
|
||||
it("flags invalid body data as invalid") {
|
||||
let jsonString: String = """
|
||||
{
|
||||
"code": 200,
|
||||
"headers": {
|
||||
"testKey": "testValue"
|
||||
},
|
||||
"body": "Hello!!!"
|
||||
}
|
||||
"""
|
||||
let subResponse: HTTP.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
|
||||
HTTP.BatchSubResponse<TestType>.self,
|
||||
from: jsonString.data(using: .utf8)!
|
||||
)
|
||||
|
||||
expect(subResponse).toNot(beNil())
|
||||
expect(subResponse?.body).to(beNil())
|
||||
expect(subResponse?.failedToParseBody).to(beTrue())
|
||||
}
|
||||
|
||||
it("does not flag a missing or invalid optional body as invalid") {
|
||||
let jsonString: String = """
|
||||
{
|
||||
"code": 200,
|
||||
"headers": {
|
||||
"testKey": "testValue"
|
||||
},
|
||||
}
|
||||
"""
|
||||
let subResponse: HTTP.BatchSubResponse<TestType?>? = try? JSONDecoder().decode(
|
||||
HTTP.BatchSubResponse<TestType?>.self,
|
||||
from: jsonString.data(using: .utf8)!
|
||||
)
|
||||
|
||||
expect(subResponse).toNot(beNil())
|
||||
expect(subResponse?.body).to(beNil())
|
||||
expect(subResponse?.failedToParseBody).to(beFalse())
|
||||
}
|
||||
|
||||
it("does not flag a NoResponse body as invalid") {
|
||||
let jsonString: String = """
|
||||
{
|
||||
"code": 200,
|
||||
"headers": {
|
||||
"testKey": "testValue"
|
||||
},
|
||||
}
|
||||
"""
|
||||
let subResponse: HTTP.BatchSubResponse<NoResponse>? = try? JSONDecoder().decode(
|
||||
HTTP.BatchSubResponse<NoResponse>.self,
|
||||
from: jsonString.data(using: .utf8)!
|
||||
)
|
||||
|
||||
expect(subResponse).toNot(beNil())
|
||||
expect(subResponse?.body).to(beNil())
|
||||
expect(subResponse?.failedToParseBody).to(beFalse())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
// MARK: --Decodable
|
||||
|
||||
describe("a Decodable") {
|
||||
it("decodes correctly") {
|
||||
let jsonData: Data = "{\"stringValue\":\"testValue\"}".data(using: .utf8)!
|
||||
let result: TestType? = try? TestType.decoded(from: jsonData)
|
||||
|
||||
expect(result).to(equal(TestType(stringValue: "testValue")))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - --Promise
|
||||
|
||||
describe("a (ResponseInfoType, Data?) Promise") {
|
||||
var responseInfo: ResponseInfoType!
|
||||
var testType: TestType!
|
||||
var testType2: TestType2!
|
||||
var data: Data!
|
||||
|
||||
beforeEach {
|
||||
responseInfo = HTTP.ResponseInfo(code: 200, headers: [:])
|
||||
testType = TestType(stringValue: "Test")
|
||||
testType2 = TestType2(intValue: 1, stringValue2: "Test2")
|
||||
data = """
|
||||
[\([
|
||||
try! JSONEncoder().encode(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: testType,
|
||||
failedToParseBody: false
|
||||
)
|
||||
),
|
||||
try! JSONEncoder().encode(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: testType2,
|
||||
failedToParseBody: false
|
||||
)
|
||||
)
|
||||
]
|
||||
.map { String(data: $0, encoding: .utf8)! }
|
||||
.joined(separator: ","))]
|
||||
""".data(using: .utf8)!
|
||||
}
|
||||
|
||||
it("decodes valid data correctly") {
|
||||
let result = Promise.value((responseInfo, data))
|
||||
.decoded(as: [
|
||||
HTTP.BatchSubResponse<TestType>.self,
|
||||
HTTP.BatchSubResponse<TestType2>.self
|
||||
])
|
||||
|
||||
expect(result.value).toNot(beNil())
|
||||
expect((result.value?[0].1 as? HTTP.BatchSubResponse<TestType>)?.body)
|
||||
.to(equal(testType))
|
||||
expect((result.value?[1].1 as? HTTP.BatchSubResponse<TestType2>)?.body)
|
||||
.to(equal(testType2))
|
||||
}
|
||||
|
||||
it("fails if there is no data") {
|
||||
let result = Promise.value((responseInfo, nil)).decoded(as: [])
|
||||
|
||||
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
|
||||
}
|
||||
|
||||
it("fails if the data is not JSON") {
|
||||
let result = Promise.value((responseInfo, Data([1, 2, 3]))).decoded(as: [])
|
||||
|
||||
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
|
||||
}
|
||||
|
||||
it("fails if the data is not a JSON array") {
|
||||
let result = Promise.value((responseInfo, "{}".data(using: .utf8))).decoded(as: [])
|
||||
|
||||
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
|
||||
}
|
||||
|
||||
it("fails if the JSON array does not have the same number of items as the expected types") {
|
||||
let result = Promise.value((responseInfo, data))
|
||||
.decoded(as: [
|
||||
HTTP.BatchSubResponse<TestType>.self,
|
||||
HTTP.BatchSubResponse<TestType2>.self,
|
||||
HTTP.BatchSubResponse<TestType2>.self
|
||||
])
|
||||
|
||||
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
|
||||
}
|
||||
|
||||
it("fails if one of the JSON array values fails to decode") {
|
||||
data = """
|
||||
[\([
|
||||
try! JSONEncoder().encode(
|
||||
HTTP.BatchSubResponse(
|
||||
code: 200,
|
||||
headers: [:],
|
||||
body: testType,
|
||||
failedToParseBody: false
|
||||
)
|
||||
)
|
||||
]
|
||||
.map { String(data: $0, encoding: .utf8)! }
|
||||
.joined(separator: ",")),{"test": "test"}]
|
||||
""".data(using: .utf8)!
|
||||
let result = Promise.value((responseInfo, data))
|
||||
.decoded(as: [
|
||||
HTTP.BatchSubResponse<TestType>.self,
|
||||
HTTP.BatchSubResponse<TestType2>.self
|
||||
])
|
||||
|
||||
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ import Foundation
|
|||
import Quick
|
||||
import Nimble
|
||||
|
||||
@testable import SessionMessagingKit
|
||||
@testable import SessionUtilitiesKit
|
||||
|
||||
class HeaderSpec: QuickSpec {
|
||||
// MARK: - Spec
|
||||
|
@ -13,7 +13,8 @@ class HeaderSpec: QuickSpec {
|
|||
override func spec() {
|
||||
describe("a Dictionary of Header to String values") {
|
||||
it("can be converted into a dictionary of String to String values") {
|
||||
expect([Header.authorization: "test"].toHTTPHeaders()).to(equal(["Authorization": "test"]))
|
||||
expect([HTTPHeader.authorization: "test"].toHTTPHeaders())
|
||||
.to(equal(["Authorization": "test"]))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,11 +4,22 @@ import Foundation
|
|||
|
||||
import Quick
|
||||
import Nimble
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@testable import SessionMessagingKit
|
||||
@testable import SessionUtilitiesKit
|
||||
|
||||
class RequestSpec: QuickSpec {
|
||||
enum TestEndpoint: EndpointType {
|
||||
case test1
|
||||
case testParams(String, Int)
|
||||
|
||||
var path: String {
|
||||
switch self {
|
||||
case .test1: return "test1"
|
||||
case .testParams(let str, let int): return "testParams/\(str)/int/\(int)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TestType: Codable, Equatable {
|
||||
let stringValue: String
|
||||
}
|
||||
|
@ -18,9 +29,9 @@ class RequestSpec: QuickSpec {
|
|||
override func spec() {
|
||||
describe("a Request") {
|
||||
it("is initialized with the correct default values") {
|
||||
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
|
||||
let request: Request<NoBody, TestEndpoint> = Request(
|
||||
server: "testServer",
|
||||
endpoint: .batch
|
||||
endpoint: .test1
|
||||
)
|
||||
|
||||
expect(request.method.rawValue).to(equal("GET"))
|
||||
|
@ -31,42 +42,42 @@ class RequestSpec: QuickSpec {
|
|||
|
||||
context("when generating a URL") {
|
||||
it("adds a leading forward slash to the endpoint path") {
|
||||
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
|
||||
let request: Request<NoBody, TestEndpoint> = Request(
|
||||
server: "testServer",
|
||||
endpoint: .batch
|
||||
endpoint: .test1
|
||||
)
|
||||
|
||||
expect(request.urlPathAndParamsString).to(equal("/batch"))
|
||||
expect(request.urlPathAndParamsString).to(equal("/test1"))
|
||||
}
|
||||
|
||||
it("creates a valid URL with no query parameters") {
|
||||
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
|
||||
let request: Request<NoBody, TestEndpoint> = Request(
|
||||
server: "testServer",
|
||||
endpoint: .batch
|
||||
endpoint: .test1
|
||||
)
|
||||
|
||||
expect(request.urlPathAndParamsString).to(equal("/batch"))
|
||||
expect(request.urlPathAndParamsString).to(equal("/test1"))
|
||||
}
|
||||
|
||||
it("creates a valid URL when query parameters are provided") {
|
||||
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
|
||||
let request: Request<NoBody, TestEndpoint> = Request(
|
||||
server: "testServer",
|
||||
endpoint: .batch,
|
||||
endpoint: .test1,
|
||||
queryParameters: [
|
||||
.limit: "123"
|
||||
]
|
||||
)
|
||||
|
||||
expect(request.urlPathAndParamsString).to(equal("/batch?limit=123"))
|
||||
expect(request.urlPathAndParamsString).to(equal("/test1?limit=123"))
|
||||
}
|
||||
}
|
||||
|
||||
context("when generating a URLRequest") {
|
||||
it("sets all the values correctly") {
|
||||
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
|
||||
let request: Request<NoBody, TestEndpoint> = Request(
|
||||
method: .delete,
|
||||
server: "testServer",
|
||||
endpoint: .batch,
|
||||
endpoint: .test1,
|
||||
headers: [
|
||||
.authorization: "test"
|
||||
]
|
||||
|
@ -79,22 +90,22 @@ class RequestSpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("throws an error if the URL is invalid") {
|
||||
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
|
||||
let request: Request<NoBody, TestEndpoint> = Request(
|
||||
server: "testServer",
|
||||
endpoint: .roomPollInfo("!!%%", 123)
|
||||
endpoint: .testParams("!!%%", 123)
|
||||
)
|
||||
|
||||
expect {
|
||||
try request.generateUrlRequest()
|
||||
}
|
||||
.to(throwError(HTTP.Error.invalidURL))
|
||||
.to(throwError(HTTPError.invalidURL))
|
||||
}
|
||||
|
||||
context("with a base64 string body") {
|
||||
it("successfully encodes the body") {
|
||||
let request: Request<String, OpenGroupAPI.Endpoint> = Request(
|
||||
let request: Request<String, TestEndpoint> = Request(
|
||||
server: "testServer",
|
||||
endpoint: .batch,
|
||||
endpoint: .test1,
|
||||
body: "TestMessage".data(using: .utf8)!.base64EncodedString()
|
||||
)
|
||||
|
||||
|
@ -106,24 +117,24 @@ class RequestSpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("throws an error if the body is not base64 encoded") {
|
||||
let request: Request<String, OpenGroupAPI.Endpoint> = Request(
|
||||
let request: Request<String, TestEndpoint> = Request(
|
||||
server: "testServer",
|
||||
endpoint: .batch,
|
||||
endpoint: .test1,
|
||||
body: "TestMessage"
|
||||
)
|
||||
|
||||
expect {
|
||||
try request.generateUrlRequest()
|
||||
}
|
||||
.to(throwError(HTTP.Error.parsingFailed))
|
||||
.to(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
}
|
||||
|
||||
context("with a byte body") {
|
||||
it("successfully encodes the body") {
|
||||
let request: Request<[UInt8], OpenGroupAPI.Endpoint> = Request(
|
||||
let request: Request<[UInt8], TestEndpoint> = Request(
|
||||
server: "testServer",
|
||||
endpoint: .batch,
|
||||
endpoint: .test1,
|
||||
body: [1, 2, 3]
|
||||
)
|
||||
|
||||
|
@ -135,9 +146,9 @@ class RequestSpec: QuickSpec {
|
|||
|
||||
context("with a JSON body") {
|
||||
it("successfully encodes the body") {
|
||||
let request: Request<TestType, OpenGroupAPI.Endpoint> = Request(
|
||||
let request: Request<TestType, TestEndpoint> = Request(
|
||||
server: "testServer",
|
||||
endpoint: .batch,
|
||||
endpoint: .test1,
|
||||
body: TestType(stringValue: "test")
|
||||
)
|
||||
|
||||
|
@ -151,9 +162,9 @@ class RequestSpec: QuickSpec {
|
|||
}
|
||||
|
||||
it("successfully encodes no body") {
|
||||
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
|
||||
let request: Request<NoBody, TestEndpoint> = Request(
|
||||
server: "testServer",
|
||||
endpoint: .batch,
|
||||
endpoint: .test1,
|
||||
body: nil
|
||||
)
|
||||
|
Loading…
Reference in New Issue