From f9c2655df48154c41a174417fdead6ac8b869e2b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 15 Mar 2022 15:19:23 +1100 Subject: [PATCH] Finalised the OpenGroupAPI and more tests Fixed an issue where messages where signed incorrectly when blinding wasn't enabled on a SOGS Fixed an issue where a single invalid message would result in all messages in that request being dropped Updated the final legacy endpoint (ban and delete all messages) Moved the OpenGroupManager poller values into the 'Cache' (so they are thread safe) Started adding unit tests for the OpenGroupManager Removed some redundant parameters from the 'Request' type --- Session.xcodeproj/project.pbxproj | 46 +- .../ConversationVC+Interaction.swift | 4 +- .../Common Networking/Request.swift | 10 +- .../File Server/FileServerAPI.swift | 4 - .../Open Groups/OpenGroupAPI.swift | 182 +++--- .../Open Groups/OpenGroupManager.swift | 96 +-- .../Open Groups/Types/SOGSEndpoint.swift | 6 +- .../Open Groups/Types/SodiumProtocols.swift | 5 + .../Sending & Receiving/MessageSender.swift | 3 +- .../Pollers/OpenGroupPoller.swift | 55 +- .../Utilities/Dependencies.swift | 16 +- SessionMessagingKit/Utilities/Failable.swift | 24 + .../Open Groups/OpenGroupAPISpec.swift | 525 ++++++++++------ .../Open Groups/OpenGroupManagerSpec.swift | 558 +++++++++++++++++- .../Open Groups/Types/SOGSEndpointSpec.swift | 3 +- .../_TestUtilities/MockEd25519.swift | 4 + .../_TestUtilities/MockUserDefaults.swift | 27 + .../_TestUtilities/Mockable.swift | 6 - .../_TestUtilities/MockedExtensions.swift | 23 + .../_TestUtilities/TestGroupThread.swift | 32 + .../_TestUtilities/TestInteraction.swift | 29 + .../_TestUtilities/TestOnionRequestAPI.swift | 60 ++ .../_TestUtilities/TestThread.swift | 23 + .../_TestUtilities/TestTransaction.swift | 31 + .../_TestUtilities/TestUserDefaults.swift | 27 - .../General}/Atomic.swift | 4 +- .../CommonMockedExtensions.swift | 10 + SharedTest/Mock.swift | 24 +- SharedTest/NimbleExtensions.swift | 8 +- 29 files changed, 1419 insertions(+), 426 deletions(-) create mode 100644 SessionMessagingKit/Utilities/Failable.swift create mode 100644 SessionMessagingKitTests/_TestUtilities/MockUserDefaults.swift create mode 100644 SessionMessagingKitTests/_TestUtilities/MockedExtensions.swift create mode 100644 SessionMessagingKitTests/_TestUtilities/TestGroupThread.swift create mode 100644 SessionMessagingKitTests/_TestUtilities/TestOnionRequestAPI.swift create mode 100644 SessionMessagingKitTests/_TestUtilities/TestTransaction.swift delete mode 100644 SessionMessagingKitTests/_TestUtilities/TestUserDefaults.swift rename {SessionMessagingKit/Utilities => SessionUtilitiesKit/General}/Atomic.swift (93%) rename SessionMessagingKitTests/_TestUtilities/BoxKeyPair+Mocked.swift => SharedTest/CommonMockedExtensions.swift (54%) 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