diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index e61bc81a7..34af08cf0 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -773,6 +773,10 @@ F5765D284BC6ECAC0C1D33F0 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4A93ECA93B3DE800CC7D7F6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; }; FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; }; FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; }; + FD078E4627E02406000769AF /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383D27B4708600C60D73 /* Atomic.swift */; }; + FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4727E02561000769AF /* CommonMockedExtensions.swift */; }; + FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4727E02561000769AF /* CommonMockedExtensions.swift */; }; + FD078E4B27E02C5D000769AF /* Failable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4A27E02C5D000769AF /* Failable.swift */; }; FD0BA51B27CD88EC00CC6805 /* BlindedIdMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */; }; FD0BA51D27CDC34600CC6805 /* SOGSV4Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */; }; FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; }; @@ -797,7 +801,7 @@ FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */; }; FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */; }; FD83B9CE27D17A04005E1583 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CD27D17A04005E1583 /* Request.swift */; }; - FD83B9D227D59495005E1583 /* TestUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* TestUserDefaults.swift */; }; + FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FD83B9D427D5A7D5005E1583 /* ConversationViewItem+Refactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D327D5A7D5005E1583 /* ConversationViewItem+Refactor.swift */; }; FD859EF227BF6BA200510D0C /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; }; FD859EF427C2F49200510D0C /* MockSodium.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF327C2F49200510D0C /* MockSodium.swift */; }; @@ -826,8 +830,10 @@ FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A527D860CE005DAE71 /* Mock.swift */; }; - FDC290AC27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290AB27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift */; }; - FDC290AD27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290AB27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift */; }; + FDC290AC27DB0B1C005DAE71 /* MockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290AB27DB0B1C005DAE71 /* MockedExtensions.swift */; }; + FDC290AF27DFEE97005DAE71 /* TestTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290AE27DFEE97005DAE71 /* TestTransaction.swift */; }; + FDC290B327DFF9F5005DAE71 /* TestOnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290B227DFF9F5005DAE71 /* TestOnionRequestAPI.swift */; }; + FDC290B727E00FDB005DAE71 /* TestGroupThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290B627E00FDB005DAE71 /* TestGroupThread.swift */; }; FDC4380927B31D4E00C60D73 /* SOGSError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* SOGSError.swift */; }; FDC4381527B329CE00C60D73 /* NonceGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381427B329CE00C60D73 /* NonceGenerator.swift */; }; FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; }; @@ -835,7 +841,6 @@ FDC4382F27B383AF00C60D73 /* UnregisterResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* UnregisterResponse.swift */; }; FDC4383127B3841C00C60D73 /* RegisterResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383027B3841C00C60D73 /* RegisterResponse.swift */; }; FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* VersionResponse.swift */; }; - FDC4383E27B4708600C60D73 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383D27B4708600C60D73 /* Atomic.swift */; }; FDC4384C27B47F7700C60D73 /* OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384B27B47F7700C60D73 /* OpenGroup.swift */; }; FDC4384F27B4804F00C60D73 /* Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384E27B4804F00C60D73 /* Header.swift */; }; FDC4385127B4807400C60D73 /* QueryParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385027B4807400C60D73 /* QueryParam.swift */; }; @@ -1913,6 +1918,8 @@ F9BBF530D71905BA9007675F /* Pods-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionShareExtension/Pods-SessionShareExtension.debug.xcconfig"; sourceTree = ""; }; FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; }; FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + FD078E4727E02561000769AF /* CommonMockedExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonMockedExtensions.swift; sourceTree = ""; }; + FD078E4A27E02C5D000769AF /* Failable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Failable.swift; sourceTree = ""; }; FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdMapping.swift; sourceTree = ""; }; FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSV4Migration.swift; sourceTree = ""; }; FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = ""; }; @@ -1936,7 +1943,7 @@ FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageResponse.swift; sourceTree = ""; }; FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSEndpoint.swift; sourceTree = ""; }; FD83B9CD27D17A04005E1583 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; - FD83B9D127D59495005E1583 /* TestUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUserDefaults.swift; sourceTree = ""; }; + FD83B9D127D59495005E1583 /* MockUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDefaults.swift; sourceTree = ""; }; FD83B9D327D5A7D5005E1583 /* ConversationViewItem+Refactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationViewItem+Refactor.swift"; sourceTree = ""; }; FD859EEF27BF207700510D0C /* SessionProtos.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = SessionProtos.proto; sourceTree = ""; }; FD859EF027BF207C00510D0C /* WebSocketResources.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = WebSocketResources.proto; sourceTree = ""; }; @@ -1966,7 +1973,10 @@ FDC290A127D85890005DAE71 /* TestInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestInteraction.swift; sourceTree = ""; }; FDC290A527D860CE005DAE71 /* Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mock.swift; sourceTree = ""; }; FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbleExtensions.swift; sourceTree = ""; }; - FDC290AB27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BoxKeyPair+Mocked.swift"; sourceTree = ""; }; + FDC290AB27DB0B1C005DAE71 /* MockedExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockedExtensions.swift; sourceTree = ""; }; + FDC290AE27DFEE97005DAE71 /* TestTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransaction.swift; sourceTree = ""; }; + FDC290B227DFF9F5005DAE71 /* TestOnionRequestAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestOnionRequestAPI.swift; sourceTree = ""; }; + FDC290B627E00FDB005DAE71 /* TestGroupThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGroupThread.swift; sourceTree = ""; }; FDC4380827B31D4E00C60D73 /* SOGSError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSError.swift; sourceTree = ""; }; FDC4381427B329CE00C60D73 /* NonceGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceGenerator.swift; sourceTree = ""; }; FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = ""; }; @@ -2542,6 +2552,7 @@ C33FDB8A255A581200E217F9 /* AppContext.h */, C33FDB85255A581100E217F9 /* AppContext.m */, C3C2A5D12553860800C340D1 /* Array+Utilities.swift */, + FDC4383D27B4708600C60D73 /* Atomic.swift */, FDC438CC27BC641200C60D73 /* Set+Utilities.swift */, C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */, B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */, @@ -3422,13 +3433,13 @@ children = ( C33FDB01255A580700E217F9 /* AppReadiness.h */, C33FDB75255A581000E217F9 /* AppReadiness.m */, - FDC4383D27B4708600C60D73 /* Atomic.swift */, FD83B9A927CF149D005E1583 /* ContactUtilities.swift */, FD859EF127BF6BA200510D0C /* Data+Utilities.swift */, FDC438C027BB4E6800C60D73 /* Dependencies.swift */, C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */, C37F53E8255BA9BB002AEA92 /* Environment.h */, C37F5402255BA9ED002AEA92 /* Environment.m */, + FD078E4A27E02C5D000769AF /* Failable.swift */, C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */, C33FDB7F255A581100E217F9 /* FullTextSearchFinder.swift */, C33FDBC1255A581700E217F9 /* General.swift */, @@ -3886,6 +3897,7 @@ FDC290A527D860CE005DAE71 /* Mock.swift */, FD83B9BD27CF2243005E1583 /* TestConstants.swift */, FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */, + FD078E4727E02561000769AF /* CommonMockedExtensions.swift */, ); path = SharedTest; sourceTree = ""; @@ -4035,10 +4047,13 @@ FD859EF727C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift */, FD859EF927C2F5C500510D0C /* MockGenericHash.swift */, FD859EFB27C2F60700510D0C /* MockEd25519.swift */, - FD83B9D127D59495005E1583 /* TestUserDefaults.swift */, + FD83B9D127D59495005E1583 /* MockUserDefaults.swift */, + FDC290B227DFF9F5005DAE71 /* TestOnionRequestAPI.swift */, + FDC290AE27DFEE97005DAE71 /* TestTransaction.swift */, FDC2909F27D85826005DAE71 /* TestThread.swift */, + FDC290B627E00FDB005DAE71 /* TestGroupThread.swift */, FDC290A127D85890005DAE71 /* TestInteraction.swift */, - FDC290AB27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift */, + FDC290AB27DB0B1C005DAE71 /* MockedExtensions.swift */, ); path = _TestUtilities; sourceTree = ""; @@ -5241,6 +5256,7 @@ B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */, C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */, B8BC00C0257D90E30032E807 /* General.swift in Sources */, + FD078E4627E02406000769AF /* Atomic.swift in Sources */, FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */, C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */, C3D9E41F25676C870040E4F3 /* OWSPrimaryStorageProtocol.swift in Sources */, @@ -5297,7 +5313,6 @@ C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */, 7B1581E2271E743B00848B49 /* OWSSounds.swift in Sources */, FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */, - FDC4383E27B4708600C60D73 /* Atomic.swift in Sources */, C32C5A76256DBBCF003C73A2 /* SignalAttachment.swift in Sources */, C32C5CA4256DD1DC003C73A2 /* TSAccountManager.m in Sources */, C352A3892557876500338F3E /* JobQueue.swift in Sources */, @@ -5400,6 +5415,7 @@ FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */, C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */, FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */, + FD078E4B27E02C5D000769AF /* Failable.swift in Sources */, B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */, C3DB66C3260ACCE6001EFC55 /* OpenGroupPoller.swift in Sources */, C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */, @@ -5619,7 +5635,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FDC290AD27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift in Sources */, + FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */, FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */, FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */, FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, @@ -5631,9 +5647,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FDC290AC27DB0B1C005DAE71 /* BoxKeyPair+Mocked.swift in Sources */, + FDC290AC27DB0B1C005DAE71 /* MockedExtensions.swift in Sources */, FD859EFA27C2F5C500510D0C /* MockGenericHash.swift in Sources */, FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */, + FDC290AF27DFEE97005DAE71 /* TestTransaction.swift in Sources */, + FDC290B327DFF9F5005DAE71 /* TestOnionRequestAPI.swift in Sources */, FDC2909127D709CA005DAE71 /* SOGSMessageSpec.swift in Sources */, FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */, FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */, @@ -5641,6 +5659,7 @@ FD83B9C327CF33F7005E1583 /* ServerSpec.swift in Sources */, FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */, FDC2909A27D71376005DAE71 /* NonceGeneratorSpec.swift in Sources */, + FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */, FDC290A027D85826005DAE71 /* TestThread.swift in Sources */, FD859EF827C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift in Sources */, FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */, @@ -5652,6 +5671,7 @@ FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */, FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */, FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, + FDC290B727E00FDB005DAE71 /* TestGroupThread.swift in Sources */, FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */, FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */, FD859EF627C2F52C00510D0C /* MockSign.swift in Sources */, @@ -5660,7 +5680,7 @@ FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */, FDC290A227D85890005DAE71 /* TestInteraction.swift in Sources */, FDC4389D27BA01F000C60D73 /* MockStorage.swift in Sources */, - FD83B9D227D59495005E1583 /* TestUserDefaults.swift in Sources */, + FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index c489ce3fc..4e5071515 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -815,11 +815,11 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc let publicKey = message.authorId guard let openGroup = Storage.shared.getOpenGroup(for: threadID) else { return } - let promise = OpenGroupAPI.userBanAndDeleteAllMessage(publicKey, from: [openGroup.room], on: openGroup.server) + let promise = OpenGroupAPI.userBanAndDeleteAllMessages(publicKey, in: openGroup.room, on: openGroup.server) promise.catch(on: DispatchQueue.main) { _ in OWSAlerts.showErrorAlert(message: NSLocalizedString("context_menu_ban_user_error_alert_message", comment: "")) } - promise.retainUntilComplete() // TODO: Test This + promise.retainUntilComplete() self?.becomeFirstResponder() })) alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { [weak self] _ in diff --git a/SessionMessagingKit/Common Networking/Request.swift b/SessionMessagingKit/Common Networking/Request.swift index a5dff15a7..19130eb98 100644 --- a/SessionMessagingKit/Common Networking/Request.swift +++ b/SessionMessagingKit/Common Networking/Request.swift @@ -25,10 +25,6 @@ struct Request { /// **Warning:** The `bodyData` value should be used to when making the actual request instead of this as there /// is custom handling for certain data types let body: T? - let isAuthRequired: Bool - /// Always `true` under normal circumstances. You might want to disable - /// this when running over Lokinet. - let useOnionRouting: Bool // MARK: - Initialization @@ -38,9 +34,7 @@ struct Request { endpoint: Endpoint, queryParameters: [QueryParam: String] = [:], headers: [Header: String] = [:], - body: T? = nil, - isAuthRequired: Bool = true, - useOnionRouting: Bool = true + body: T? = nil ) { self.method = method self.server = server @@ -48,8 +42,6 @@ struct Request { self.queryParameters = queryParameters self.headers = headers self.body = body - self.isAuthRequired = isAuthRequired - self.useOnionRouting = useOnionRouting } // MARK: - Internal Methods diff --git a/SessionMessagingKit/File Server/FileServerAPI.swift b/SessionMessagingKit/File Server/FileServerAPI.swift index 012ce514e..493af02f2 100644 --- a/SessionMessagingKit/File Server/FileServerAPI.swift +++ b/SessionMessagingKit/File Server/FileServerAPI.swift @@ -75,10 +75,6 @@ public final class FileServerAPI: NSObject { // MARK: - Convenience private static func send(_ request: Request, serverPublicKey: String) -> Promise { - guard request.useOnionRouting else { - preconditionFailure("It's currently not allowed to send non onion routed requests.") - } - let urlRequest: URLRequest do { diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 953b14911..638f5be44 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -76,7 +76,7 @@ public enum OpenGroupAPI { .roomMessagesSince(openGroup.room, seqNo: targetSeqNo) ) ), - responseType: [Message].self + responseType: [Failable].self ) ] } @@ -242,7 +242,7 @@ public enum OpenGroupAPI { for roomToken: String, on server: String, using dependencies: Dependencies = Dependencies() - ) -> Promise<(capabilities: (OnionRequestResponseInfoType, Capabilities?), room: (OnionRequestResponseInfoType, Room?))> { + ) -> Promise<(capabilities: (info: OnionRequestResponseInfoType, data: Capabilities), room: (info: OnionRequestResponseInfoType, data: Room))> { let requestResponseType: [BatchRequestInfoType] = [ // Get the latest capabilities for the server (in case it's a new server or the cached ones are stale) BatchRequestInfo( @@ -264,8 +264,8 @@ public enum OpenGroupAPI { ] return sequence(server, requests: requestResponseType, using: dependencies) - .map { (response: [Endpoint: (OnionRequestResponseInfoType, Codable?)]) -> (capabilities: (OnionRequestResponseInfoType, Capabilities?), room: (OnionRequestResponseInfoType, Room?)) in - let maybeCapabilities: (OnionRequestResponseInfoType, Capabilities?)? = response[.capabilities] + .map { (response: [Endpoint: (OnionRequestResponseInfoType, Codable?)]) -> (capabilities: (OnionRequestResponseInfoType, Capabilities), room: (OnionRequestResponseInfoType, Room)) in + let maybeCapabilities: (info: OnionRequestResponseInfoType, data: Capabilities?)? = response[.capabilities] .map { info, data in (info, (data as? BatchSubResponse)?.body) } let maybeRoomResponse: (OnionRequestResponseInfoType, Codable?)? = response .first(where: { key, _ in @@ -275,14 +275,22 @@ public enum OpenGroupAPI { } }) .map { _, value in value } - let maybeRoom: (OnionRequestResponseInfoType, Room?)? = maybeRoomResponse + let maybeRoom: (info: OnionRequestResponseInfoType, data: Room?)? = maybeRoomResponse .map { info, data in (info, (data as? BatchSubResponse)?.body) } - guard let capabilities: (OnionRequestResponseInfoType, Capabilities?) = maybeCapabilities, let room: (OnionRequestResponseInfoType, Room?) = maybeRoom else { + guard + let capabilitiesInfo: OnionRequestResponseInfoType = maybeCapabilities?.info, + let capabilities: Capabilities = maybeCapabilities?.data, + let roomInfo: OnionRequestResponseInfoType = maybeRoom?.info, + let room: Room = maybeRoom?.data + else { throw HTTP.Error.parsingFailed } - return (capabilities, room) + return ( + (capabilitiesInfo, capabilities), + (roomInfo, room) + ) } } @@ -298,7 +306,7 @@ public enum OpenGroupAPI { fileIds: [String]?, using dependencies: Dependencies = Dependencies() ) -> Promise<(OnionRequestResponseInfoType, Message)> { - guard let signResult: (publicKey: String, signature: Bytes) = sign(plaintext.bytes, for: server, using: dependencies) else { + guard let signResult: (publicKey: String, signature: Bytes) = sign(plaintext.bytes, for: server, fallbackSigningType: .standard, using: dependencies) else { return Promise(error: Error.signingFailed) } @@ -352,7 +360,7 @@ public enum OpenGroupAPI { on server: String, using dependencies: Dependencies = Dependencies() ) -> Promise<(OnionRequestResponseInfoType, Data?)> { - guard let signResult: (publicKey: String, signature: Bytes) = sign(plaintext.bytes, for: server, using: dependencies) else { + guard let signResult: (publicKey: String, signature: Bytes) = sign(plaintext.bytes, for: server, fallbackSigningType: .standard, using: dependencies) else { return Promise(error: Error.signingFailed) } @@ -429,6 +437,34 @@ public enum OpenGroupAPI { .decoded(as: [Message].self, on: OpenGroupAPI.workQueue, using: dependencies) } + /// Deletes all messages from a given sessionId within the provided rooms (or globally) on a server + /// + /// - Parameters: + /// - sessionId: The sessionId (either standard or blinded) of the user whose messages should be deleted + /// + /// - roomToken: The room token from which the messages should be deleted + /// + /// The invoking user **must** be a moderator of the given room or an admin if trying to delete the messages + /// of another admin. + /// + /// - server: The server to delete messages from + /// + /// - dependencies: Injected dependencies (used for unit testing) + public static func messagesDeleteAll( + _ sessionId: String, + in roomToken: String, + on server: String, + using dependencies: Dependencies = Dependencies() + ) -> Promise<(OnionRequestResponseInfoType, Data?)> { + let request: Request = Request( + method: .delete, + server: server, + endpoint: Endpoint.roomDeleteMessages(roomToken, sessionId: sessionId) + ) + + return send(request, using: dependencies) + } + // MARK: - Pinning /// Adds a pinned message to this room @@ -791,65 +827,19 @@ public enum OpenGroupAPI { return send(request, using: dependencies) } - // TODO: Need to test this once the API has been implemented - // TODO: Update docs to align with the API documentation once implemented - /// Deletes all messages from a given sessionId within the provided rooms (or globally) on a server - /// - /// - Parameters: - /// - sessionId: The sessionId (either standard or blinded) of the user whose messages should be deleted - /// - /// - roomTokens: List of one or more room tokens from which the messages should be deleted - /// - /// The invoking user **must** be an admin of all of the given rooms. - /// - /// This may be set to the single-element list `["*"]` to add or remove the moderator from all rooms in which the current user has admin - /// permissions (the call will succeed if the calling user is an admin in at least one channel) - /// - /// **Note:** You can delete messages from all rooms on a server by providing a `nil` value for this parameter - /// - /// - server: The server to delete messages from - /// - /// - dependencies: Injected dependencies (used for unit testing) - public static func userDeleteMessages( - _ sessionId: String, - from roomTokens: [String]?, - on server: String, - using dependencies: Dependencies = Dependencies() - ) -> Promise<(OnionRequestResponseInfoType, UserDeleteMessagesResponse)> { - let requestBody: UserDeleteMessagesRequest = UserDeleteMessagesRequest( - rooms: roomTokens, - global: (roomTokens == nil ? true : nil) - ) - - let request: Request = Request( - method: .post, - server: server, - endpoint: Endpoint.userDeleteMessages(sessionId), - body: requestBody - ) - - return send(request, using: dependencies) - .decoded(as: UserDeleteMessagesResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) - } - - // TODO: Need to test this once the API has been implemented /// This is a convenience method which constructs a `/sequence` of the `userBan` and `userDeleteMessages` requests, refer to those /// methods for the documented behaviour of each method - public static func userBanAndDeleteAllMessage( + public static func userBanAndDeleteAllMessages( _ sessionId: String, - from roomTokens: [String]?, + in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies() ) -> Promise<[OnionRequestResponseInfoType]> { let banRequestBody: UserBanRequest = UserBanRequest( - rooms: roomTokens, - global: (roomTokens == nil ? true : nil), + rooms: [roomToken], + global: nil, timeout: nil ) - let deleteMessageRequestBody: UserDeleteMessagesRequest = UserDeleteMessagesRequest( - rooms: roomTokens, - global: (roomTokens == nil ? true : nil) - ) // Generate the requests let requestResponseType: [BatchRequestInfoType] = [ @@ -862,27 +852,22 @@ public enum OpenGroupAPI { ) ), BatchRequestInfo( - request: Request( - method: .post, + request: Request( + method: .delete, server: server, - endpoint: .userDeleteMessages(sessionId), - body: deleteMessageRequestBody - ), - responseType: UserDeleteMessagesResponse.self + endpoint: Endpoint.roomDeleteMessages(roomToken, sessionId: sessionId) + ) ) ] return sequence(server, requests: requestResponseType, using: dependencies) - .map { results in - // TODO: Handle deletions...???? Hand off to OpenGroupAPIManager? - return results.values.map { responseInfo, _ in responseInfo } - } + .map { $0.values.map { responseInfo, _ in responseInfo } } } // MARK: - Authentication /// Sign a message to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities) - private static func sign(_ messageBytes: Bytes, for serverName: String, using dependencies: Dependencies = Dependencies()) -> (publicKey: String, signature: Bytes)? { + private static func sign(_ messageBytes: Bytes, for serverName: String, fallbackSigningType signingType: SessionId.Prefix, using dependencies: Dependencies = Dependencies()) -> (publicKey: String, signature: Bytes)? { guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { return nil } guard let serverPublicKey: String = dependencies.storage.getOpenGroupPublicKey(for: serverName) else { return nil @@ -906,15 +891,30 @@ public enum OpenGroupAPI { ) } - // Otherwise fall back to sign using the unblinded key - guard let signatureResult: Bytes = dependencies.sign.signature(message: messageBytes, secretKey: userEdKeyPair.secretKey) else { - return nil + // Otherwise sign using the fallback type + switch signingType { + case .unblinded: + guard let signatureResult: Bytes = dependencies.sign.signature(message: messageBytes, secretKey: userEdKeyPair.secretKey) else { + return nil + } + + return ( + publicKey: SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString, + signature: signatureResult + ) + + // Default to using the 'standard' key + default: + guard let userKeyPair: ECKeyPair = dependencies.storage.getUserKeyPair() else { return nil } + guard let signatureResult: Bytes = try? dependencies.ed25519.sign(data: messageBytes, keyPair: userKeyPair) else { + return nil + } + + return ( + publicKey: SessionId(.standard, publicKey: userKeyPair.publicKey.bytes).hexString, + signature: signatureResult + ) } - - return ( - publicKey: SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString, - signature: signatureResult - ) } /// Sign a request to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities) @@ -954,7 +954,7 @@ public enum OpenGroupAPI { .appending(bodyHash ?? []) /// Sign the above message - guard let signResult: (publicKey: String, signature: Bytes) = sign(messageBytes, for: serverName, using: dependencies) else { + guard let signResult: (publicKey: String, signature: Bytes) = sign(messageBytes, for: serverName, fallbackSigningType: .unblinded, using: dependencies) else { return nil } @@ -981,23 +981,15 @@ public enum OpenGroupAPI { return Promise(error: error) } - if request.useOnionRouting { - guard let publicKey = dependencies.storage.getOpenGroupPublicKey(for: request.server) else { - return Promise(error: Error.noPublicKey) - } - - if request.isAuthRequired { - // Attempt to sign the request with the new auth - guard let signedRequest: URLRequest = sign(urlRequest, for: request.server, with: publicKey, using: dependencies) else { - return Promise(error: Error.signingFailed) - } - - return dependencies.api.sendOnionRequest(signedRequest, to: request.server, with: publicKey) - } - - return dependencies.api.sendOnionRequest(urlRequest, to: request.server, with: publicKey) + guard let publicKey = dependencies.storage.getOpenGroupPublicKey(for: request.server) else { + return Promise(error: Error.noPublicKey) } - preconditionFailure("It's currently not allowed to send non onion routed requests.") + // Attempt to sign the request with the new auth + guard let signedRequest: URLRequest = sign(urlRequest, for: request.server, with: publicKey, using: dependencies) else { + return Promise(error: Error.signingFailed) + } + + return dependencies.onionApi.sendOnionRequest(signedRequest, to: request.server, with: publicKey) } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 8e096e5fb..38de61a63 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -9,6 +9,9 @@ public final class OpenGroupManager: NSObject { public var defaultRoomsPromise: Promise<[OpenGroupAPI.Room]>? fileprivate var groupImagePromises: [String: Promise] = [:] + public var pollers: [String: OpenGroupAPI.Poller] = [:] // One for each server + public var isPolling: Bool = false + /// Server URL to room ID to set of user IDs fileprivate var moderators: [String: [String: Set]] = [:] fileprivate var admins: [String: [String: Set]] = [:] @@ -40,29 +43,31 @@ public final class OpenGroupManager: NSObject { public let mutableCache: Atomic = Atomic(Cache()) public var cache: Cache { return mutableCache.wrappedValue } - private var pollers: [String: OpenGroupAPI.Poller] = [:] // One for each server - private var isPolling = false - // MARK: - Polling - - @objc public func startPolling() { - guard !isPolling else { return } + + public func startPolling(using dependencies: Dependencies = Dependencies()) { + guard !cache.isPolling else { return } - isPolling = true - pollers = Set(Storage.shared.getAllOpenGroups().values.map { $0.server }) - .reduce(into: [:]) { prev, server in - pollers[server]?.stop() // Should never occur - - let poller = OpenGroupAPI.Poller(for: server) - poller.startIfNeeded() - - prev[server] = poller - } + mutableCache.mutate { cache in + cache.isPolling = true + cache.pollers = Set(dependencies.storage.getAllOpenGroups().values.map { openGroup in openGroup.server }) + .reduce(into: [:]) { prev, server in + cache.pollers[server]?.stop() // Should never occur + + let poller = OpenGroupAPI.Poller(for: server) + poller.startIfNeeded(using: dependencies) + + prev[server] = poller + } + } } @objc public func stopPolling() { - pollers.forEach { (_, openGroupPoller) in openGroupPoller.stop() } - pollers.removeAll() + mutableCache.mutate { + $0.pollers.forEach { (_, openGroupPoller) in openGroupPoller.stop() } + $0.pollers.removeAll() + $0.isPolling = false + } } // MARK: - Adding & Removing @@ -71,7 +76,7 @@ public final class OpenGroupManager: NSObject { // If we are currently polling for this server and already have a TSGroupThread for this room the do nothing let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData("\(server).\(roomToken)") - if OpenGroupManager.shared.pollers[server] != nil && TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupId), transaction: transaction) != nil { + if OpenGroupManager.shared.cache.pollers[server] != nil && TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupId), transaction: transaction) != nil { SNLog("Ignoring join open group attempt (already joined), user initiated: \(!isConfigMessage)") return Promise.value(()) } @@ -86,19 +91,13 @@ public final class OpenGroupManager: NSObject { transaction.addCompletionQueue(DispatchQueue.global(qos: .userInitiated)) { OpenGroupAPI.capabilitiesAndRoom(for: roomToken, on: server, using: dependencies) - .done(on: DispatchQueue.global(qos: .userInitiated)) { (capabilitiesResponse: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), roomResponse: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?)) in - guard let capabilities: OpenGroupAPI.Capabilities = capabilitiesResponse.data, let room: OpenGroupAPI.Room = roomResponse.data else { - SNLog("Failed to join open group due to invalid data.") - seal.reject(HTTP.Error.generic) - return - } - - dependencies.storage.write { anyTransactionas in - guard let transaction: YapDatabaseReadWriteTransaction = anyTransactionas as? YapDatabaseReadWriteTransaction else { return } + .done(on: DispatchQueue.global(qos: .userInitiated)) { response in + dependencies.storage.write { anyTransaction in + guard let transaction: YapDatabaseReadWriteTransaction = anyTransaction as? YapDatabaseReadWriteTransaction else { return } // Store the capabilities first OpenGroupManager.handleCapabilities( - capabilities, + response.capabilities.data, on: server, using: transaction, dependencies: dependencies @@ -106,7 +105,7 @@ public final class OpenGroupManager: NSObject { // Then the room OpenGroupManager.handleRoom( - room, + response.room.data, publicKey: publicKey, for: roomToken, on: server, @@ -118,6 +117,7 @@ public final class OpenGroupManager: NSObject { } } .catch(on: DispatchQueue.global(qos: .userInitiated)) { error in + SNLog("Failed to join open group.") seal.reject(error) } } @@ -125,15 +125,13 @@ public final class OpenGroupManager: NSObject { return promise } - public func delete(_ openGroup: OpenGroup, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) { - let storage = SNMessagingKitConfiguration.shared.storage - + public func delete(_ openGroup: OpenGroup, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction, dependencies: Dependencies = Dependencies()) { // Stop the poller if needed - let openGroups = storage.getAllOpenGroups().values.filter { $0.server == openGroup.server } + let openGroups = dependencies.storage.getAllOpenGroups().values.filter { $0.server == openGroup.server } if openGroups.count == 1 && openGroups.last == openGroup { - let poller = pollers[openGroup.server] + let poller = cache.pollers[openGroup.server] poller?.stop() - pollers[openGroup.server] = nil + mutableCache.mutate { $0.pollers[openGroup.server] = nil } } // Remove all data @@ -143,17 +141,18 @@ public final class OpenGroupManager: NSObject { messageIDs.insert(interaction.uniqueId!) messageTimestamps.insert(interaction.timestamp) } - storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction) - Storage.shared.removeReceivedMessageTimestamps(messageTimestamps, using: transaction) - Storage.shared.removeOpenGroupSequenceNumber(for: openGroup.room, on: openGroup.server, using: transaction) + dependencies.storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction) + dependencies.storage.removeReceivedMessageTimestamps(messageTimestamps, using: transaction) + dependencies.storage.removeOpenGroupSequenceNumber(for: openGroup.room, on: openGroup.server, using: transaction) thread.removeAllThreadInteractions(with: transaction) thread.remove(with: transaction) - Storage.shared.removeOpenGroup(for: thread.uniqueId!, using: transaction) + dependencies.storage.removeOpenGroup(for: thread.uniqueId!, using: transaction) - // Only remove the open group public key if the user isn't in any other rooms + // Only remove the open group public key and server info if the user isn't in any other rooms if openGroups.count <= 1 { - Storage.shared.removeOpenGroupPublicKey(for: openGroup.server, using: transaction) + dependencies.storage.removeOpenGroupServer(name: openGroup.server, using: transaction) + dependencies.storage.removeOpenGroupPublicKey(for: openGroup.server, using: transaction) } } @@ -262,9 +261,11 @@ public final class OpenGroupManager: NSObject { transaction.addCompletionQueue(DispatchQueue.global(qos: .userInitiated)) { // Start the poller if needed - if OpenGroupManager.shared.pollers[server] == nil { - OpenGroupManager.shared.pollers[server] = OpenGroupAPI.Poller(for: server) - OpenGroupManager.shared.pollers[server]?.startIfNeeded() + if OpenGroupManager.shared.cache.pollers[server] == nil { + OpenGroupManager.shared.mutableCache.mutate { + $0.pollers[server] = OpenGroupAPI.Poller(for: server) + $0.pollers[server]?.startIfNeeded(using: dependencies) + } } // - Moderators @@ -649,6 +650,11 @@ public final class OpenGroupManager: NSObject { } extension OpenGroupManager { + @objc(startPolling) + public func objc_startPolling() { + startPolling() + } + @objc(getDefaultRoomsIfNeeded) public static func objc_getDefaultRoomsIfNeeded() { getDefaultRoomsIfNeeded() diff --git a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift index 920eb2693..330647db6 100644 --- a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift @@ -24,6 +24,7 @@ extension OpenGroupAPI { case roomMessagesRecent(String) case roomMessagesBefore(String, id: UInt64) case roomMessagesSince(String, seqNo: Int64) + case roomDeleteMessages(String, sessionId: String) // Pinning @@ -50,7 +51,6 @@ extension OpenGroupAPI { case userBan(String) case userUnban(String) case userModerator(String) - case userDeleteMessages(String) var path: String { switch self { @@ -84,6 +84,9 @@ extension OpenGroupAPI { case .roomMessagesSince(let roomToken, let seqNo): return "room/\(roomToken)/messages/since/\(seqNo)" + case .roomDeleteMessages(let roomToken, let sessionId): + return "room/\(roomToken)/all/\(sessionId)" + // Pinning case .roomPinMessage(let roomToken, let messageId): @@ -114,7 +117,6 @@ extension OpenGroupAPI { case .userBan(let sessionId): return "user/\(sessionId)/ban" case .userUnban(let sessionId): return "user/\(sessionId)/unban" case .userModerator(let sessionId): return "user/\(sessionId)/moderator" - case .userDeleteMessages(let sessionId): return "user/\(sessionId)/deleteMessages" } } } diff --git a/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift b/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift index 28f162b00..d37137ee1 100644 --- a/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift +++ b/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift @@ -28,6 +28,7 @@ public protocol AeadXChaCha20Poly1305IetfType { } public protocol Ed25519Type { + func sign(data: Bytes, keyPair: ECKeyPair) throws -> Bytes? func verifySignature(_ signature: Data, publicKey: Data, data: Data) throws -> Bool } @@ -82,6 +83,10 @@ extension Sign: SignType {} extension GenericHash: GenericHashType {} struct Ed25519Wrapper: Ed25519Type { + func sign(data: Bytes, keyPair: ECKeyPair) throws -> Bytes? { + return try Ed25519.sign(Data(data), with: keyPair).bytes + } + func verifySignature(_ signature: Data, publicKey: Data, data: Data) throws -> Bool { return try Ed25519.verifySignature(signature, publicKey: publicKey, data: data) } diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index ef60a0f22..8f37fe51c 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -391,7 +391,8 @@ public final class MessageSender : NSObject { on: server, whisperTo: whisperTo, whisperMods: whisperMods, - fileIds: fileIds + fileIds: fileIds, + using: dependencies ) .done(on: DispatchQueue.global(qos: .userInitiated)) { responseInfo, data in message.openGroupServerMessageID = UInt64(data.id) diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift index 95e46e58b..20b7de9da 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift @@ -18,15 +18,15 @@ extension OpenGroupAPI { public init(for server: String) { self.server = server } - - @objc public func startIfNeeded() { + + public func startIfNeeded(using dependencies: Dependencies = Dependencies()) { guard !hasStarted else { return } hasStarted = true timer = Timer.scheduledTimerOnMainThread(withTimeInterval: Poller.pollInterval, repeats: true) { _ in - self.poll().retainUntilComplete() + self.poll(using: dependencies).retainUntilComplete() } - poll().retainUntilComplete() + poll(using: dependencies).retainUntilComplete() } @objc public func stop() { @@ -37,12 +37,12 @@ extension OpenGroupAPI { // MARK: - Polling @discardableResult - public func poll() -> Promise { - return poll(isBackgroundPoll: false) + public func poll(using dependencies: Dependencies = Dependencies()) -> Promise { + return poll(isBackgroundPoll: false, using: dependencies) } @discardableResult - public func poll(isBackgroundPoll: Bool) -> Promise { + public func poll(isBackgroundPoll: Bool, using dependencies: Dependencies = Dependencies()) -> Promise { guard !self.isPolling else { return Promise.value(()) } self.isPolling = true @@ -57,12 +57,13 @@ extension OpenGroupAPI { hasPerformedInitialPoll: OpenGroupManager.shared.cache.hasPerformedInitialPoll[server] == true, timeSinceLastPoll: ( OpenGroupManager.shared.cache.timeSinceLastPoll[server] ?? - OpenGroupManager.shared.cache.getTimeSinceLastOpen() - ) + OpenGroupManager.shared.cache.getTimeSinceLastOpen(using: dependencies) + ), + using: dependencies ) .done(on: OpenGroupAPI.workQueue) { [weak self] response in self?.isPolling = false - self?.handlePollResponse(response, isBackgroundPoll: isBackgroundPoll) + self?.handlePollResponse(response, isBackgroundPoll: isBackgroundPoll, using: dependencies) OpenGroupManager.shared.mutableCache.mutate { cache in cache.hasPerformedInitialPoll[server] = true @@ -81,10 +82,10 @@ extension OpenGroupAPI { return promise } - private func handlePollResponse(_ response: [OpenGroupAPI.Endpoint: (info: OnionRequestResponseInfoType, data: Codable?)], isBackgroundPoll: Bool) { + private func handlePollResponse(_ response: [OpenGroupAPI.Endpoint: (info: OnionRequestResponseInfoType, data: Codable?)], isBackgroundPoll: Bool, using dependencies: Dependencies = Dependencies()) { let server: String = self.server - Storage.shared.write { anyTransaction in + dependencies.storage.write { anyTransaction in guard let transaction: YapDatabaseReadWriteTransaction = anyTransaction as? YapDatabaseReadWriteTransaction else { SNLog("Open group polling failed due to invalid database transaction.") return @@ -101,21 +102,30 @@ extension OpenGroupAPI { OpenGroupManager.handleCapabilities( responseBody, on: server, - using: transaction + using: transaction, + dependencies: dependencies ) case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _): - guard let responseData: BatchSubResponse<[Message]> = endpointResponse.data as? BatchSubResponse<[Message]>, let responseBody: [Message] = responseData.body else { + guard let responseData: BatchSubResponse<[Failable]> = endpointResponse.data as? BatchSubResponse<[Failable]>, let responseBody: [Failable] = responseData.body else { SNLog("Open group polling failed due to invalid data.") return } + let successfulMessages: [Message] = responseBody.compactMap { $0.value } + + if successfulMessages.count != responseBody.count { + let droppedCount: Int = (responseBody.count - successfulMessages.count) + + SNLog("Dropped \(droppedCount) invalid open group message\(droppedCount == 1 ? "" : "s").") + } OpenGroupManager.handleMessages( - responseBody, + successfulMessages, for: roomToken, on: server, isBackgroundPoll: isBackgroundPoll, - using: transaction + using: transaction, + dependencies: dependencies ) case .roomPollInfo(let roomToken, _): @@ -129,7 +139,8 @@ extension OpenGroupAPI { publicKey: nil, for: roomToken, on: server, - using: transaction + using: transaction, + dependencies: dependencies ) case .inbox, .inboxSince, .outbox, .outboxSince: @@ -150,7 +161,8 @@ extension OpenGroupAPI { fromOutbox: fromOutbox, on: server, isBackgroundPoll: isBackgroundPoll, - using: transaction + using: transaction, + dependencies: dependencies ) default: break // No custom handling needed @@ -160,3 +172,10 @@ extension OpenGroupAPI { } } } + +extension OpenGroupAPI.Poller { + @objc(startIfNeeded) + public func objc_startIfNeeded() { + startIfNeeded() + } +} diff --git a/SessionMessagingKit/Utilities/Dependencies.swift b/SessionMessagingKit/Utilities/Dependencies.swift index 31afcf434..e1b405c80 100644 --- a/SessionMessagingKit/Utilities/Dependencies.swift +++ b/SessionMessagingKit/Utilities/Dependencies.swift @@ -8,10 +8,10 @@ import SessionUtilitiesKit // MARK: - Dependencies public class Dependencies { - private var _api: OnionRequestAPIType.Type? - public var api: OnionRequestAPIType.Type { - get { getValueSettingIfNull(&_api) { OnionRequestAPI.self } } - set { _api = newValue } + private var _onionApi: OnionRequestAPIType.Type? + public var onionApi: OnionRequestAPIType.Type { + get { getValueSettingIfNull(&_onionApi) { OnionRequestAPI.self } } + set { _onionApi = newValue } } private var _storage: SessionMessagingKitStorageProtocol? @@ -77,7 +77,7 @@ public class Dependencies { // MARK: - Initialization public init( - api: OnionRequestAPIType.Type? = nil, + onionApi: OnionRequestAPIType.Type? = nil, storage: SessionMessagingKitStorageProtocol? = nil, sodium: SodiumType? = nil, aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil, @@ -89,7 +89,7 @@ public class Dependencies { standardUserDefaults: UserDefaultsType? = nil, date: Date? = nil ) { - _api = api + _onionApi = onionApi _storage = storage _sodium = sodium _aeadXChaCha20Poly1305Ietf = aeadXChaCha20Poly1305Ietf @@ -105,7 +105,7 @@ public class Dependencies { // MARK: - Convenience public func with( - api: OnionRequestAPIType.Type? = nil, + onionApi: OnionRequestAPIType.Type? = nil, storage: SessionMessagingKitStorageProtocol? = nil, sodium: SodiumType? = nil, aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil, @@ -118,7 +118,7 @@ public class Dependencies { date: Date? = nil ) -> Dependencies { return Dependencies( - api: (api ?? self._api), + onionApi: (onionApi ?? self._onionApi), storage: (storage ?? self._storage), sodium: (sodium ?? self._sodium), aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf), diff --git a/SessionMessagingKit/Utilities/Failable.swift b/SessionMessagingKit/Utilities/Failable.swift new file mode 100644 index 000000000..80aa529a0 --- /dev/null +++ b/SessionMessagingKit/Utilities/Failable.swift @@ -0,0 +1,24 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +struct Failable: Codable { + let value: T? + + init(from decoder: Decoder) throws { + guard let container = try? decoder.singleValueContainer() else { + self.value = nil + return + } + + self.value = try? container.decode(T.self) + } + + func encode(to encoder: Encoder) throws { + guard let value: T = value else { return } + + var container: SingleValueEncodingContainer = encoder.singleValueContainer() + + try container.encode(value) + } +} diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index 416172861..12fa35142 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -10,18 +10,6 @@ import Nimble @testable import SessionMessagingKit class OpenGroupAPISpec: QuickSpec { - class TestResponseInfo: OnionRequestResponseInfoType { - let requestData: TestApi.RequestData - let code: Int - let headers: [String: String] - - init(requestData: TestApi.RequestData, code: Int, headers: [String: String]) { - self.requestData = requestData - self.code = code - self.headers = headers - } - } - struct TestNonce16Generator: NonceGenerator16ByteType { var NonceBytes: Int = 16 @@ -34,56 +22,15 @@ class OpenGroupAPISpec: QuickSpec { func nonce() -> Array { return Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes } } - class TestApi: OnionRequestAPIType { - struct RequestData: Codable { - let urlString: String? - let httpMethod: String - let headers: [String: String] - let snodeMethod: String? - let body: Data? - - let server: String - let version: OnionRequestAPI.Version - let publicKey: String? - } - - class var mockResponse: Data? { return nil } - - static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPI.Version, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { - let responseInfo: TestResponseInfo = TestResponseInfo( - requestData: RequestData( - urlString: request.url?.absoluteString, - httpMethod: (request.httpMethod ?? "GET"), - headers: (request.allHTTPHeaderFields ?? [:]), - snodeMethod: nil, - body: request.httpBody, - - server: server, - version: version, - publicKey: x25519PublicKey - ), - code: 200, - headers: [:] - ) - - return Promise.value((responseInfo, mockResponse)) - } - - static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, using version: OnionRequestAPI.Version, associatedWith publicKey: String?) -> Promise { - // TODO: Test the 'responseInfo' somehow? - return Promise.value(mockResponse!) - } - } - // MARK: - Spec override func spec() { var mockStorage: MockStorage! var mockSodium: MockSodium! var mockAeadXChaCha20Poly1305Ietf: MockAeadXChaCha20Poly1305Ietf! - var mockGenericHash: MockGenericHash! var mockSign: MockSign! - var testUserDefaults: TestUserDefaults! + var mockGenericHash: MockGenericHash! + var mockEd25519: MockEd25519! var dependencies: Dependencies! var response: (OnionRequestResponseInfoType, Codable)? = nil @@ -97,20 +44,19 @@ class OpenGroupAPISpec: QuickSpec { mockStorage = MockStorage() mockSodium = MockSodium() mockAeadXChaCha20Poly1305Ietf = MockAeadXChaCha20Poly1305Ietf() - mockGenericHash = MockGenericHash() mockSign = MockSign() - testUserDefaults = TestUserDefaults() + mockGenericHash = MockGenericHash() + mockEd25519 = MockEd25519() dependencies = Dependencies( - api: TestApi.self, + onionApi: TestOnionRequestAPI.self, storage: mockStorage, sodium: mockSodium, aeadXChaCha20Poly1305Ietf: mockAeadXChaCha20Poly1305Ietf, sign: mockSign, genericHash: mockGenericHash, - ed25519: MockEd25519(), + ed25519: mockEd25519, nonceGenerator16: TestNonce16Generator(), nonceGenerator24: TestNonce24Generator(), - standardUserDefaults: testUserDefaults, date: Date(timeIntervalSince1970: 1234567890) ) @@ -190,15 +136,16 @@ class OpenGroupAPISpec: QuickSpec { } .thenReturn("TestSogsSignature".bytes) mockSign.when { $0.signature(message: any(), secretKey: any()) }.thenReturn("TestSignature".bytes) + mockEd25519.when { try $0.sign(data: any(), keyPair: any()) }.thenReturn("TestStandardSignature".bytes) } afterEach { mockStorage = nil mockSodium = nil mockAeadXChaCha20Poly1305Ietf = nil - mockGenericHash = nil mockSign = nil - testUserDefaults = nil + mockGenericHash = nil + mockEd25519 = nil dependencies = nil response = nil @@ -211,7 +158,7 @@ class OpenGroupAPISpec: QuickSpec { context("when polling") { context("and given a correct response") { beforeEach { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { let responses: [Data] = [ try! JSONEncoder().encode( @@ -271,7 +218,7 @@ class OpenGroupAPISpec: QuickSpec { } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) } it("generates the correct request") { @@ -294,10 +241,10 @@ class OpenGroupAPISpec: QuickSpec { expect(pollResponse?.keys).to(contain(.roomMessagesRecent("testRoom"))) expect(pollResponse?.keys).to(contain(.inbox)) expect(pollResponse?.keys).to(contain(.outbox)) - expect(pollResponse?[.capabilities]?.0).to(beAKindOf(TestResponseInfo.self)) + expect(pollResponse?[.capabilities]?.0).to(beAKindOf(TestOnionRequestAPI.ResponseInfo.self)) // Validate request data - let requestData: TestApi.RequestData? = (pollResponse?[.capabilities]?.0 as? TestResponseInfo)?.requestData + 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")) @@ -484,24 +431,6 @@ class OpenGroupAPISpec: QuickSpec { } context("and given an invalid response") { - it("does not update the poll state") { - mockStorage.when { $0.getOpenGroupSequenceNumber(for: any(), on: any()) }.thenReturn(123) - - OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) - .get { result in pollResponse = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(HTTP.Error.invalidResponse.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(pollResponse).to(beNil()) - expect(testUserDefaults[.lastOpen]).to(beNil()) - } - it("errors when no data is returned") { OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) .get { result in pollResponse = result } @@ -518,10 +447,10 @@ class OpenGroupAPISpec: QuickSpec { } it("errors when invalid data is returned") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return Data() } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) .get { result in pollResponse = result } @@ -538,10 +467,10 @@ class OpenGroupAPISpec: QuickSpec { } it("errors when an empty array is returned") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return "[]".data(using: .utf8) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) .get { result in pollResponse = result } @@ -558,10 +487,10 @@ class OpenGroupAPISpec: QuickSpec { } it("errors when an empty object is returned") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return "{}".data(using: .utf8) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) .get { result in pollResponse = result } @@ -578,7 +507,7 @@ class OpenGroupAPISpec: QuickSpec { } it("errors when a different number of responses are returned") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { let responses: [Data] = [ try! JSONEncoder().encode( @@ -613,7 +542,7 @@ class OpenGroupAPISpec: QuickSpec { return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) .get { result in pollResponse = result } @@ -630,7 +559,7 @@ class OpenGroupAPISpec: QuickSpec { } it("errors when an unexpected response is returned") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { let responses: [Data] = [ try! JSONEncoder().encode( @@ -662,7 +591,7 @@ class OpenGroupAPISpec: QuickSpec { return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) .get { result in pollResponse = result } @@ -684,12 +613,12 @@ class OpenGroupAPISpec: QuickSpec { context("when doing a capabilities request") { it("generates the request and handles the response correctly") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { static let data: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil) override class var mockResponse: Data? { try! JSONEncoder().encode(data) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities)? @@ -706,10 +635,10 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate the response data - expect(response?.data).to(equal(LocalTestApi.data)) + expect(response?.data).to(equal(TestApi.data)) // Validate request data - let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + 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")) @@ -720,7 +649,7 @@ class OpenGroupAPISpec: QuickSpec { context("when doing a rooms request") { it("generates the request and handles the response correctly") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { static let data: [OpenGroupAPI.Room] = [ OpenGroupAPI.Room( token: "test", @@ -753,7 +682,7 @@ class OpenGroupAPISpec: QuickSpec { override class var mockResponse: Data? { return try! JSONEncoder().encode(data) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) var response: (info: OnionRequestResponseInfoType, data: [OpenGroupAPI.Room])? @@ -770,10 +699,10 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate the response data - expect(response?.data).to(equal(LocalTestApi.data)) + expect(response?.data).to(equal(TestApi.data)) // Validate request data - let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + 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")) @@ -785,7 +714,7 @@ class OpenGroupAPISpec: QuickSpec { context("when doing a capabilitiesAndRoom request") { context("and given a correct response") { it("generates the request and handles the response correctly") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { static let capabilitiesData: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil) static let roomData: OpenGroupAPI.Room = OpenGroupAPI.Room( token: "test", @@ -838,7 +767,7 @@ class OpenGroupAPISpec: QuickSpec { return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))? @@ -855,11 +784,11 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate the response data - expect(response?.capabilities.data).to(equal(LocalTestApi.capabilitiesData)) - expect(response?.room.data).to(equal(LocalTestApi.roomData)) + expect(response?.capabilities.data).to(equal(TestApi.capabilitiesData)) + expect(response?.room.data).to(equal(TestApi.roomData)) // Validate request data - let requestData: TestApi.RequestData? = (response?.capabilities.info as? TestResponseInfo)?.requestData + 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")) @@ -868,7 +797,7 @@ class OpenGroupAPISpec: QuickSpec { context("and given an invalid response") { it("errors when only a capabilities response is returned") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { static let capabilitiesData: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil) override class var mockResponse: Data? { @@ -886,7 +815,7 @@ class OpenGroupAPISpec: QuickSpec { return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))? @@ -905,7 +834,7 @@ class OpenGroupAPISpec: QuickSpec { } it("errors when only a room response is returned") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { static let roomData: OpenGroupAPI.Room = OpenGroupAPI.Room( token: "test", name: "test", @@ -949,7 +878,7 @@ class OpenGroupAPISpec: QuickSpec { return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))? @@ -968,7 +897,7 @@ class OpenGroupAPISpec: QuickSpec { } it("errors when an extra response is returned") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { static let capabilitiesData: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil) static let roomData: OpenGroupAPI.Room = OpenGroupAPI.Room( token: "test", @@ -1029,7 +958,7 @@ class OpenGroupAPISpec: QuickSpec { return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))? @@ -1055,7 +984,7 @@ class OpenGroupAPISpec: QuickSpec { var messageData: OpenGroupAPI.Message! beforeEach { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { static let data: OpenGroupAPI.Message = OpenGroupAPI.Message( id: 126, sender: "testSender", @@ -1071,10 +1000,8 @@ class OpenGroupAPISpec: QuickSpec { override class var mockResponse: Data? { return try! JSONEncoder().encode(data) } } - messageData = LocalTestApi.data - dependencies = dependencies.with(api: LocalTestApi.self) - - mockStorage.when { $0.getUserED25519KeyPair() }.thenReturn(Box.KeyPair(publicKey: [], secretKey: [])) + messageData = TestApi.data + dependencies = dependencies.with(onionApi: TestApi.self) } afterEach { @@ -1109,7 +1036,7 @@ class OpenGroupAPISpec: QuickSpec { expect(response?.data).to(equal(messageData)) // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + 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")) @@ -1181,11 +1108,11 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request body - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData let requestBody: OpenGroupAPI.SendMessageRequest = try! JSONDecoder().decode(OpenGroupAPI.SendMessageRequest.self, from: requestData!.body!) expect(requestBody.data).to(equal("test".data(using: .utf8))) - expect(requestBody.signature).to(equal("TestSignature".data(using: .utf8))) + expect(requestBody.signature).to(equal("TestStandardSignature".data(using: .utf8))) } it("fails to sign if there is no public key") { @@ -1215,6 +1142,63 @@ class OpenGroupAPISpec: QuickSpec { expect(response).to(beNil()) } + + it("fails to sign if there is no user key pair") { + mockStorage.when { $0.getUserKeyPair() }.thenReturn(nil) + + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)? + + OpenGroupAPI + .send( + "test".data(using: .utf8)!, + to: "testRoom", + on: "testServer", + whisperTo: nil, + whisperMods: false, + fileIds: nil, + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + it("fails to sign if no signature is generated") { + mockEd25519.reset() // The 'keyPair' value doesn't equate so have to explicitly reset + mockEd25519.when { try $0.sign(data: any(), keyPair: any()) }.thenReturn(nil) + + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)? + + OpenGroupAPI + .send( + "test".data(using: .utf8)!, + to: "testRoom", + on: "testServer", + whisperTo: nil, + whisperMods: false, + fileIds: nil, + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } } context("when blinded") { @@ -1254,7 +1238,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request body - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData let requestBody: OpenGroupAPI.SendMessageRequest = try! JSONDecoder().decode(OpenGroupAPI.SendMessageRequest.self, from: requestData!.body!) expect(requestBody.data).to(equal("test".data(using: .utf8))) @@ -1288,12 +1272,70 @@ class OpenGroupAPISpec: QuickSpec { expect(response).to(beNil()) } + + it("fails to sign if there is no ed key pair key") { + mockStorage.when { $0.getUserED25519KeyPair() }.thenReturn(nil) + + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)? + + OpenGroupAPI + .send( + "test".data(using: .utf8)!, + to: "testRoom", + on: "testServer", + whisperTo: nil, + whisperMods: false, + fileIds: nil, + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + it("fails to sign if no signature is generated") { + mockSodium + .when { $0.sogsSignature(message: any(), secretKey: any(), blindedSecretKey: any(), blindedPublicKey: any()) } + .thenReturn(nil) + + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)? + + OpenGroupAPI + .send( + "test".data(using: .utf8)!, + to: "testRoom", + on: "testServer", + whisperTo: nil, + whisperMods: false, + fileIds: nil, + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } } } context("when getting an individual message") { it("generates the request and handles the response correctly") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { static let data: OpenGroupAPI.Message = OpenGroupAPI.Message( id: 126, sender: "testSender", @@ -1309,7 +1351,7 @@ class OpenGroupAPISpec: QuickSpec { override class var mockResponse: Data? { return try! JSONEncoder().encode(data) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)? @@ -1326,10 +1368,10 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate the response data - expect(response?.data).to(equal(LocalTestApi.data)) + expect(response?.data).to(equal(TestApi.data)) // Validate request data - let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + 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")) @@ -1338,10 +1380,10 @@ class OpenGroupAPISpec: QuickSpec { context("when updating a message") { beforeEach { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return Data() } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) mockStorage.when { $0.getUserED25519KeyPair() }.thenReturn(Box.KeyPair(publicKey: [], secretKey: [])) } @@ -1370,7 +1412,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + 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")) @@ -1412,11 +1454,11 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request body - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData let requestBody: OpenGroupAPI.UpdateMessageRequest = try! JSONDecoder().decode(OpenGroupAPI.UpdateMessageRequest.self, from: requestData!.body!) expect(requestBody.data).to(equal("test".data(using: .utf8))) - expect(requestBody.signature).to(equal("TestSignature".data(using: .utf8))) + expect(requestBody.signature).to(equal("TestStandardSignature".data(using: .utf8))) } it("fails to sign if there is no public key") { @@ -1445,6 +1487,61 @@ class OpenGroupAPISpec: QuickSpec { expect(response).to(beNil()) } + + it("fails to sign if there is no user key pair") { + mockStorage.when { $0.getUserKeyPair() }.thenReturn(nil) + + var response: (info: OnionRequestResponseInfoType, data: Data?)? + + OpenGroupAPI + .messageUpdate( + 123, + plaintext: "test".data(using: .utf8)!, + fileIds: nil, + in: "testRoom", + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + it("fails to sign if no signature is generated") { + mockEd25519.reset() // The 'keyPair' value doesn't equate so have to explicitly reset + mockEd25519.when { try $0.sign(data: any(), keyPair: any()) }.thenReturn(nil) + + var response: (info: OnionRequestResponseInfoType, data: Data?)? + + OpenGroupAPI + .messageUpdate( + 123, + plaintext: "test".data(using: .utf8)!, + fileIds: nil, + in: "testRoom", + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } } context("when blinded") { @@ -1483,7 +1580,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request body - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData let requestBody: OpenGroupAPI.UpdateMessageRequest = try! JSONDecoder().decode(OpenGroupAPI.UpdateMessageRequest.self, from: requestData!.body!) expect(requestBody.data).to(equal("test".data(using: .utf8))) @@ -1516,15 +1613,71 @@ class OpenGroupAPISpec: QuickSpec { expect(response).to(beNil()) } + + it("fails to sign if there is no ed key pair key") { + mockStorage.when { $0.getUserED25519KeyPair() }.thenReturn(nil) + + var response: (info: OnionRequestResponseInfoType, data: Data?)? + + OpenGroupAPI + .messageUpdate( + 123, + plaintext: "test".data(using: .utf8)!, + fileIds: nil, + in: "testRoom", + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + it("fails to sign if no signature is generated") { + mockSodium + .when { $0.sogsSignature(message: any(), secretKey: any(), blindedSecretKey: any(), blindedPublicKey: any()) } + .thenReturn(nil) + + var response: (info: OnionRequestResponseInfoType, data: Data?)? + + OpenGroupAPI + .messageUpdate( + 123, + plaintext: "test".data(using: .utf8)!, + fileIds: nil, + in: "testRoom", + on: "testServer", + using: dependencies + ) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPI.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } } } context("when deleting a message") { it("generates the request and handles the response correctly") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return Data() } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) var response: (info: OnionRequestResponseInfoType, data: Data?)? @@ -1541,7 +1694,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + 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")) @@ -1552,10 +1705,10 @@ class OpenGroupAPISpec: QuickSpec { var response: (info: OnionRequestResponseInfoType, data: Data?)? beforeEach { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return Data() } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) } afterEach { @@ -1582,7 +1735,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + 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")) @@ -1593,10 +1746,10 @@ class OpenGroupAPISpec: QuickSpec { context("when pinning a message") { it("generates the request and handles the response correctly") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return Data() } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) var response: OnionRequestResponseInfoType? @@ -1613,7 +1766,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response as? TestResponseInfo)?.requestData + 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")) @@ -1622,10 +1775,10 @@ class OpenGroupAPISpec: QuickSpec { context("when unpinning a message") { it("generates the request and handles the response correctly") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return Data() } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) var response: OnionRequestResponseInfoType? @@ -1642,7 +1795,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response as? TestResponseInfo)?.requestData + 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")) @@ -1651,10 +1804,10 @@ class OpenGroupAPISpec: QuickSpec { context("when unpinning all messages") { it("generates the request and handles the response correctly") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return Data() } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) var response: OnionRequestResponseInfoType? @@ -1671,7 +1824,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response as? TestResponseInfo)?.requestData + 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")) @@ -1682,12 +1835,12 @@ class OpenGroupAPISpec: QuickSpec { context("when uploading files") { it("generates the request and handles the response correctly") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return try! JSONEncoder().encode(FileUploadResponse(id: "1")) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) OpenGroupAPI.uploadFile([], to: "testRoom", on: "testServer", using: dependencies) .get { result in response = result } @@ -1702,19 +1855,19 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + 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")) } it("doesn't add a fileName to the content-disposition header when not provided") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return try! JSONEncoder().encode(FileUploadResponse(id: "1")) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) OpenGroupAPI.uploadFile([], to: "testRoom", on: "testServer", using: dependencies) .get { result in response = result } @@ -1729,18 +1882,18 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData expect(requestData?.headers[Header.contentDisposition.rawValue]) .toNot(contain("filename")) } it("adds the fileName to the content-disposition header when provided") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return try! JSONEncoder().encode(FileUploadResponse(id: "1")) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) OpenGroupAPI.uploadFile([], fileName: "TestFileName", to: "testRoom", on: "testServer", using: dependencies) .get { result in response = result } @@ -1755,19 +1908,19 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData expect(requestData?.headers[Header.contentDisposition.rawValue]).to(contain("TestFileName")) } } context("when downloading files") { it("generates the request and handles the response correctly") { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return Data() } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) OpenGroupAPI.downloadFile(1, from: "testRoom", on: "testServer", using: dependencies) .get { result in response = result } @@ -1782,7 +1935,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + 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")) @@ -1795,7 +1948,7 @@ class OpenGroupAPISpec: QuickSpec { var messageData: OpenGroupAPI.SendDirectMessageResponse! beforeEach { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { static let data: OpenGroupAPI.SendDirectMessageResponse = OpenGroupAPI.SendDirectMessageResponse( id: 126, sender: "testSender", @@ -1806,8 +1959,8 @@ class OpenGroupAPISpec: QuickSpec { override class var mockResponse: Data? { return try! JSONEncoder().encode(data) } } - messageData = LocalTestApi.data - dependencies = dependencies.with(api: LocalTestApi.self) + messageData = TestApi.data + dependencies = dependencies.with(onionApi: TestApi.self) } afterEach { @@ -1839,7 +1992,7 @@ class OpenGroupAPISpec: QuickSpec { expect(response?.data).to(equal(messageData)) // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + 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")) @@ -1878,10 +2031,10 @@ class OpenGroupAPISpec: QuickSpec { var response: (info: OnionRequestResponseInfoType, data: Data?)? beforeEach { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return Data() } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) } afterEach { @@ -1909,7 +2062,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + 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")) @@ -1936,7 +2089,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData let requestBody: OpenGroupAPI.UserBanRequest = try! JSONDecoder().decode(OpenGroupAPI.UserBanRequest.self, from: requestData!.body!) expect(requestBody.global).to(beTrue()) @@ -1964,7 +2117,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData let requestBody: OpenGroupAPI.UserBanRequest = try! JSONDecoder().decode(OpenGroupAPI.UserBanRequest.self, from: requestData!.body!) expect(requestBody.global).to(beNil()) @@ -1976,10 +2129,10 @@ class OpenGroupAPISpec: QuickSpec { var response: (info: OnionRequestResponseInfoType, data: Data?)? beforeEach { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return Data() } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) } afterEach { @@ -2006,7 +2159,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + 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")) @@ -2032,7 +2185,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData let requestBody: OpenGroupAPI.UserUnbanRequest = try! JSONDecoder().decode(OpenGroupAPI.UserUnbanRequest.self, from: requestData!.body!) expect(requestBody.global).to(beTrue()) @@ -2059,7 +2212,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData let requestBody: OpenGroupAPI.UserUnbanRequest = try! JSONDecoder().decode(OpenGroupAPI.UserUnbanRequest.self, from: requestData!.body!) expect(requestBody.global).to(beNil()) @@ -2071,10 +2224,10 @@ class OpenGroupAPISpec: QuickSpec { var response: (info: OnionRequestResponseInfoType, data: Data?)? beforeEach { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return Data() } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) } afterEach { @@ -2104,7 +2257,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + 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")) @@ -2133,7 +2286,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData let requestBody: OpenGroupAPI.UserModeratorRequest = try! JSONDecoder().decode(OpenGroupAPI.UserModeratorRequest.self, from: requestData!.body!) expect(requestBody.global).to(beTrue()) @@ -2163,7 +2316,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData let requestBody: OpenGroupAPI.UserModeratorRequest = try! JSONDecoder().decode(OpenGroupAPI.UserModeratorRequest.self, from: requestData!.body!) expect(requestBody.global).to(beNil()) @@ -2199,7 +2352,7 @@ class OpenGroupAPISpec: QuickSpec { var response: [OnionRequestResponseInfoType]? beforeEach { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { let responses: [Data] = [ try! JSONEncoder().encode( @@ -2223,7 +2376,7 @@ class OpenGroupAPISpec: QuickSpec { return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) } afterEach { @@ -2250,7 +2403,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response?.first as? TestResponseInfo)?.requestData + 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")) @@ -2276,7 +2429,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate request data - let requestData: TestApi.RequestData? = (response?.first as? TestResponseInfo)?.requestData + let requestData: TestOnionRequestAPI.RequestData? = (response?.first as? TestOnionRequestAPI.ResponseInfo)?.requestData let jsonObject: Any = try! JSONSerialization.jsonObject( with: requestData!.body!, options: [.fragmentsAllowed] @@ -2295,13 +2448,13 @@ class OpenGroupAPISpec: QuickSpec { context("when signing") { beforeEach { - class LocalTestApi: TestApi { + class TestApi: TestOnionRequestAPI { override class var mockResponse: Data? { return try! JSONEncoder().encode([OpenGroupAPI.Room]()) } } - dependencies = dependencies.with(api: LocalTestApi.self) + dependencies = dependencies.with(onionApi: TestApi.self) } it("fails when there is no userEdKeyPair") { @@ -2381,7 +2534,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + 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")) @@ -2438,7 +2591,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate signature headers - let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + 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")) diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 0a8ab0dac..75d34aeb0 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -1,3 +1,559 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -import Foundation +import PromiseKit +import Sodium +import SessionSnodeKit + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class OpenGroupManagerSpec: QuickSpec { + class TestCapabilitiesAndRoomApi: TestOnionRequestAPI { + static let capabilitiesData: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil) + static let roomData: OpenGroupAPI.Room = OpenGroupAPI.Room( + token: "test", + name: "test", + description: nil, + infoUpdates: 0, + messageSequence: 0, + created: 0, + activeUsers: 0, + activeUsersCutoff: 0, + imageId: nil, + pinnedMessages: nil, + admin: false, + globalAdmin: false, + admins: [], + hiddenAdmins: nil, + moderator: false, + globalModerator: false, + moderators: [], + hiddenModerators: nil, + read: false, + defaultRead: nil, + defaultAccessible: nil, + write: false, + defaultWrite: nil, + upload: false, + defaultUpload: nil + ) + + override class var mockResponse: Data? { + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: capabilitiesData, + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: roomData, + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + } + } + + // MARK: - Spec + + override func spec() { + var mockStorage: MockStorage! + var mockSodium: MockSodium! + var mockAeadXChaCha20Poly1305Ietf: MockAeadXChaCha20Poly1305Ietf! + var mockGenericHash: MockGenericHash! + var mockSign: MockSign! + var mockUserDefaults: MockUserDefaults! + var dependencies: Dependencies! + + var testInteraction: TestInteraction! + var testGroupThread: TestGroupThread! + var testTransaction: TestTransaction! + + describe("an OpenGroupAPI") { + // MARK: - Configuration + + beforeEach { + mockStorage = MockStorage() + mockSodium = MockSodium() + mockAeadXChaCha20Poly1305Ietf = MockAeadXChaCha20Poly1305Ietf() + mockGenericHash = MockGenericHash() + mockSign = MockSign() + mockUserDefaults = MockUserDefaults() + dependencies = Dependencies( + onionApi: TestCapabilitiesAndRoomApi.self, + storage: mockStorage, + sodium: mockSodium, + aeadXChaCha20Poly1305Ietf: mockAeadXChaCha20Poly1305Ietf, + sign: mockSign, + genericHash: mockGenericHash, + ed25519: MockEd25519(), + nonceGenerator16: OpenGroupAPISpec.TestNonce16Generator(), + nonceGenerator24: OpenGroupAPISpec.TestNonce24Generator(), + standardUserDefaults: mockUserDefaults, + date: Date(timeIntervalSince1970: 1234567890) + ) + testInteraction = TestInteraction() + testInteraction.mockData[.uniqueId] = "TestInteractionId" + testInteraction.mockData[.timestamp] = UInt64(123) + + testGroupThread = TestGroupThread() + testGroupThread.mockData[.groupModel] = TSGroupModel( + title: "TestTitle", + memberIds: [], + image: nil, + groupId: LKGroupUtilities.getEncodedOpenGroupIDAsData("testServer.testRoom"), + groupType: .openGroup, + adminIds: [], + moderatorIds: [] + ) + testGroupThread.mockData[.interactions] = [testInteraction] + + testTransaction = TestTransaction() + testTransaction.mockData[.objectForKey] = testGroupThread + + mockStorage + .when { $0.write(with: { _ in }) } + .then { args in (args.first as? ((Any) -> Void))?(testTransaction as Any) } + .thenReturn(Promise.value(())) + mockStorage + .when { $0.write(with: { _ in }, completion: { }) } + .then { args in + (args.first as? ((Any) -> Void))?(testTransaction as Any) + (args.last as? (() -> Void))?() + } + .thenReturn(Promise.value(())) + mockStorage + .when { $0.getUserKeyPair() } + .thenReturn( + try! ECKeyPair( + publicKeyData: Data.data(fromHex: TestConstants.publicKey)!, + privateKeyData: Data.data(fromHex: TestConstants.privateKey)! + ) + ) + mockStorage + .when { $0.getUserED25519KeyPair() } + .thenReturn( + Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ) + ) + mockStorage + .when { $0.getAllOpenGroups() } + .thenReturn([ + "0": OpenGroup( + server: "testServer", + room: "testRoom", + publicKey: TestConstants.publicKey, + name: "Test", + groupDescription: nil, + imageID: nil, + infoUpdates: 0 + ) + ]) + mockStorage + .when { $0.getOpenGroup(for: any()) } + .thenReturn( + OpenGroup( + server: "testServer", + room: "testRoom", + publicKey: TestConstants.publicKey, + name: "Test", + groupDescription: nil, + imageID: nil, + infoUpdates: 0 + ) + ) + mockStorage + .when { $0.getOpenGroupServer(name: any()) } + .thenReturn( + OpenGroupAPI.Server( + name: "testServer", + capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: []) + ) + ) + mockStorage + .when { $0.getOpenGroupPublicKey(for: any()) } + .thenReturn(TestConstants.publicKey) + + mockGenericHash.when { $0.hash(message: any(), outputLength: any()) }.thenReturn([]) + mockSodium + .when { $0.blindedKeyPair(serverPublicKey: any(), edKeyPair: any(), genericHash: mockGenericHash) } + .thenReturn( + Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ) + ) + mockSodium + .when { + $0.sogsSignature( + message: any(), + secretKey: any(), + blindedSecretKey: any(), + blindedPublicKey: any() + ) + } + .thenReturn("TestSogsSignature".bytes) + mockSign.when { $0.signature(message: any(), secretKey: any()) }.thenReturn("TestSignature".bytes) + } + + afterEach { + OpenGroupManager.shared.stopPolling() // Need to stop any pollers which get created during tests + + mockStorage = nil + mockSodium = nil + mockAeadXChaCha20Poly1305Ietf = nil + mockGenericHash = nil + mockSign = nil + mockUserDefaults = nil + dependencies = nil + + testInteraction = nil + testGroupThread = nil + testTransaction = nil + } + + // MARK: - Polling + + context("when starting polling") { + beforeEach { + mockStorage + .when { $0.getAllOpenGroups() } + .thenReturn([ + "0": OpenGroup( + server: "testServer", + room: "testRoom", + publicKey: TestConstants.publicKey, + name: "Test", + groupDescription: nil, + imageID: nil, + infoUpdates: 0 + ), + "1": OpenGroup( + server: "testServer1", + room: "testRoom1", + publicKey: TestConstants.publicKey, + name: "Test1", + groupDescription: nil, + imageID: nil, + infoUpdates: 0 + ) + ]) + mockStorage.when { $0.removeOpenGroupSequenceNumber(for: any(), on: any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.setOpenGroupPublicKey(for: any(), to: any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.setOpenGroupServer(any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.setOpenGroup(any(), for: any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.setUserCount(to: any(), forOpenGroupWithID: any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.getOpenGroupInboxLatestMessageId(for: any()) }.thenReturn(nil) + mockStorage.when { $0.getOpenGroupOutboxLatestMessageId(for: any()) }.thenReturn(nil) + mockStorage.when { $0.getOpenGroupSequenceNumber(for: any(), on: any()) }.thenReturn(nil) + + mockUserDefaults + .when { $0.object(forKey: SNUserDefaults.Date.lastOpen.rawValue) } + .thenReturn(Date(timeIntervalSince1970: 1234567890)) + } + + it("creates pollers for all of the open groups") { + OpenGroupManager.shared.startPolling(using: dependencies) + + expect(OpenGroupManager.shared.cache.pollers.keys.map { String($0) }.sorted) + .to(equal(["testserver", "testserver1"])) + } + + it("updates the isPolling flag") { + OpenGroupManager.shared.startPolling(using: dependencies) + + expect(OpenGroupManager.shared.cache.isPolling).to(beTrue()) + } + + it("does nothing if already polling") { + OpenGroupManager.shared.mutableCache.mutate { $0.isPolling = true } + OpenGroupManager.shared.startPolling(using: dependencies) + + expect(OpenGroupManager.shared.cache.pollers.keys.map { String($0) }) + .to(equal([])) + } + } + + context("when stopping polling") { + beforeEach { + mockStorage + .when { $0.getAllOpenGroups() } + .thenReturn([ + "0": OpenGroup( + server: "testServer", + room: "testRoom", + publicKey: TestConstants.publicKey, + name: "Test", + groupDescription: nil, + imageID: nil, + infoUpdates: 0 + ), + "1": OpenGroup( + server: "testServer1", + room: "testRoom1", + publicKey: TestConstants.publicKey, + name: "Test1", + groupDescription: nil, + imageID: nil, + infoUpdates: 0 + ) + ]) + mockStorage.when { $0.removeOpenGroupSequenceNumber(for: any(), on: any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.setOpenGroupPublicKey(for: any(), to: any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.setOpenGroupServer(any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.setOpenGroup(any(), for: any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.setUserCount(to: any(), forOpenGroupWithID: any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.getOpenGroupInboxLatestMessageId(for: any()) }.thenReturn(nil) + mockStorage.when { $0.getOpenGroupOutboxLatestMessageId(for: any()) }.thenReturn(nil) + mockStorage.when { $0.getOpenGroupSequenceNumber(for: any(), on: any()) }.thenReturn(nil) + + mockUserDefaults + .when { $0.object(forKey: SNUserDefaults.Date.lastOpen.rawValue) } + .thenReturn(Date(timeIntervalSince1970: 1234567890)) + + OpenGroupManager.shared.startPolling(using: dependencies) + } + + it("removes all pollers") { + OpenGroupManager.shared.stopPolling() + + expect(OpenGroupManager.shared.cache.pollers.keys.map { String($0) }) + .to(equal([])) + } + + it("updates the isPolling flag") { + OpenGroupManager.shared.stopPolling() + + expect(OpenGroupManager.shared.cache.isPolling).to(beFalse()) + } + } + + // MARK: - Adding & Removing + + context("when adding") { + beforeEach { + mockStorage.when { $0.removeOpenGroupSequenceNumber(for: any(), on: any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.setOpenGroupPublicKey(for: any(), to: any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.setOpenGroupServer(any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.setOpenGroup(any(), for: any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.setUserCount(to: any(), forOpenGroupWithID: any(), using: any()) }.thenReturn(()) + mockStorage.when { $0.getOpenGroupInboxLatestMessageId(for: any()) }.thenReturn(nil) + mockStorage.when { $0.getOpenGroupOutboxLatestMessageId(for: any()) }.thenReturn(nil) + mockStorage.when { $0.getOpenGroupSequenceNumber(for: any(), on: any()) }.thenReturn(nil) + + mockUserDefaults + .when { $0.object(forKey: SNUserDefaults.Date.lastOpen.rawValue) } + .thenReturn(Date(timeIntervalSince1970: 1234567890)) + } + + it("resets the sequence number of the open group") { + OpenGroupManager.shared + .add( + roomToken: "testRoom", + server: "testServer", + publicKey: "testKey", + isConfigMessage: false, + using: testTransaction, + dependencies: dependencies + ) + .retainUntilComplete() + + expect(mockStorage) + .to( + call(.exactly(times: 1)) { + $0.removeOpenGroupSequenceNumber( + for: "testRoom", + on: "testServer", + using: testTransaction as Any + ) + } + ) + } + + it("sets the public key of the open group server") { + OpenGroupManager.shared + .add( + roomToken: "testRoom", + server: "testServer", + publicKey: "testKey", + isConfigMessage: false, + using: testTransaction, + dependencies: dependencies + ) + .retainUntilComplete() + + expect(mockStorage) + .to( + call(.exactly(times: 1)) { + $0.setOpenGroupPublicKey( + for: "testRoom", + to: "testKey", + using: testTransaction as Any + ) + } + ) + } + + it("adds a poller") { + OpenGroupManager.shared + .add( + roomToken: "testRoom", + server: "testServer", + publicKey: "testKey", + isConfigMessage: false, + using: testTransaction, + dependencies: dependencies + ) + .retainUntilComplete() + + expect(OpenGroupManager.shared.cache.pollers["testServer"]) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + } + + context("an existing room") { + beforeEach { + OpenGroupManager.shared + .add( + roomToken: "testRoom", + server: "testServer", + publicKey: "testKey", + isConfigMessage: false, + using: testTransaction, + dependencies: dependencies + ) + .retainUntilComplete() + + // There is a bunch of async code in this function so we need to wait until if finishes + // processing before doing the actual tests + expect(mockStorage) + .toEventually( + call { $0.setOpenGroup(any(), for: "testServer", using: testTransaction as Any) }, + timeout: .milliseconds(100) + ) + + mockStorage.resetCallCounts() + } + + it("does not reset the sequence number or update the public key") { + OpenGroupManager.shared + .add( + roomToken: "testRoom", + server: "testServer", + publicKey: "testKey", + isConfigMessage: false, + using: testTransaction, + dependencies: dependencies + ) + .retainUntilComplete() + + expect(mockStorage) + .toEventuallyNot( + call { + $0.removeOpenGroupSequenceNumber( + for: "testRoom", + on: "testServer", + using: testTransaction as Any + ) + }, + timeout: .milliseconds(100) + ) + expect(mockStorage) + .toEventuallyNot( + call { + $0.setOpenGroupPublicKey( + for: "testRoom", + to: "testKey", + using: testTransaction as Any + ) + }, + timeout: .milliseconds(100) + ) + } + } + + context("with an invalid response") { + beforeEach { + class TestApi: TestOnionRequestAPI { + override class var mockResponse: Data? { return Data() } + } + dependencies = dependencies.with(onionApi: TestApi.self) + + mockUserDefaults + .when { $0.object(forKey: SNUserDefaults.Date.lastOpen.rawValue) } + .thenReturn(Date(timeIntervalSince1970: 1234567890)) + } + + it("fails with the error") { + var error: Error? + + let promise = OpenGroupManager.shared + .add( + roomToken: "testRoom", + server: "testServer", + publicKey: "testKey", + isConfigMessage: false, + using: testTransaction, + dependencies: dependencies + ) + promise.catch { error = $0 } + promise.retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(HTTP.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + } + } + } + + context("when removing") { + +// public func delete(_ openGroup: OpenGroup, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction, dependencies: Dependencies = Dependencies()) { +// // Stop the poller if needed +// let openGroups = dependencies.storage.getAllOpenGroups().values.filter { $0.server == openGroup.server } +// if openGroups.count == 1 && openGroups.last == openGroup { +// let poller = pollers[openGroup.server] +// poller?.stop() +// pollers[openGroup.server] = nil +// } +// +// // Remove all data +// var messageIDs: Set = [] +// var messageTimestamps: Set = [] +// thread.enumerateInteractions(with: transaction) { interaction, _ in +// messageIDs.insert(interaction.uniqueId!) +// messageTimestamps.insert(interaction.timestamp) +// } +// dependencies.storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction) +// dependencies.storage.removeReceivedMessageTimestamps(messageTimestamps, using: transaction) +// dependencies.storage.removeOpenGroupSequenceNumber(for: openGroup.room, on: openGroup.server, using: transaction) +// +// thread.removeAllThreadInteractions(with: transaction) +// thread.remove(with: transaction) +// dependencies.storage.removeOpenGroup(for: thread.uniqueId!, using: transaction) +// +// // Only remove the open group public key and server info if the user isn't in any other rooms +// if openGroups.count <= 1 { +// dependencies.storage.removeOpenGroupServer(name: openGroup.server, using: transaction) +// dependencies.storage.removeOpenGroupPublicKey(for: openGroup.server, using: transaction) +// } + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift b/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift index 733ca8ef8..7147b95fa 100644 --- a/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift @@ -34,6 +34,8 @@ class SOGSEndpointSpec: QuickSpec { expect(OpenGroupAPI.Endpoint.roomMessagesBefore("test", id: 123).path).to(equal("room/test/messages/before/123")) expect(OpenGroupAPI.Endpoint.roomMessagesSince("test", seqNo: 123).path) .to(equal("room/test/messages/since/123")) + expect(OpenGroupAPI.Endpoint.roomDeleteMessages("test", sessionId: "testId").path) + .to(equal("room/test/all/testId")) // Pinning @@ -60,7 +62,6 @@ class SOGSEndpointSpec: QuickSpec { expect(OpenGroupAPI.Endpoint.userBan("test").path).to(equal("user/test/ban")) expect(OpenGroupAPI.Endpoint.userUnban("test").path).to(equal("user/test/unban")) expect(OpenGroupAPI.Endpoint.userModerator("test").path).to(equal("user/test/moderator")) - expect(OpenGroupAPI.Endpoint.userDeleteMessages("test").path).to(equal("user/test/deleteMessages")) } } } diff --git a/SessionMessagingKitTests/_TestUtilities/MockEd25519.swift b/SessionMessagingKitTests/_TestUtilities/MockEd25519.swift index ce05862dd..23632d67e 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockEd25519.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockEd25519.swift @@ -7,6 +7,10 @@ import Sodium @testable import SessionMessagingKit class MockEd25519: Mock, Ed25519Type { + func sign(data: Bytes, keyPair: ECKeyPair) throws -> Bytes? { + return accept(args: [data, keyPair]) as? Bytes + } + func verifySignature(_ signature: Data, publicKey: Data, data: Data) throws -> Bool { return accept(args: [signature, publicKey, data]) as! Bool } diff --git a/SessionMessagingKitTests/_TestUtilities/MockUserDefaults.swift b/SessionMessagingKitTests/_TestUtilities/MockUserDefaults.swift new file mode 100644 index 000000000..4814016a5 --- /dev/null +++ b/SessionMessagingKitTests/_TestUtilities/MockUserDefaults.swift @@ -0,0 +1,27 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +class MockUserDefaults: Mock, UserDefaultsType { + var storage: [String: Any] = [:] + + func object(forKey defaultName: String) -> Any? { return accept(args: [defaultName]) } + func string(forKey defaultName: String) -> String? { return accept(args: [defaultName]) as? String } + func array(forKey defaultName: String) -> [Any]? { return accept(args: [defaultName]) as? [Any] } + func dictionary(forKey defaultName: String) -> [String: Any]? { return accept(args: [defaultName]) as? [String: Any] } + func data(forKey defaultName: String) -> Data? { return accept(args: [defaultName]) as? Data } + func stringArray(forKey defaultName: String) -> [String]? { return accept(args: [defaultName]) as? [String] } + func integer(forKey defaultName: String) -> Int { return ((accept(args: [defaultName]) as? Int) ?? 0) } + func float(forKey defaultName: String) -> Float { return ((accept(args: [defaultName]) as? Float) ?? 0) } + func double(forKey defaultName: String) -> Double { return ((accept(args: [defaultName]) as? Double) ?? 0) } + func bool(forKey defaultName: String) -> Bool { return ((accept(args: [defaultName]) as? Bool) ?? false) } + func url(forKey defaultName: String) -> URL? { return accept(args: [defaultName]) as? URL } + + func set(_ value: Any?, forKey defaultName: String) { accept(args: [value, defaultName]) } + func set(_ value: Int, forKey defaultName: String) { accept(args: [value, defaultName]) } + func set(_ value: Float, forKey defaultName: String) { accept(args: [value, defaultName]) } + func set(_ value: Double, forKey defaultName: String) { accept(args: [value, defaultName]) } + func set(_ value: Bool, forKey defaultName: String) { accept(args: [value, defaultName]) } + func set(_ url: URL?, forKey defaultName: String) { accept(args: [url, defaultName]) } +} diff --git a/SessionMessagingKitTests/_TestUtilities/Mockable.swift b/SessionMessagingKitTests/_TestUtilities/Mockable.swift index 6c438d881..b903f0fa3 100644 --- a/SessionMessagingKitTests/_TestUtilities/Mockable.swift +++ b/SessionMessagingKitTests/_TestUtilities/Mockable.swift @@ -7,9 +7,3 @@ protocol Mockable { var mockData: [Key: Any] { get } } - -protocol StaticMockable { - associatedtype Key: Hashable - - static var mockData: [Key: Any] { get } -} diff --git a/SessionMessagingKitTests/_TestUtilities/MockedExtensions.swift b/SessionMessagingKitTests/_TestUtilities/MockedExtensions.swift new file mode 100644 index 000000000..0824acabf --- /dev/null +++ b/SessionMessagingKitTests/_TestUtilities/MockedExtensions.swift @@ -0,0 +1,23 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionMessagingKit + +extension OpenGroup: Mocked { + static var mockValue: OpenGroup = OpenGroup( + server: any(), + room: any(), + publicKey: TestConstants.publicKey, + name: any(), + groupDescription: any(), + imageID: any(), + infoUpdates: any() + ) +} + +extension OpenGroupAPI.Server: Mocked { + static var mockValue: OpenGroupAPI.Server = OpenGroupAPI.Server( + name: any(), + capabilities: OpenGroupAPI.Capabilities(capabilities: any(), missing: any()) + ) +} diff --git a/SessionMessagingKitTests/_TestUtilities/TestGroupThread.swift b/SessionMessagingKitTests/_TestUtilities/TestGroupThread.swift new file mode 100644 index 000000000..4ec2469eb --- /dev/null +++ b/SessionMessagingKitTests/_TestUtilities/TestGroupThread.swift @@ -0,0 +1,32 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionMessagingKit + +// FIXME: Turn this into a protocol to make mocking possible +class TestGroupThread: TSGroupThread, Mockable { + // MARK: - Mockable + + enum DataKey: Hashable { + case groupModel + case interactions + } + + typealias Key = DataKey + + var mockData: [DataKey: Any] = [:] + var didCallSave: Bool = false + + // MARK: - TSGroupThread + + override var groupModel: TSGroupModel { + get { (mockData[.groupModel] as! TSGroupModel) } + set {} + } + + override func enumerateInteractions(_ block: @escaping (TSInteraction) -> Void) { + ((mockData[.interactions] as? [TSInteraction]) ?? []).forEach(block) + } + + override func save(with transaction: YapDatabaseReadWriteTransaction) { didCallSave = true } +} diff --git a/SessionMessagingKitTests/_TestUtilities/TestInteraction.swift b/SessionMessagingKitTests/_TestUtilities/TestInteraction.swift index 0a8ab0dac..353c97a9b 100644 --- a/SessionMessagingKitTests/_TestUtilities/TestInteraction.swift +++ b/SessionMessagingKitTests/_TestUtilities/TestInteraction.swift @@ -1,3 +1,32 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import SessionMessagingKit + +// FIXME: Turn this into a protocol to make mocking possible +class TestInteraction: TSInteraction, Mockable { + // MARK: - Mockable + + enum DataKey: Hashable { + case uniqueId + case timestamp + } + + typealias Key = DataKey + + var mockData: [DataKey: Any] = [:] + var didCallSave: Bool = true + + // MARK: - TSInteraction + + override var uniqueId: String? { + get { (mockData[.uniqueId] as? String) } + set { mockData[.uniqueId] = newValue } + } + + override var timestamp: UInt64 { + (mockData[.timestamp] as! UInt64) + } + + override func save(with transaction: YapDatabaseReadWriteTransaction) { didCallSave = true } +} diff --git a/SessionMessagingKitTests/_TestUtilities/TestOnionRequestAPI.swift b/SessionMessagingKitTests/_TestUtilities/TestOnionRequestAPI.swift new file mode 100644 index 000000000..fd1789626 --- /dev/null +++ b/SessionMessagingKitTests/_TestUtilities/TestOnionRequestAPI.swift @@ -0,0 +1,60 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import PromiseKit +import SessionSnodeKit + +@testable import SessionMessagingKit + +// FIXME: Change 'OnionRequestAPIType' to have instance methods instead of static methods once everything is updated to use 'Dependencies' +class TestOnionRequestAPI: OnionRequestAPIType { + struct RequestData: Codable { + let urlString: String? + let httpMethod: String + let headers: [String: String] + let snodeMethod: String? + let body: Data? + + let server: String + let version: OnionRequestAPI.Version + let publicKey: String? + } + class ResponseInfo: OnionRequestResponseInfoType { + let requestData: RequestData + let code: Int + let headers: [String: String] + + init(requestData: RequestData, code: Int, headers: [String: String]) { + self.requestData = requestData + self.code = code + self.headers = headers + } + } + + class var mockResponse: Data? { return nil } + + static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPI.Version, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, 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 + ), + code: 200, + headers: [:] + ) + + return Promise.value((responseInfo, mockResponse)) + } + + static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, using version: OnionRequestAPI.Version, associatedWith publicKey: String?) -> Promise { + // TODO: Test the 'responseInfo' somehow? + return Promise.value(mockResponse!) + } +} diff --git a/SessionMessagingKitTests/_TestUtilities/TestThread.swift b/SessionMessagingKitTests/_TestUtilities/TestThread.swift index 0a8ab0dac..6ef2ce18a 100644 --- a/SessionMessagingKitTests/_TestUtilities/TestThread.swift +++ b/SessionMessagingKitTests/_TestUtilities/TestThread.swift @@ -1,3 +1,26 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import SessionMessagingKit + +// FIXME: Turn this into a protocol to make mocking possible +class TestThread: TSThread, Mockable { + // MARK: - Mockable + + enum DataKey: Hashable { + case interactions + } + + typealias Key = DataKey + + var mockData: [DataKey: Any] = [:] + var didCallSave: Bool = false + + // MARK: - TSThread + + override func enumerateInteractions(_ block: @escaping (TSInteraction) -> Void) { + ((mockData[.interactions] as? [TSInteraction]) ?? []).forEach(block) + } + + override func save(with transaction: YapDatabaseReadWriteTransaction) { didCallSave = true } +} diff --git a/SessionMessagingKitTests/_TestUtilities/TestTransaction.swift b/SessionMessagingKitTests/_TestUtilities/TestTransaction.swift new file mode 100644 index 000000000..0cd847d91 --- /dev/null +++ b/SessionMessagingKitTests/_TestUtilities/TestTransaction.swift @@ -0,0 +1,31 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import YapDatabase + +// FIXME: Turn this into a protocol to make mocking possible +final class TestTransaction: YapDatabaseReadWriteTransaction, Mockable { + // MARK: - Mockable + + enum DataKey: Hashable { + case objectForKey + } + + typealias Key = DataKey + + var mockData: [DataKey: Any] = [:] + + // MARK: - YapDatabaseReadWriteTransaction + + override func object(forKey key: String, inCollection collection: String?) -> Any? { + return mockData[.objectForKey] + } + + override func addCompletionQueue(_ completionQueue: DispatchQueue?, completionBlock: @escaping () -> Void) { + completionBlock() + } +} + +extension TestTransaction: Mocked { + static var mockValue: TestTransaction = TestTransaction() +} diff --git a/SessionMessagingKitTests/_TestUtilities/TestUserDefaults.swift b/SessionMessagingKitTests/_TestUtilities/TestUserDefaults.swift deleted file mode 100644 index a01b5eda2..000000000 --- a/SessionMessagingKitTests/_TestUtilities/TestUserDefaults.swift +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import SessionUtilitiesKit - -class TestUserDefaults: UserDefaultsType { - var storage: [String: Any] = [:] - - func object(forKey defaultName: String) -> Any? { return storage[defaultName] } - func string(forKey defaultName: String) -> String? { return storage[defaultName] as? String } - func array(forKey defaultName: String) -> [Any]? { return storage[defaultName] as? [Any] } - func dictionary(forKey defaultName: String) -> [String: Any]? { return storage[defaultName] as? [String: Any] } - func data(forKey defaultName: String) -> Data? { return storage[defaultName] as? Data } - func stringArray(forKey defaultName: String) -> [String]? { return storage[defaultName] as? [String] } - func integer(forKey defaultName: String) -> Int { return ((storage[defaultName] as? Int) ?? 0) } - func float(forKey defaultName: String) -> Float { return ((storage[defaultName] as? Float) ?? 0) } - func double(forKey defaultName: String) -> Double { return ((storage[defaultName] as? Double) ?? 0) } - func bool(forKey defaultName: String) -> Bool { return ((storage[defaultName] as? Bool) ?? false) } - func url(forKey defaultName: String) -> URL? { return storage[defaultName] as? URL } - - func set(_ value: Any?, forKey defaultName: String) { storage[defaultName] = value } - func set(_ value: Int, forKey defaultName: String) { storage[defaultName] = value } - func set(_ value: Float, forKey defaultName: String) { storage[defaultName] = value } - func set(_ value: Double, forKey defaultName: String) { storage[defaultName] = value } - func set(_ value: Bool, forKey defaultName: String) { storage[defaultName] = value } - func set(_ url: URL?, forKey defaultName: String) { storage[defaultName] = url } -} diff --git a/SessionMessagingKit/Utilities/Atomic.swift b/SessionUtilitiesKit/General/Atomic.swift similarity index 93% rename from SessionMessagingKit/Utilities/Atomic.swift rename to SessionUtilitiesKit/General/Atomic.swift index 7d8b07d95..e3c2bbf9b 100644 --- a/SessionMessagingKit/Utilities/Atomic.swift +++ b/SessionUtilitiesKit/General/Atomic.swift @@ -27,13 +27,13 @@ public class Atomic { // MARK: - Initialization - init(_ initialValue: Value) { + public init(_ initialValue: Value) { self.value = initialValue } // MARK: - Functions - func mutate(_ mutation: (inout Value) -> Void) { + public func mutate(_ mutation: (inout Value) -> Void) { return queue.sync { mutation(&value) } diff --git a/SessionMessagingKitTests/_TestUtilities/BoxKeyPair+Mocked.swift b/SharedTest/CommonMockedExtensions.swift similarity index 54% rename from SessionMessagingKitTests/_TestUtilities/BoxKeyPair+Mocked.swift rename to SharedTest/CommonMockedExtensions.swift index 0a6d2ed2a..bbc01747b 100644 --- a/SessionMessagingKitTests/_TestUtilities/BoxKeyPair+Mocked.swift +++ b/SharedTest/CommonMockedExtensions.swift @@ -2,6 +2,7 @@ import Foundation import Sodium +import Curve25519Kit extension Box.KeyPair: Mocked { static var mockValue: Box.KeyPair = Box.KeyPair( @@ -9,3 +10,12 @@ extension Box.KeyPair: Mocked { secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes ) } + +extension ECKeyPair: Mocked { + static var mockValue: Self { + try! Self.init( + publicKeyData: Data.data(fromHex: TestConstants.publicKey)!, + privateKeyData: Data.data(fromHex: TestConstants.privateKey)! + ) + } +} diff --git a/SharedTest/Mock.swift b/SharedTest/Mock.swift index e22321b68..35c270f15 100644 --- a/SharedTest/Mock.swift +++ b/SharedTest/Mock.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import SessionUtilitiesKit // MARK: - Mocked @@ -22,11 +23,15 @@ public class Mock { private let functionHandler: MockFunctionHandler internal let functionConsumer: FunctionConsumer + // MARK: - Initialization + internal required init(functionHandler: MockFunctionHandler? = nil) { self.functionConsumer = FunctionConsumer() self.functionHandler = (functionHandler ?? self.functionConsumer) } + // MARK: - MockFunctionHandler + @discardableResult internal func accept(funcName: String = #function, args: [Any?] = []) -> Any? { return accept(funcName: funcName, checkArgs: args, actionArgs: args) } @@ -35,6 +40,19 @@ public class Mock { return functionHandler.accept(funcName, parameterSummary: summary(for: checkArgs), actionArgs: actionArgs) } + // MARK: - Functions + + internal func reset() { + functionConsumer.trackCalls = true + functionConsumer.functionBuilders = [] + functionConsumer.functionHandlers = [:] + functionConsumer.calls.mutate { $0 = [:] } + } + + internal func resetCallCounts() { + functionConsumer.calls.mutate { $0 = [:] } + } + internal func when(_ callBlock: @escaping (T) throws -> R) -> MockFunctionBuilder { let builder: MockFunctionBuilder = MockFunctionBuilder(callBlock, mockInit: type(of: self).init) functionConsumer.functionBuilders.append(builder.build) @@ -42,6 +60,8 @@ public class Mock { return builder } + // MARK: - Convenience + private func summary(for argument: Any) -> String { switch argument { case let string as String: return string @@ -134,7 +154,7 @@ internal class FunctionConsumer: MockFunctionHandler { var trackCalls: Bool = true var functionBuilders: [() throws -> MockFunction?] = [] var functionHandlers: [String: [String: MockFunction]] = [:] - var calls: [String: [String]] = [:] + var calls: Atomic<[String: [String]]> = Atomic([:]) func accept(_ functionName: String, parameterSummary: String, actionArgs: [Any?]) -> Any? { if !functionBuilders.isEmpty { @@ -154,7 +174,7 @@ internal class FunctionConsumer: MockFunctionHandler { // Record the call so it can be validated later (assuming we are tracking calls) if trackCalls { - calls[functionName] = (calls[functionName] ?? []).appending(parameterSummary) + calls.mutate { $0[functionName] = ($0[functionName] ?? []).appending(parameterSummary) } } for action in expectation.actions { diff --git a/SharedTest/NimbleExtensions.swift b/SharedTest/NimbleExtensions.swift index 6fe5bafda..afed2e7b0 100644 --- a/SharedTest/NimbleExtensions.swift +++ b/SharedTest/NimbleExtensions.swift @@ -181,12 +181,12 @@ fileprivate func generateCallInfo(_ actualExpression: Expression, _ return } - allFunctionsCalled = Array(validInstance.functionConsumer.calls.keys) + allFunctionsCalled = Array(validInstance.functionConsumer.calls.wrappedValue.keys) let builder: MockFunctionBuilder = builderCreator(validInstance) validInstance.functionConsumer.trackCalls = false maybeFunction = try? builder.build() - desiredFunctionCalls = (validInstance.functionConsumer.calls[maybeFunction?.name ?? ""] ?? []) + desiredFunctionCalls = (validInstance.functionConsumer.calls.wrappedValue[maybeFunction?.name ?? ""] ?? []) validInstance.functionConsumer.trackCalls = true } catch { @@ -205,12 +205,12 @@ fileprivate func generateCallInfo(_ actualExpression: Expression, _ // Just hope for the best and if there is a force-cast there's not much we can do guard let validInstance: M = try? actualExpression.evaluate() else { return CallInfo.error } - allFunctionsCalled = Array(validInstance.functionConsumer.calls.keys) + allFunctionsCalled = Array(validInstance.functionConsumer.calls.wrappedValue.keys) let builder: MockExpectationBuilder = builderCreator(validInstance) validInstance.functionConsumer.trackCalls = false maybeFunction = try? builder.build() - desiredFunctionCalls = (validInstance.functionConsumer.calls[maybeFunction?.name ?? ""] ?? []) + desiredFunctionCalls = (validInstance.functionConsumer.calls.wrappedValue[maybeFunction?.name ?? ""] ?? []) validInstance.functionConsumer.trackCalls = true #endif