From 4963a84ddd092646f44ab5680933a24ef23ec5dd Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 15 Feb 2022 16:00:51 +1100 Subject: [PATCH] Added more unit tests for the OpenGroupAPI (fixed a couple bugs as well) --- Session.xcodeproj/project.pbxproj | 32 +- .../xcschemes/SessionMessagingKit.xcscheme | 12 +- .../Common Networking/Header.swift | 1 + .../Common Networking/QueryParam.swift | 1 - .../Database/Storage+OpenGroups.swift | 7 +- .../Open Groups/Models/BatchRequestInfo.swift | 6 +- .../Open Groups/OpenGroupAPIV2.swift | 175 +++---- .../Open Groups/Types/Dependencies.swift | 52 ++ .../Open Groups/Types/Request.swift | 8 +- .../Open Groups/Types/SodiumProtocols.swift | 27 ++ SessionMessagingKit/Storage.swift | 2 +- .../Open Groups/OpenGroupAPIV2Tests.swift | 455 +++++++++++++++--- .../_TestUtilities/TestStorage.swift | 8 + .../General/String+Encoding.swift | 2 +- 14 files changed, 613 insertions(+), 175 deletions(-) create mode 100644 SessionMessagingKit/Open Groups/Types/Dependencies.swift create mode 100644 SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index b51ac4466..f65d6f9b8 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -151,6 +151,7 @@ A1C32D5017A06538000A904E /* AddressBookUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1C32D4F17A06537000A904E /* AddressBookUI.framework */; }; A1C32D5117A06544000A904E /* AddressBook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1C32D4D17A0652C000A904E /* AddressBook.framework */; }; A5509ECA1A69AB8B00ABA4BC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A5509EC91A69AB8B00ABA4BC /* Main.storyboard */; }; + B5FE70D512E75D659386BAD4 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F1E0F51F17E4443731B94D32 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework */; }; B66DBF4A19D5BBC8006EA940 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; }; B67EBF5D19194AC60084CCFD /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B67EBF5C19194AC60084CCFD /* Settings.bundle */; }; B6B226971BE4B7D200860F4D /* ContactsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6B226961BE4B7D200860F4D /* ContactsUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; @@ -767,7 +768,6 @@ D24B5BD5169F568C00681372 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D24B5BD4169F568C00681372 /* AudioToolbox.framework */; }; D2AEACDC16C426DA00C364C0 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2AEACDB16C426DA00C364C0 /* CFNetwork.framework */; }; D48CEFD2222D323FEFEFC6CC /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8962372EEC51D3F56FE3A68A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework */; }; - E197F4A653289312F13926E6 /* Pods_SessionMessagingKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CF061FF98D2BFA3ECCF8C6F6 /* Pods_SessionMessagingKitTests.framework */; }; EF764C351DB67CC5000D9A87 /* UIViewController+Permissions.m in Sources */ = {isa = PBXBuildFile; fileRef = EF764C341DB67CC5000D9A87 /* UIViewController+Permissions.m */; }; 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 */; }; @@ -842,6 +842,8 @@ FDC438B727BB160000C60D73 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438B627BB160000C60D73 /* Error.swift */; }; FDC438B927BB161E00C60D73 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438B827BB161E00C60D73 /* Version.swift */; }; FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438BC27BB2AB400C60D73 /* Mockable.swift */; }; + FDC438C127BB4E6800C60D73 /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C027BB4E6800C60D73 /* Dependencies.swift */; }; + FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C227BB512200C60D73 /* SodiumProtocols.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1022,6 +1024,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0208C84C4D15048D699BEC10 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig"; sourceTree = ""; }; 038A3BABD5BA0CE41D8C17F5 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0840117FDFD286D1CC14A2E1 /* Pods-SessionTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionTests/Pods-SessionTests.debug.xcconfig"; sourceTree = ""; }; 0D3D13FEE4FF6A2E2ED85322 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; @@ -1227,6 +1230,7 @@ 8981C8F64D94D3C52EB67A2C /* Pods-SignalTests.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.test.xcconfig"; sourceTree = ""; }; 8EEE74B0753448C085B48721 /* Pods-SignalMessaging.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.app store release.xcconfig"; sourceTree = ""; }; 948239851C08032C842937CC /* Pods-SignalMessaging.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.test.xcconfig"; sourceTree = ""; }; + 949F269926ABA08C125DCA9D /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig"; sourceTree = ""; }; 9AE1058A3BB2148A279432B2 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.debug.xcconfig"; sourceTree = ""; }; 9B3329176C10E9640865E65B /* Pods-GlobalDependencies-Session.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-Session.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-Session/Pods-GlobalDependencies-Session.debug.xcconfig"; sourceTree = ""; }; 9B533A9FA46206D3D99C9ADA /* Pods-SignalMessaging.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.debug.xcconfig"; sourceTree = ""; }; @@ -1869,7 +1873,6 @@ C8153B96A292A25045BE2C54 /* Pods-SessionTests.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionTests.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionTests/Pods-SessionTests.app store release.xcconfig"; sourceTree = ""; }; C88965DE4F4EC4FC919BEC4E /* Pods-SessionUIKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUIKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionUIKit/Pods-SessionUIKit.debug.xcconfig"; sourceTree = ""; }; C98441E849C3CA7FE8220D33 /* Pods-SessionNotificationServiceExtension.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionNotificationServiceExtension.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionNotificationServiceExtension/Pods-SessionNotificationServiceExtension.app store release.xcconfig"; sourceTree = ""; }; - CF061FF98D2BFA3ECCF8C6F6 /* Pods_SessionMessagingKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionMessagingKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D2179CFB16BB0B3A0006F3AB /* CoreTelephony.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreTelephony.framework; path = System/Library/Frameworks/CoreTelephony.framework; sourceTree = SDKROOT; }; D2179CFD16BB0B480006F3AB /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; D221A089169C9E5E00537ABF /* Session.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Session.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1894,6 +1897,7 @@ EF764C331DB67CC5000D9A87 /* UIViewController+Permissions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+Permissions.h"; sourceTree = ""; }; EF764C341DB67CC5000D9A87 /* UIViewController+Permissions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+Permissions.m"; sourceTree = ""; }; F121FB43E2A1C1CF7F2AFC23 /* Pods-SessionPushNotificationExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionPushNotificationExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionPushNotificationExtension/Pods-SessionPushNotificationExtension.debug.xcconfig"; sourceTree = ""; }; + F1E0F51F17E4443731B94D32 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F4DC483F404B65C59D9C2CF8 /* Pods-SessionMessagingKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKitTests/Pods-SessionMessagingKitTests.debug.xcconfig"; sourceTree = ""; }; F62ECF7B8AF4F8089AA705B3 /* Pods-LokiPushNotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LokiPushNotificationService.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LokiPushNotificationService/Pods-LokiPushNotificationService.debug.xcconfig"; sourceTree = ""; }; 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 = ""; }; @@ -1968,6 +1972,8 @@ FDC438B627BB160000C60D73 /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; FDC438B827BB161E00C60D73 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; FDC438BC27BB2AB400C60D73 /* Mockable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mockable.swift; sourceTree = ""; }; + FDC438C027BB4E6800C60D73 /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = ""; }; + FDC438C227BB512200C60D73 /* SodiumProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SodiumProtocols.swift; sourceTree = ""; }; FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = ""; }; FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2089,7 +2095,7 @@ buildActionMask = 2147483647; files = ( FDC4389227B9FFC700C60D73 /* SessionMessagingKit.framework in Frameworks */, - E197F4A653289312F13926E6 /* Pods_SessionMessagingKitTests.framework in Frameworks */, + B5FE70D512E75D659386BAD4 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2303,6 +2309,8 @@ A0FB43B511403A5FAFAC88B8 /* Pods-SessionMessagingKitTests.app store release.xcconfig */, 0840117FDFD286D1CC14A2E1 /* Pods-SessionTests.debug.xcconfig */, C8153B96A292A25045BE2C54 /* Pods-SessionTests.app store release.xcconfig */, + 949F269926ABA08C125DCA9D /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig */, + 0208C84C4D15048D699BEC10 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -3761,8 +3769,8 @@ 038A3BABD5BA0CE41D8C17F5 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework */, C5060C3B36A848B71CCE4685 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework */, 8962372EEC51D3F56FE3A68A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework */, - CF061FF98D2BFA3ECCF8C6F6 /* Pods_SessionMessagingKitTests.framework */, D2C155B76C8483CB9A6EA9B4 /* Pods_SessionTests.framework */, + F1E0F51F17E4443731B94D32 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -3814,6 +3822,8 @@ FDC4380827B31D4E00C60D73 /* Error.swift */, FDC4381627B32EC700C60D73 /* Personalization.swift */, FDC4381427B329CE00C60D73 /* NonceGenerator16Byte.swift */, + FDC438C027BB4E6800C60D73 /* Dependencies.swift */, + FDC438C227BB512200C60D73 /* SodiumProtocols.swift */, ); path = Types; sourceTree = ""; @@ -4666,15 +4676,15 @@ files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SessionMessagingKitTests/Pods-SessionMessagingKitTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SessionMessagingKitTests/Pods-SessionMessagingKitTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SessionMessagingKitTests/Pods-SessionMessagingKitTests-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 7E2D14F857C70F98DED3B8E9 /* [CP] Check Pods Manifest.lock */ = { @@ -4736,7 +4746,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-SessionMessagingKitTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -5210,6 +5220,7 @@ FDC4384827B47F4D00C60D73 /* LegacyRoomInfo.swift in Sources */, C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */, C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */, + FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */, B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */, C3DB66C3260ACCE6001EFC55 /* OpenGroupPollerV2.swift in Sources */, FDC438AE27BB148700C60D73 /* UserDeleteMessagesResponse.swift in Sources */, @@ -5230,6 +5241,7 @@ C32C5FD6256E0346003C73A2 /* Notification+Thread.swift in Sources */, FDC438A827BB11CD00C60D73 /* UserPermissionsRequest.swift in Sources */, C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */, + FDC438C127BB4E6800C60D73 /* Dependencies.swift in Sources */, FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */, C32C5C88256DD0D2003C73A2 /* Storage+Messaging.swift in Sources */, C32C59C7256DB41F003C73A2 /* TSThread.m in Sources */, @@ -6878,7 +6890,7 @@ }; FDC4389627B9FFC700C60D73 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F4DC483F404B65C59D9C2CF8 /* Pods-SessionMessagingKitTests.debug.xcconfig */; + baseConfigurationReference = 949F269926ABA08C125DCA9D /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -6919,7 +6931,7 @@ }; FDC4389727B9FFC700C60D73 /* App Store Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A0FB43B511403A5FAFAC88B8 /* Pods-SessionMessagingKitTests.app store release.xcconfig */; + baseConfigurationReference = 0208C84C4D15048D699BEC10 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; diff --git a/Session.xcodeproj/xcshareddata/xcschemes/SessionMessagingKit.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/SessionMessagingKit.xcscheme index d10ea9ea8..cbee62b91 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/SessionMessagingKit.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/SessionMessagingKit.xcscheme @@ -27,7 +27,17 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" - codeCoverageEnabled = "YES"> + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + diff --git a/SessionMessagingKit/Common Networking/Header.swift b/SessionMessagingKit/Common Networking/Header.swift index 9081fbf05..56b37c988 100644 --- a/SessionMessagingKit/Common Networking/Header.swift +++ b/SessionMessagingKit/Common Networking/Header.swift @@ -7,6 +7,7 @@ enum Header: String { case contentType = "Content-Type" case room = "Room" // TODO: Confirm this is needed + case fileName = "X-Filename" case sogsPubKey = "X-SOGS-Pubkey" case sogsNonce = "X-SOGS-Nonce" diff --git a/SessionMessagingKit/Common Networking/QueryParam.swift b/SessionMessagingKit/Common Networking/QueryParam.swift index 611b30eb8..81e9d849e 100644 --- a/SessionMessagingKit/Common Networking/QueryParam.swift +++ b/SessionMessagingKit/Common Networking/QueryParam.swift @@ -7,6 +7,5 @@ enum QueryParam: String { case fromServerId = "from_server_id" case required = "required" - case fileName = "X-Filename" case limit // For messages - number between 1 and 256 (default is 100) } diff --git a/SessionMessagingKit/Database/Storage+OpenGroups.swift b/SessionMessagingKit/Database/Storage+OpenGroups.swift index 5ea663550..a769b2fd7 100644 --- a/SessionMessagingKit/Database/Storage+OpenGroups.swift +++ b/SessionMessagingKit/Database/Storage+OpenGroups.swift @@ -1,5 +1,10 @@ -extension Storage { +public protocol SessionMessagingKitOpenGroupStorageProtocol { + func getOpenGroupImage(for room: String, on server: String) -> Data? + func setOpenGroupImage(to data: Data, for room: String, on server: String, using transaction: Any) +} + +extension Storage: SessionMessagingKitOpenGroupStorageProtocol { // MARK: - Open Groups diff --git a/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift b/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift index e75ceb191..b5ac2812a 100644 --- a/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift +++ b/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift @@ -68,7 +68,7 @@ public extension Decodable { } extension Promise where T == (OnionRequestResponseInfoType, Data?) { - func decoded(as types: OpenGroupAPIV2.BatchResponseTypes, on queue: DispatchQueue? = nil, error: Error? = nil) -> Promise { + func decoded(as types: OpenGroupAPIV2.BatchResponseTypes, on queue: DispatchQueue? = nil, error: Error) -> Promise { self.map(on: queue) { responseInfo, maybeData -> OpenGroupAPIV2.BatchResponse in // Need to split the data into an array of data so each item can be Decoded correctly guard let data: Data = maybeData else { throw OpenGroupAPIV2.Error.parsingFailed } @@ -85,8 +85,8 @@ extension Promise where T == (OnionRequestResponseInfoType, Data?) { .map { data, type in try type.decoded(from: data) } .map { data in (responseInfo, data) } } - catch let thrownError { - throw (error ?? thrownError) + catch _ { + throw error } } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift b/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift index 8d9cd0faf..c490dedcc 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift @@ -197,7 +197,7 @@ public final class OpenGroupAPIV2: NSObject { // MARK: - Capabilities - public static func capabilities(on server: String) -> Promise<(OnionRequestResponseInfoType, Capabilities)> { + public static func capabilities(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Capabilities)> { let request: Request = Request( server: server, endpoint: .capabilities, @@ -205,45 +205,39 @@ public final class OpenGroupAPIV2: NSObject { ) // TODO: Handle a `412` response (ie. a required capability isn't supported). - return send(request) + return send(request, using: dependencies) .decoded(as: Capabilities.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } // MARK: - Room - public static func rooms( - for server: String, - through api: OnionRequestAPIType.Type = OnionRequestAPI.self, - using storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage, - nonceGenerator: NonceGenerator16ByteType = NonceGenerator16Byte(), - date: Date = Date() - ) -> Promise<(OnionRequestResponseInfoType, [Room])> { + public static func rooms(for server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Room])> { let request: Request = Request( server: server, endpoint: .rooms ) - return send(request, through: api, using: storage, nonceGenerator: nonceGenerator, date: date) + return send(request, using: dependencies) .decoded(as: [Room].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } - public static func room(for roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, Room)> { + public static func room(for roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Room)> { let request: Request = Request( server: server, endpoint: .room(roomToken) ) - return send(request) + return send(request, using: dependencies) .decoded(as: Room.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } - public static func roomPollInfo(lastUpdated: Int64, for roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, RoomPollInfo)> { + public static func roomPollInfo(lastUpdated: Int64, for roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, RoomPollInfo)> { let request: Request = Request( server: server, endpoint: .roomPollInfo(roomToken, lastUpdated) ) - return send(request) + return send(request, using: dependencies) .decoded(as: RoomPollInfo.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } @@ -255,7 +249,8 @@ public final class OpenGroupAPIV2: NSObject { on server: String, whisperTo: String?, whisperMods: Bool, - with serverPublicKey: String + with serverPublicKey: String, + using dependencies: Dependencies = Dependencies() ) -> Promise<(OnionRequestResponseInfoType, Message)> { // TODO: Change this to use '.blinded' once it's working. guard let signedRequest: (data: Data, signature: Data) = SendMessageRequest.sign(message: plaintext, for: .standard, with: serverPublicKey) else { @@ -281,12 +276,11 @@ public final class OpenGroupAPIV2: NSObject { body: body ) - return send(request) + return send(request, using: dependencies) .decoded(as: Message.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } - public static func recentMessages(in roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, [Message])> { - // TODO: Recent vs. Since? + public static func recentMessages(in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> { let request: Request = Request( server: server, endpoint: .roomMessagesRecent(roomToken) @@ -296,15 +290,15 @@ public final class OpenGroupAPIV2: NSObject { // ].compactMapValues { $0 } ) - return send(request) + return send(request, using: dependencies) .decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) .then(on: OpenGroupAPIV2.workQueue) { responseInfo, messages -> Promise<(OnionRequestResponseInfoType, [Message])> in - process(messages: messages, for: roomToken, on: server) + process(messages: messages, for: roomToken, on: server, using: dependencies) .map { processedMessages in (responseInfo, processedMessages) } } } - public static func messagesBefore(messageId: Int64, in roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, [Message])> { + public static func messagesBefore(messageId: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> { // TODO: Recent vs. Since? let request: Request = Request( server: server, @@ -315,10 +309,10 @@ public final class OpenGroupAPIV2: NSObject { // ].compactMapValues { $0 } ) - return send(request) + return send(request, using: dependencies) .decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) .then(on: OpenGroupAPIV2.workQueue) { responseInfo, messages -> Promise<(OnionRequestResponseInfoType, [Message])> in - process(messages: messages, for: roomToken, on: server) + process(messages: messages, for: roomToken, on: server, using: dependencies) .map { processedMessages in (responseInfo, processedMessages) } } } @@ -334,53 +328,53 @@ public final class OpenGroupAPIV2: NSObject { // ].compactMapValues { $0 } ) - return send(request) + return send(request, using: dependencies) .decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) .then(on: OpenGroupAPIV2.workQueue) { responseInfo, messages -> Promise<(OnionRequestResponseInfoType, [Message])> in - process(messages: messages, for: roomToken, on: server) + process(messages: messages, for: roomToken, on: server, using: dependencies) .map { processedMessages in (responseInfo, processedMessages) } } } // MARK: - Pinning - public static func pinMessage(id: Int64, in roomToken: String, on server: String) -> Promise { + public static func pinMessage(id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise { let request: Request = Request( method: .post, server: server, endpoint: .roomPinMessage(roomToken, id: id) ) - return send(request) + return send(request, using: dependencies) .map { responseInfo, _ in responseInfo } } - public static func unpinMessage(id: Int64, in roomToken: String, on server: String) -> Promise { + public static func unpinMessage(id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise { let request: Request = Request( method: .post, server: server, endpoint: .roomUnpinMessage(roomToken, id: id) ) - return send(request) + return send(request, using: dependencies) .map { responseInfo, _ in responseInfo } } - public static func unpinAll(in roomToken: String, on server: String) -> Promise { + public static func unpinAll(in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise { let request: Request = Request( method: .post, server: server, endpoint: .roomUnpinAll(roomToken) ) - return send(request) + return send(request, using: dependencies) .map { responseInfo, _ in responseInfo } } // MARK: - Files // TODO: Shift this logic to the `OpenGroupManager` (makes more sense since it's not API logic) - public static func roomImage(_ fileId: Int64, for roomToken: String, on server: String) -> Promise { + public static func roomImage(_ fileId: Int64, for roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise { // Normally the image for a given group is stored with the group thread, so it's only // fetched once. However, on the join open group screen we show images for groups the // user * hasn't * joined yet. We don't want to re-fetch these images every time the @@ -391,11 +385,11 @@ public final class OpenGroupAPIV2: NSObject { // don't double up on fetch requests by storing the existing request as a promise if // there is one. let lastOpenGroupImageUpdate: Date? = UserDefaults.standard[.lastOpenGroupImageUpdate] - let now: Date = Date() + let now: Date = dependencies.date let timeSinceLastUpdate: TimeInterval = (given(lastOpenGroupImageUpdate) { now.timeIntervalSince($0) } ?? .greatestFiniteMagnitude) let updateInterval: TimeInterval = (7 * 24 * 60 * 60) - if let data = Storage.shared.getOpenGroupImage(for: roomToken, on: server), server == defaultServer, timeSinceLastUpdate < updateInterval { + if let data = dependencies.storage.getOpenGroupImage(for: roomToken, on: server), server == defaultServer, timeSinceLastUpdate < updateInterval { return Promise.value(data) } @@ -403,12 +397,12 @@ public final class OpenGroupAPIV2: NSObject { return promise } - let promise: Promise = downloadFile(fileId, from: roomToken, on: server) + let promise: Promise = downloadFile(fileId, from: roomToken, on: server, using: dependencies) .map { _, data in data } _ = promise.done(on: OpenGroupAPIV2.workQueue) { imageData in if server == defaultServer { - Storage.shared.write { transaction in - Storage.shared.setOpenGroupImage(to: imageData, for: roomToken, on: server, using: transaction) + dependencies.storage.write { transaction in + dependencies.storage.setOpenGroupImage(to: imageData, for: roomToken, on: server, using: transaction) } UserDefaults.standard[.lastOpenGroupImageUpdate] = now } @@ -418,41 +412,41 @@ public final class OpenGroupAPIV2: NSObject { return promise } - public static func uploadFile(_ bytes: [UInt8], fileName: String? = nil, to roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, FileUploadResponse)> { + public static func uploadFile(_ bytes: [UInt8], fileName: String? = nil, to roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, FileUploadResponse)> { let request: Request = Request( method: .post, server: server, endpoint: .roomFile(roomToken), - queryParameters: [ .fileName: fileName ].compactMapValues { $0 }, + headers: [ .fileName: fileName ].compactMapValues { $0 }, body: Data(bytes) ) - return send(request) + return send(request, using: dependencies) .decoded(as: FileUploadResponse.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } /// Warning: This approach is less efficient as it expects the data to be base64Encoded (with is 33% larger than binary), please use the binary approach /// whenever possible - public static func uploadFile(_ base64EncodedString: String, fileName: String? = nil, to roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, FileUploadResponse)> { + public static func uploadFile(_ base64EncodedString: String, fileName: String? = nil, to roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, FileUploadResponse)> { let request: Request = Request( method: .post, server: server, endpoint: .roomFileJson(roomToken), - queryParameters: [ .fileName: fileName ].compactMapValues { $0 }, + headers: [ .fileName: fileName ].compactMapValues { $0 }, body: Data(base64Encoded: base64EncodedString) ) - return send(request) + return send(request, using: dependencies) .decoded(as: FileUploadResponse.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } - public static func downloadFile(_ fileId: Int64, from roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, Data)> { + public static func downloadFile(_ fileId: Int64, from roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data)> { let request: Request = Request( server: server, endpoint: .roomFileIndividual(roomToken, fileId) ) - return send(request) + return send(request, using: dependencies) .map { responseInfo, maybeData in guard let data: Data = maybeData else { throw Error.parsingFailed } @@ -460,19 +454,19 @@ public final class OpenGroupAPIV2: NSObject { } } - public static func downloadFileJson(_ fileId: Int64, from roomToken: String, on server: String) -> Promise<(OnionRequestResponseInfoType, FileDownloadResponse)> { + public static func downloadFileJson(_ fileId: Int64, from roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, FileDownloadResponse)> { let request: Request = Request( server: server, endpoint: .roomFileIndividualJson(roomToken, fileId) ) // TODO: This endpoint is getting rewritten to return just data (properties would come through as headers) - return send(request) + return send(request, using: dependencies) .decoded(as: FileDownloadResponse.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } // MARK: - Users - public static func userBan(_ sessionId: String, for timeout: TimeInterval? = nil, from roomTokens: [String]? = nil, on server: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { + public static func userBan(_ sessionId: String, for timeout: TimeInterval? = nil, from roomTokens: [String]? = nil, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> { let requestBody: UserBanRequest = UserBanRequest( rooms: roomTokens, global: (roomTokens == nil ? true : nil), @@ -490,10 +484,10 @@ public final class OpenGroupAPIV2: NSObject { body: body ) - return send(request) + return send(request, using: dependencies) } - public static func userUnban(_ sessionId: String, from roomTokens: [String]? = nil, on server: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { + public static func userUnban(_ sessionId: String, from roomTokens: [String]? = nil, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> { let requestBody: UserUnbanRequest = UserUnbanRequest( rooms: roomTokens, global: (roomTokens == nil ? true : nil) @@ -510,10 +504,10 @@ public final class OpenGroupAPIV2: NSObject { body: body ) - return send(request) + return send(request, using: dependencies) } - public static func userPermissionUpdate(_ sessionId: String, read: Bool, write: Bool, upload: Bool, for roomTokens: [String], timeout: TimeInterval, on server: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { + public static func userPermissionUpdate(_ sessionId: String, read: Bool, write: Bool, upload: Bool, for roomTokens: [String], timeout: TimeInterval, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> { let requestBody: UserPermissionsRequest = UserPermissionsRequest( rooms: roomTokens, timeout: timeout, @@ -533,10 +527,10 @@ public final class OpenGroupAPIV2: NSObject { body: body ) - return send(request) + return send(request, using: dependencies) } - public static func userModeratorUpdate(_ sessionId: String, moderator: Bool, admin: Bool, visible: Bool, for roomTokens: [String]? = nil, on server: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { + public static func userModeratorUpdate(_ sessionId: String, moderator: Bool, admin: Bool, visible: Bool, for roomTokens: [String]? = nil, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> { let requestBody: UserModeratorRequest = UserModeratorRequest( rooms: roomTokens, global: (roomTokens == nil ? true : nil), @@ -556,10 +550,10 @@ public final class OpenGroupAPIV2: NSObject { body: body ) - return send(request) + return send(request, using: dependencies) } - public static func userDeleteMessages(_ sessionId: String, for roomTokens: [String]? = nil, on server: String) -> Promise<(OnionRequestResponseInfoType, UserDeleteMessagesResponse)> { + public static func userDeleteMessages(_ sessionId: String, for roomTokens: [String]? = nil, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, UserDeleteMessagesResponse)> { let requestBody: UserDeleteMessagesRequest = UserDeleteMessagesRequest( rooms: roomTokens, global: (roomTokens == nil ? true : nil) @@ -576,26 +570,25 @@ public final class OpenGroupAPIV2: NSObject { body: body ) - return send(request) + return send(request, using: dependencies) .decoded(as: UserDeleteMessagesResponse.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } // MARK: - Processing // TODO: Move these methods to the OpenGroupManager? (seems odd for them to be in the API) - private static func process(messages: [Message]?, for room: String, on server: String) -> Promise<[Message]> { + private static func process(messages: [Message]?, for room: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<[Message]> { guard let messages: [Message] = messages, !messages.isEmpty else { return Promise.value([]) } - let storage = SNMessagingKitConfiguration.shared.storage let seqNo: Int64 = (messages.compactMap { $0.seqNo }.max() ?? 0) - let lastMessageSeqNo: Int64 = (storage.getLastMessageServerID(for: room, on: server) ?? 0) + let lastMessageSeqNo: Int64 = (dependencies.storage.getLastMessageServerID(for: room, on: server) ?? 0) if seqNo > lastMessageSeqNo { let (promise, seal) = Promise<[Message]>.pending() - storage.write( + dependencies.storage.write( with: { transaction in - storage.setLastMessageServerID(for: room, on: server, to: seqNo, using: transaction) + dependencies.storage.setLastMessageServerID(for: room, on: server, to: seqNo, using: transaction) }, completion: { seal.fulfill(messages) @@ -608,19 +601,18 @@ public final class OpenGroupAPIV2: NSObject { return Promise.value(messages) } - private static func process(deletions: [Deletion]?, for room: String, on server: String) -> Promise<[Deletion]> { + private static func process(deletions: [Deletion]?, for room: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<[Deletion]> { guard let deletions: [Deletion] = deletions else { return Promise.value([]) } - let storage = SNMessagingKitConfiguration.shared.storage let serverID: Int64 = (deletions.compactMap { $0.id }.max() ?? 0) - let lastDeletionServerID: Int64 = (storage.getLastDeletionServerID(for: room, on: server) ?? 0) + let lastDeletionServerID: Int64 = (dependencies.storage.getLastDeletionServerID(for: room, on: server) ?? 0) if serverID > lastDeletionServerID { let (promise, seal) = Promise<[Deletion]>.pending() - storage.write( + dependencies.storage.write( with: { transaction in - storage.setLastDeletionServerID(for: room, on: server, to: serverID, using: transaction) + dependencies.storage.setLastDeletionServerID(for: room, on: server, to: serverID, using: transaction) }, completion: { seal.fulfill(deletions) @@ -639,14 +631,15 @@ public final class OpenGroupAPIV2: NSObject { // MARK: - General - public static func getDefaultRoomsIfNeeded() { + // TODO: Shift this to the OpenGroupManagerV2? (seems more at place there than in the API) + public static func getDefaultRoomsIfNeeded(using dependencies: Dependencies = Dependencies()) { Storage.shared.write( with: { transaction in - Storage.shared.setOpenGroupPublicKey(for: defaultServer, to: defaultServerPublicKey, using: transaction) + dependencies.storage.setOpenGroupPublicKey(for: defaultServer, to: defaultServerPublicKey, using: transaction) }, completion: { let promise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) { - OpenGroupAPIV2.rooms(for: defaultServer) + OpenGroupAPIV2.rooms(for: defaultServer, using: dependencies) .map { _, data in data } } _ = promise.done(on: OpenGroupAPIV2.workQueue) { items in @@ -657,7 +650,7 @@ public final class OpenGroupAPIV2: NSObject { return (imageId, room.token) } .forEach { imageId, roomToken in - roomImage(imageId, for: roomToken, on: defaultServer) + roomImage(imageId, for: roomToken, on: defaultServer, using: dependencies) .retainUntilComplete() } } @@ -671,26 +664,18 @@ public final class OpenGroupAPIV2: NSObject { // MARK: - Authentication - // TODO: Turn 'Sodium' into a protocol for unit testing - static func sign( - _ request: URLRequest, - with publicKey: String, - using storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage, - sodium: Sodium = Sodium(), - nonceGenerator: NonceGenerator16ByteType = NonceGenerator16Byte(), - date: Date = Date() - ) -> URLRequest? { + private static func sign(_ request: URLRequest, with publicKey: String, using dependencies: Dependencies = Dependencies()) -> URLRequest? { guard let url: URL = request.url else { return nil } var updatedRequest: URLRequest = request let path: String = url.path .appending(url.query.map { value in "?\(value)" }) let method: String = (request.httpMethod ?? "GET") - let timestamp: Int = Int(floor(date.timeIntervalSince1970)) - let nonce: Data = Data(nonceGenerator.nonce()) + let timestamp: Int = Int(floor(dependencies.date.timeIntervalSince1970)) + let nonce: Data = Data(dependencies.nonceGenerator.nonce()) guard let publicKeyData: Data = publicKey.dataFromHex() else { return nil } - guard let userKeyPair: ECKeyPair = storage.getUserKeyPair() else { + guard let userKeyPair: ECKeyPair = dependencies.storage.getUserKeyPair() else { return nil } // guard let blindedKeyPair: ECKeyPair = try? userKeyPair.convert(to: .blinded, with: publicKey) else { @@ -702,7 +687,7 @@ public final class OpenGroupAPIV2: NSObject { /// Generate the sharedSecret by "aB || A || B" where /// a, A are the users private and public keys respectively, /// B is the SOGS public key - let maybeSharedSecret: Data? = sodium.sharedSecret(blindedKeyPair.privateKey.bytes, publicKeyData.bytes)? + let maybeSharedSecret: Data? = dependencies.sodium.sharedSecret(blindedKeyPair.privateKey.bytes, publicKeyData.bytes)? .appending(blindedKeyPair.publicKey) .appending(publicKeyData.bytes) @@ -721,10 +706,10 @@ public final class OpenGroupAPIV2: NSObject { .appending(request.httpBody?.bytes ?? []) // TODO: Might need to do the 'httpBodyStream' as well??? guard let sharedSecret: Data = maybeSharedSecret else { return nil } - guard let intermediateHash: Bytes = sodium.genericHash.hashSaltPersonal(message: sharedSecret.bytes, outputLength: 42, key: nil, salt: nonce.bytes, personal: Personalization.sharedKeys.bytes) else { + guard let intermediateHash: Bytes = dependencies.genericHash.hashSaltPersonal(message: sharedSecret.bytes, outputLength: 42, key: nil, salt: nonce.bytes, personal: Personalization.sharedKeys.bytes) else { return nil } - guard let secretHash: Bytes = sodium.genericHash.hashSaltPersonal(message: secretHashMessage, outputLength: 42, key: intermediateHash, salt: nonce.bytes, personal: Personalization.authHeader.bytes) else { + guard let secretHash: Bytes = dependencies.genericHash.hashSaltPersonal(message: secretHashMessage, outputLength: 42, key: intermediateHash, salt: nonce.bytes, personal: Personalization.authHeader.bytes) else { return nil } @@ -741,13 +726,7 @@ public final class OpenGroupAPIV2: NSObject { // MARK: - Convenience - private static func send( - _ request: Request, - through api: OnionRequestAPIType.Type = OnionRequestAPI.self, - using storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage, - nonceGenerator: NonceGenerator16ByteType = NonceGenerator16Byte(), - date: Date = Date() - ) -> Promise<(OnionRequestResponseInfoType, Data?)> { + private static func send(_ request: Request, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> { guard let url: URL = request.url else { return Promise(error: Error.invalidURL) } var urlRequest: URLRequest = URLRequest(url: url) @@ -758,21 +737,21 @@ public final class OpenGroupAPIV2: NSObject { urlRequest.httpBody = request.body if request.useOnionRouting { - guard let publicKey = storage.getOpenGroupPublicKey(for: request.server) else { + 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, with: publicKey, using: storage, nonceGenerator: nonceGenerator, date: date) else { + guard let signedRequest: URLRequest = sign(urlRequest, with: publicKey, using: dependencies) else { return Promise(error: Error.signingFailed) } // TODO: 'removeAuthToken' as a migration??? (would previously do this when getting a `401`). - return api.sendOnionRequest(signedRequest, to: request.server, with: publicKey) + return dependencies.api.sendOnionRequest(signedRequest, to: request.server, with: publicKey) } - return api.sendOnionRequest(urlRequest, to: request.server, with: publicKey) + return dependencies.api.sendOnionRequest(urlRequest, to: request.server, with: publicKey) } preconditionFailure("It's currently not allowed to send non onion routed requests.") @@ -866,11 +845,11 @@ public final class OpenGroupAPIV2: NSObject { server: server, room: room, endpoint: .legacyAuthTokenClaim(legacyAuth: true), - body: body, headers: [ // Set explicitly here because is isn't in the database yet at this point .authorization: authToken ], + body: body, isAuthRequired: false ) diff --git a/SessionMessagingKit/Open Groups/Types/Dependencies.swift b/SessionMessagingKit/Open Groups/Types/Dependencies.swift new file mode 100644 index 000000000..58d4e4fc0 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Types/Dependencies.swift @@ -0,0 +1,52 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Sodium +import SessionSnodeKit + +extension OpenGroupAPIV2 { + public struct Dependencies { + let api: OnionRequestAPIType.Type + let storage: SessionMessagingKitStorageProtocol + let sodium: SodiumType + let genericHash: GenericHashType + let nonceGenerator: NonceGenerator16ByteType + let date: Date + + public init( + api: OnionRequestAPIType.Type = OnionRequestAPI.self, + storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage, + sodium: SodiumType = Sodium(), + genericHash: GenericHashType? = nil, + nonceGenerator: NonceGenerator16ByteType = NonceGenerator16Byte(), + date: Date = Date() + ) { + self.api = api + self.storage = storage + self.sodium = sodium + self.genericHash = (genericHash ?? sodium.getGenericHash()) + self.nonceGenerator = nonceGenerator + self.date = date + } + + // MARK: - Convenience + + public func with( + api: OnionRequestAPIType.Type? = nil, + storage: SessionMessagingKitStorageProtocol? = nil, + sodium: SodiumType? = nil, + genericHash: GenericHashType? = nil, + nonceGenerator: NonceGenerator16ByteType? = nil, + date: Date? = nil + ) -> Dependencies { + return Dependencies( + api: (api ?? self.api), + storage: (storage ?? self.storage), + sodium: (sodium ?? self.sodium), + genericHash: (genericHash ?? self.genericHash), + nonceGenerator: (nonceGenerator ?? self.nonceGenerator), + date: (date ?? self.date) + ) + } + } +} diff --git a/SessionMessagingKit/Open Groups/Types/Request.swift b/SessionMessagingKit/Open Groups/Types/Request.swift index 2e34adebc..955a5e07c 100644 --- a/SessionMessagingKit/Open Groups/Types/Request.swift +++ b/SessionMessagingKit/Open Groups/Types/Request.swift @@ -10,8 +10,8 @@ extension OpenGroupAPIV2 { let room: String? // TODO: Remove this? let endpoint: Endpoint let queryParameters: [QueryParam: String] - let body: Data? let headers: [Header: String] + let body: Data? let isAuthRequired: Bool /// Always `true` under normal circumstances. You might want to disable /// this when running over Lokinet. @@ -23,8 +23,8 @@ extension OpenGroupAPIV2 { room: String? = nil, endpoint: Endpoint, queryParameters: [QueryParam: String] = [:], - body: Data? = nil, headers: [Header: String] = [:], + body: Data? = nil, isAuthRequired: Bool = true, useOnionRouting: Bool = true ) { @@ -33,8 +33,8 @@ extension OpenGroupAPIV2 { self.room = room self.endpoint = endpoint self.queryParameters = queryParameters - self.body = body self.headers = headers + self.body = body self.isAuthRequired = isAuthRequired self.useOnionRouting = useOnionRouting } @@ -44,8 +44,6 @@ extension OpenGroupAPIV2 { } var urlPathAndParamsString: String { - guard method == .get else { return "/\(endpoint.path)" } - return [ "/\(endpoint.path)", queryParameters diff --git a/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift b/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift new file mode 100644 index 000000000..ae6f42847 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift @@ -0,0 +1,27 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Sodium + +public protocol SodiumType { + func getGenericHash() -> GenericHashType + + func sharedSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Sodium.SharedSecret? +} + +public protocol GenericHashType { + func hashSaltPersonal(message: Bytes, outputLength: Int, key: Bytes?, salt: Bytes, personal: Bytes) -> Bytes? +} + +extension GenericHashType { + func hashSaltPersonal(message: Bytes, outputLength: Int, salt: Bytes, personal: Bytes) -> Bytes? { + return hashSaltPersonal(message: message, outputLength: outputLength, key: nil, salt: salt, personal: personal) + } +} + +extension Sodium: SodiumType { + public func getGenericHash() -> GenericHashType { return genericHash } +} + +extension GenericHash: GenericHashType {} + diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index 4444307d3..fab20bd78 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -1,7 +1,7 @@ import PromiseKit import Sodium -public protocol SessionMessagingKitStorageProtocol { +public protocol SessionMessagingKitStorageProtocol: SessionMessagingKitOpenGroupStorageProtocol { // MARK: - Shared diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPIV2Tests.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPIV2Tests.swift index 8923287b6..7d01bb41a 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPIV2Tests.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPIV2Tests.swift @@ -3,6 +3,7 @@ import XCTest import Nimble import PromiseKit +import Sodium import SessionSnodeKit @testable import SessionMessagingKit @@ -66,14 +67,21 @@ class OpenGroupAPIV2Tests: XCTestCase { } var testStorage: TestStorage! + var dependencies: OpenGroupAPIV2.Dependencies! // MARK: - Configuration override func setUpWithError() throws { testStorage = TestStorage() + dependencies = OpenGroupAPIV2.Dependencies( + api: TestApi.self, + storage: testStorage, + nonceGenerator: TestNonceGenerator(), + date: Date(timeIntervalSince1970: 1234567890) + ) testStorage.mockData[.allV2OpenGroups] = [ - "0": OpenGroupV2(server: "testServer", room: "test1", name: "Test", publicKey: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d", imageID: nil) + "0": OpenGroupV2(server: "testServer", room: "testRoom", name: "Test", publicKey: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d", imageID: nil) ] testStorage.mockData[.openGroupPublicKeys] = ["testServer": "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"] @@ -85,14 +93,14 @@ class OpenGroupAPIV2Tests: XCTestCase { } override func tearDownWithError() throws { + dependencies = nil testStorage = nil } // MARK: - Batching & Polling func testPollGeneratesTheCorrectRequest() throws { - // Define a custom TestApi class so we can override the response - class TestApi1: TestApi { + class LocalTestApi: TestApi { override class var mockResponse: Data? { let responses: [Data] = [ try! JSONEncoder().encode( @@ -106,36 +114,7 @@ class OpenGroupAPIV2Tests: XCTestCase { OpenGroupAPIV2.BatchSubResponse( code: 200, headers: [:], - body: OpenGroupAPIV2.RoomPollInfo( - token: nil, - created: nil, - name: nil, - description: nil, - imageId: nil, - - infoUpdates: nil, - messageSequence: nil, - activeUsers: nil, - activeUsersCutoff: nil, - pinnedMessages: nil, - - admin: nil, - globalAdmin: nil, - admins: nil, - hiddenAdmins: nil, - - moderator: nil, - globalModerator: nil, - moderators: nil, - hiddenModerators: nil, - read: nil, - defaultRead: nil, - write: nil, - defaultWrite: nil, - upload: nil, - defaultUpload: nil, - details: nil - ) + body: try! JSONDecoder().decode(OpenGroupAPIV2.RoomPollInfo.self, from: "{}".data(using: .utf8)!) ) ), try! JSONEncoder().encode( @@ -150,60 +129,304 @@ class OpenGroupAPIV2Tests: XCTestCase { return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) } } + dependencies = dependencies.with(api: LocalTestApi.self) - var pollResponse: [Endpoint: (OnionRequestResponseInfoType, Codable)]? = nil + var response: [Endpoint: (OnionRequestResponseInfoType, Codable)]? = nil + var error: Error? = nil - OpenGroupAPIV2.poll("testServer", through: TestApi1.self, using: testStorage, nonceGenerator: TestNonceGenerator(), date: Date(timeIntervalSince1970: 1234567890)) - .map { result -> [Endpoint: (OnionRequestResponseInfoType, Codable)] in - pollResponse = result - return result - } + OpenGroupAPIV2.poll("testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } .retainUntilComplete() - expect(pollResponse) + expect(response) .toEventuallyNot( beNil(), - timeout: .milliseconds(10000) + timeout: .milliseconds(100) ) + expect(error?.localizedDescription).to(beNil()) // Validate the response data - expect(pollResponse?.values).to(haveCount(3)) - expect(pollResponse?.keys).to(contain(.capabilities)) - expect(pollResponse?.keys).to(contain(.roomPollInfo("test1", 0))) - expect(pollResponse?.keys).to(contain(.roomMessagesRecent("test1"))) - expect(pollResponse?[.capabilities]?.0).to(beAKindOf(TestResponseInfo.self)) + expect(response?.values).to(haveCount(3)) + expect(response?.keys).to(contain(.capabilities)) + expect(response?.keys).to(contain(.roomPollInfo("testRoom", 0))) + expect(response?.keys).to(contain(.roomMessagesRecent("testRoom"))) + expect(response?[.capabilities]?.0).to(beAKindOf(TestResponseInfo.self)) // Validate request data - let requestData: TestApi.RequestData? = (pollResponse?[.capabilities]?.0 as? TestResponseInfo)?.requestData + let requestData: TestApi.RequestData? = (response?[.capabilities]?.0 as? TestResponseInfo)?.requestData expect(requestData?.urlString).to(equal("testServer/batch")) expect(requestData?.httpMethod).to(equal("POST")) expect(requestData?.server).to(equal("testServer")) expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) } + func testPollReturnsAnErrorWhenGivenNoData() throws { + var response: [Endpoint: (OnionRequestResponseInfoType, Codable)]? = nil + var error: Error? = nil + + OpenGroupAPIV2.poll("testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPIV2.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + func testPollReturnsAnErrorWhenGivenInvalidData() throws { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return Data() } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: [Endpoint: (OnionRequestResponseInfoType, Codable)]? = nil + var error: Error? = nil + + OpenGroupAPIV2.poll("testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPIV2.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + func testPollReturnsAnErrorWhenGivenAnEmptyResponse() throws { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return "[]".data(using: .utf8) } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: [Endpoint: (OnionRequestResponseInfoType, Codable)]? = nil + var error: Error? = nil + + OpenGroupAPIV2.poll("testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPIV2.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + func testPollReturnsAnErrorWhenGivenAnObjectResponse() throws { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return "{}".data(using: .utf8) } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: [Endpoint: (OnionRequestResponseInfoType, Codable)]? = nil + var error: Error? = nil + + OpenGroupAPIV2.poll("testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPIV2.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + func testPollReturnsAnErrorWhenGivenAnDifferentNumberOfResponses() throws { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPIV2.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPIV2.Capabilities(capabilities: [], missing: nil) + ) + ), + try! JSONEncoder().encode( + OpenGroupAPIV2.BatchSubResponse( + code: 200, + headers: [:], + body: try! JSONDecoder().decode(OpenGroupAPIV2.RoomPollInfo.self, from: "{}".data(using: .utf8)!) + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: [Endpoint: (OnionRequestResponseInfoType, Codable)]? = nil + var error: Error? = nil + + OpenGroupAPIV2.poll("testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPIV2.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + func testPollReturnsAnErrorWhenGivenAnUnexpectedResponse() throws { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPIV2.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPIV2.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: "") + ) + ), + try! JSONEncoder().encode( + OpenGroupAPIV2.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPIV2.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: "") + ) + ), + try! JSONEncoder().encode( + OpenGroupAPIV2.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPIV2.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: "") + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: [Endpoint: (OnionRequestResponseInfoType, Codable)]? = nil + var error: Error? = nil + + OpenGroupAPIV2.poll("testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPIV2.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + // MARK: - Files + + func testItDoesNotAddAFileNameHeaderWhenNotProvided() throws { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + return try! JSONEncoder().encode(FileUploadResponse(id: 1)) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: (OnionRequestResponseInfoType, FileUploadResponse)? = nil + var error: Error? = nil + + OpenGroupAPIV2.uploadFile([], to: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate signature headers + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + expect(requestData?.urlString).to(equal("testServer/room/testRoom/file")) + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.headers).to(haveCount(4)) + expect(requestData?.headers.keys).toNot(contain(Header.fileName.rawValue)) + } + + func testItAddsAFileNameHeaderWhenProvided() throws { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + return try! JSONEncoder().encode(FileUploadResponse(id: 1)) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: (OnionRequestResponseInfoType, FileUploadResponse)? = nil + var error: Error? = nil + + OpenGroupAPIV2.uploadFile([], fileName: "TestFileName", to: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate signature headers + let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData + expect(requestData?.urlString).to(equal("testServer/room/testRoom/file")) + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.headers).to(haveCount(5)) + expect(requestData?.headers[Header.fileName.rawValue]).to(equal("TestFileName")) + } + // MARK: - Authentication func testItSignsTheRequestCorrectly() throws { - class TestApi1: TestApi { + class LocalTestApi: TestApi { override class var mockResponse: Data? { return try! JSONEncoder().encode([OpenGroupAPIV2.Room]()) } } + dependencies = dependencies.with(api: LocalTestApi.self) var response: (OnionRequestResponseInfoType, [OpenGroupAPIV2.Room])? = nil + var error: Error? = nil - OpenGroupAPIV2.rooms(for: "testServer", through: TestApi1.self, using: testStorage, nonceGenerator: TestNonceGenerator(), date: Date(timeIntervalSince1970: 1234567890)) - .map { result -> (OnionRequestResponseInfoType, [OpenGroupAPIV2.Room]) in - response = result - return result - } + OpenGroupAPIV2.rooms(for: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } .retainUntilComplete() expect(response) .toEventuallyNot( beNil(), - timeout: .milliseconds(10000) + timeout: .milliseconds(100) ) + expect(error?.localizedDescription).to(beNil()) // Validate signature headers let requestData: TestApi.RequestData? = (response?.0 as? TestResponseInfo)?.requestData @@ -217,4 +440,128 @@ class OpenGroupAPIV2Tests: XCTestCase { expect(requestData?.headers[Header.sogsHash.rawValue]).to(equal("fxqLy5ZDWCsLQpwLw0Dax+4xe7cG2vPRk1NlHORIm0DPd3o9UA24KLZY")) expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890")) } + + func testItFailsToSignIfTheServerPublicKeyIsInvalid() throws { + testStorage.mockData[.openGroupPublicKeys] = ["testServer": ""] + + var response: Any? = nil + var error: Error? = nil + + OpenGroupAPIV2.rooms(for: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPIV2.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + func testItFailsToSignIfThereIsNoUserKeyPair() throws { + testStorage.mockData[.userKeyPair] = nil + + var response: Any? = nil + var error: Error? = nil + + OpenGroupAPIV2.rooms(for: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPIV2.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + func testItFailsToSignIfTheSharedSecretDoesNotGetGenerated() throws { + class InvalidSodium: SodiumType { + func getGenericHash() -> GenericHashType { return Sodium().genericHash } + func sharedSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Sodium.SharedSecret? { return nil } + } + + dependencies = dependencies.with(sodium: InvalidSodium()) + + var response: Any? = nil + var error: Error? = nil + + OpenGroupAPIV2.rooms(for: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPIV2.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + func testItFailsToSignIfTheIntermediateHashDoesNotGetGenerated() throws { + class InvalidGenericHash: GenericHashType { + func hashSaltPersonal(message: Bytes, outputLength: Int, key: Bytes?, salt: Bytes, personal: Bytes) -> Bytes? { + return nil + } + } + + dependencies = dependencies.with(genericHash: InvalidGenericHash()) + + var response: Any? = nil + var error: Error? = nil + + OpenGroupAPIV2.rooms(for: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPIV2.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } + + func testItFailsToSignIfTheSecretHashDoesNotGetGenerated() throws { + class InvalidSecondGenericHash: GenericHashType { + static var didSucceedOnce: Bool = false + + func hashSaltPersonal(message: Bytes, outputLength: Int, key: Bytes?, salt: Bytes, personal: Bytes) -> Bytes? { + if !InvalidSecondGenericHash.didSucceedOnce { + InvalidSecondGenericHash.didSucceedOnce = true + return Data().bytes + } + + return nil + } + } + + dependencies = dependencies.with(genericHash: InvalidSecondGenericHash()) + + var response: Any? = nil + var error: Error? = nil + + OpenGroupAPIV2.rooms(for: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(OpenGroupAPIV2.Error.signingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } } diff --git a/SessionMessagingKitTests/_TestUtilities/TestStorage.swift b/SessionMessagingKitTests/_TestUtilities/TestStorage.swift index 82f21d4a5..cc5ad174b 100644 --- a/SessionMessagingKitTests/_TestUtilities/TestStorage.swift +++ b/SessionMessagingKitTests/_TestUtilities/TestStorage.swift @@ -13,6 +13,7 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable { case allV2OpenGroups case openGroupPublicKeys case userKeyPair + case openGroupImage } typealias Key = DataKey @@ -112,3 +113,10 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable { func setAttachmentState(to state: TSAttachmentPointerState, for pointer: TSAttachmentPointer, associatedWith tsIncomingMessageID: String, using transaction: Any) {} func persist(_ stream: TSAttachmentStream, associatedWith tsIncomingMessageID: String, using transaction: Any) {} } + +// MARK: - SessionMessagingKitOpenGroupStorageProtocol + +extension TestStorage: SessionMessagingKitOpenGroupStorageProtocol { + func getOpenGroupImage(for room: String, on server: String) -> Data? { return (mockData[.openGroupImage] as? Data) } + func setOpenGroupImage(to data: Data, for room: String, on server: String, using transaction: Any) {} +} diff --git a/SessionUtilitiesKit/General/String+Encoding.swift b/SessionUtilitiesKit/General/String+Encoding.swift index 270f43ac2..bb208adec 100644 --- a/SessionUtilitiesKit/General/String+Encoding.swift +++ b/SessionUtilitiesKit/General/String+Encoding.swift @@ -4,7 +4,7 @@ import Foundation extension String { public func dataFromHex() -> Data? { - guard (self.count % 2) == 0 else { return nil } + guard self.count > 0 && (self.count % 2) == 0 else { return nil } let chars = self.map { $0 } let bytes: [UInt8] = stride(from: 0, to: chars.count, by: 2)