From 34bcc9f027471070d40844f230cf0329203df56c Mon Sep 17 00:00:00 2001 From: jguz-pubnub <102806147+jguz-pubnub@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:43:22 +0100 Subject: [PATCH] Removing SubscribeSessionFactory and SubscriptionConfiguration (#162) refactor(subscribe): removing SubscribeSessionFactory with SubscriptionConfiguration refactor(subscribe): making SubscriptionSession class an internal --- .swiftformat | 19 +- .swiftlint.yml | 2 + PubNub.xcodeproj/project.pbxproj | 64 +- .../DependencyContainer.swift | 338 +++++++++ .../EventEngine/Core/EventEngineFactory.swift | 47 -- .../Presence/Effects/WaitEffect.swift | 8 +- .../Helpers/PresenceHeartbeatRequest.swift | 6 +- .../Helpers/PresenceLeaveRequest.swift | 4 +- .../EventEngine/Presence/Presence.swift | 23 +- .../Subscribe/Helpers/SubscribeRequest.swift | 4 +- .../EventEngine/Subscribe/Subscribe.swift | 44 +- .../Helpers/Crypto/Cryptors/Cryptor.swift | 28 +- Sources/PubNub/Networking/HTTPSession.swift | 56 +- .../Networking/Routers/PresenceRouter.swift | 16 +- .../Networking/Routers/SubscribeRouter.swift | 12 +- Sources/PubNub/PubNub.swift | 154 ++--- ...entEngineSubscriptionSessionStrategy.swift | 10 +- .../LegacySubscriptionSessionStrategy.swift | 10 +- .../SubscriptionSessionStrategy.swift | 5 +- .../SubscribeSessionFactory.swift | 181 ----- .../Subscription/SubscriptionSession.swift | 111 ++- .../PubNubEventEngineTestsHelpers.swift | 13 +- ...ubNubPresenceEngineContractTestSteps.swift | 107 ++- ...NubSubscribeEngineContractTestsSteps.swift | 121 ++-- .../DelayedHeartbeatEffectTests.swift | 20 +- .../Subscribe/SubscribeEffectsTests.swift | 553 ++++++++------- .../Subscribe/subscription_invalid_json.json | 23 + .../Operators/InstanceIdOperatorTests.swift | 57 -- .../Routers/SubscribeRouterTests.swift | 650 +++++++----------- .../SubscribeSessionFactoryTests.swift | 27 +- .../SubscriptionSessionTests.swift | 63 +- 31 files changed, 1361 insertions(+), 1415 deletions(-) create mode 100644 Sources/PubNub/DependencyContainer/DependencyContainer.swift delete mode 100644 Sources/PubNub/EventEngine/Core/EventEngineFactory.swift delete mode 100644 Sources/PubNub/Subscription/SubscribeSessionFactory.swift create mode 100644 Tests/PubNubTests/Mocking/Responses/Subscribe/subscription_invalid_json.json delete mode 100644 Tests/PubNubTests/Networking/Operators/InstanceIdOperatorTests.swift diff --git a/.swiftformat b/.swiftformat index d180dd25..55ae0e87 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,11 +1,24 @@ -# format options +# Format options --indent 2 --commas inline --disable wrapMultilineStatementBraces +--wraparguments before-first +--wrapparameters before-first +--maxwidth 130 +--disable wrapSingleLineComments +--voidtype void +--redundanttype inferred +--self remove -# file options +--enable spaceAroundBrackets,spaceAroundComments,spaceAroundGenerics,spaceAroundParens,spaceInsideBraces +--enable spaceInsideBrackets,spaceInsideComments,spaceInsideGenerics,spaceInsideParens,trailingclosures,trailingCommas +--enable wrapConditionalBodies, wrapEnumCases, wrapLoopBodies +--enable redundantLetError,redundantParens,redundantSelf,redundantReturn,redundantBreak +--enable duplicateImports + +# File options --exclude .build # Swift Version ---swiftversion 5.0 +--swiftversion 5.8 diff --git a/.swiftlint.yml b/.swiftlint.yml index d04a6c2d..04650714 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,4 +1,5 @@ line_length: + warning: 130 ignores_comments: true disabled_rules: - identifier_name @@ -11,6 +12,7 @@ excluded: - .bundle - fastlane - Tests + - Pods opt_in_rules: - force_unwrapping - overridden_super_call diff --git a/PubNub.xcodeproj/project.pbxproj b/PubNub.xcodeproj/project.pbxproj index 431548aa..18a5af73 100644 --- a/PubNub.xcodeproj/project.pbxproj +++ b/PubNub.xcodeproj/project.pbxproj @@ -128,7 +128,6 @@ 3557CE0723886434004BBACC /* PubNubAPNSPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3557CE0623886434004BBACC /* PubNubAPNSPayload.swift */; }; 35580682230F3A34005CDD92 /* RequestIdOperator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35580681230F3A34005CDD92 /* RequestIdOperator.swift */; }; 35580686230F47EA005CDD92 /* RequestIdOperatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35580684230F4771005CDD92 /* RequestIdOperatorTests.swift */; }; - 3558068A230F4C99005CDD92 /* InstanceIdOperatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35580687230F4B75005CDD92 /* InstanceIdOperatorTests.swift */; }; 3558069C231303D9005CDD92 /* AutomaticRetryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558069B231303D9005CDD92 /* AutomaticRetryTests.swift */; }; 355806DB23145749005CDD92 /* PubNub.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "PubNub::PubNub::Product" /* PubNub.framework */; }; 3559977B23073D53000BCFD1 /* WeakBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3559977A23073D53000BCFD1 /* WeakBoxTests.swift */; }; @@ -209,7 +208,6 @@ 35A66A7F22F861BA00AC67A9 /* WeakBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35A66A7522F861BA00AC67A9 /* WeakBox.swift */; }; 35A66A8022F861BA00AC67A9 /* AutomaticRetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35A66A7722F861BA00AC67A9 /* AutomaticRetry.swift */; }; 35A66A8322F861BA00AC67A9 /* PubNubMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35A66A7C22F861BA00AC67A9 /* PubNubMessage.swift */; }; - 35A66A8E22F911DB00AC67A9 /* SubscribeSessionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35A66A8D22F911DB00AC67A9 /* SubscribeSessionFactory.swift */; }; 35A66A9022F913B200AC67A9 /* ConnectionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35A66A8F22F913B200AC67A9 /* ConnectionStatus.swift */; }; 35A66A9622F9B71200AC67A9 /* setState_missing_state.json in Resources */ = {isa = PBXBuildFile; fileRef = 35A66A9522F9B71200AC67A9 /* setState_missing_state.json */; }; 35A66A9722F9B72200AC67A9 /* setState_success.json in Resources */ = {isa = PBXBuildFile; fileRef = 35A66A8C22F9084000AC67A9 /* setState_success.json */; }; @@ -367,11 +365,11 @@ 35FE941222EFB70B0051C455 /* unrecognizedEndpointError.json in Resources */ = {isa = PBXBuildFile; fileRef = 35FE941122EFB70B0051C455 /* unrecognizedEndpointError.json */; }; 35FE941422EFB7C10051C455 /* unknownEndpointError.json in Resources */ = {isa = PBXBuildFile; fileRef = 35FE941322EFB7C10051C455 /* unknownEndpointError.json */; }; 35FE941F22F0929A0051C455 /* RequestRetrierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FE941E22F0929A0051C455 /* RequestRetrierTests.swift */; }; + 3D34D1C42B989B440055A7FA /* subscription_invalid_json.json in Resources */ = {isa = PBXBuildFile; fileRef = 3D34D1C32B989B440055A7FA /* subscription_invalid_json.json */; }; 3D389FE12B35AF4A006928E7 /* TransitionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D389FC32B35AF4A006928E7 /* TransitionProtocol.swift */; }; 3D389FE22B35AF4A006928E7 /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D389FC42B35AF4A006928E7 /* Dispatcher.swift */; }; 3D389FE32B35AF4A006928E7 /* EffectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D389FC52B35AF4A006928E7 /* EffectHandler.swift */; }; 3D389FE42B35AF4A006928E7 /* EventEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D389FC62B35AF4A006928E7 /* EventEngine.swift */; }; - 3D389FE52B35AF4A006928E7 /* EventEngineFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D389FC72B35AF4A006928E7 /* EventEngineFactory.swift */; }; 3D389FE62B35AF4A006928E7 /* EmitMessagesEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D389FCA2B35AF4A006928E7 /* EmitMessagesEffect.swift */; }; 3D389FE72B35AF4A006928E7 /* EmitStatusEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D389FCB2B35AF4A006928E7 /* EmitStatusEffect.swift */; }; 3D389FE82B35AF4A006928E7 /* SubscribeEffects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D389FCC2B35AF4A006928E7 /* SubscribeEffects.swift */; }; @@ -428,6 +426,7 @@ 3D758DD22AB0A91C005D2B36 /* AESCBCCryptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D758DD12AB0A91C005D2B36 /* AESCBCCryptor.swift */; }; 3D758DD52AB48A6A005D2B36 /* CryptorHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D758DD32AB48A6A005D2B36 /* CryptorHeader.swift */; }; 3D758DD62AB48A6A005D2B36 /* CryptorHeaderWithinStreamFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D758DD42AB48A6A005D2B36 /* CryptorHeaderWithinStreamFinder.swift */; }; + 3D8BAC102B8C96D70059A5C3 /* DependencyContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8BAC0F2B8C96D70059A5C3 /* DependencyContainer.swift */; }; 3D9134972A1216F7000A5124 /* PubNubPushTargetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9134962A1216F7000A5124 /* PubNubPushTargetTests.swift */; }; 3DACC7F72AB88F8E00210B14 /* Data+CommonCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DACC7F62AB88F8E00210B14 /* Data+CommonCrypto.swift */; }; 3DB9255C2B7A2B89001B7E90 /* SubscriptionStreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB925592B7A2B89001B7E90 /* SubscriptionStreamTests.swift */; }; @@ -711,7 +710,6 @@ 3557CE0623886434004BBACC /* PubNubAPNSPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubNubAPNSPayload.swift; sourceTree = ""; }; 35580681230F3A34005CDD92 /* RequestIdOperator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestIdOperator.swift; sourceTree = ""; }; 35580684230F4771005CDD92 /* RequestIdOperatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestIdOperatorTests.swift; sourceTree = ""; }; - 35580687230F4B75005CDD92 /* InstanceIdOperatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstanceIdOperatorTests.swift; sourceTree = ""; }; 3558069B231303D9005CDD92 /* AutomaticRetryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticRetryTests.swift; sourceTree = ""; }; 3558073723145749005CDD92 /* PubNubIntTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PubNubIntTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3559977A23073D53000BCFD1 /* WeakBoxTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakBoxTests.swift; sourceTree = ""; }; @@ -811,7 +809,6 @@ 35A66A7C22F861BA00AC67A9 /* PubNubMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PubNubMessage.swift; sourceTree = ""; }; 35A66A8B22F9080A00AC67A9 /* getState_success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = getState_success.json; sourceTree = ""; }; 35A66A8C22F9084000AC67A9 /* setState_success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = setState_success.json; sourceTree = ""; }; - 35A66A8D22F911DB00AC67A9 /* SubscribeSessionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeSessionFactory.swift; sourceTree = ""; }; 35A66A8F22F913B200AC67A9 /* ConnectionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionStatus.swift; sourceTree = ""; }; 35A66A9522F9B71200AC67A9 /* setState_missing_state.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = setState_missing_state.json; sourceTree = ""; }; 35A6C77C22FB159F00E97CC5 /* PresenceRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresenceRouter.swift; sourceTree = ""; }; @@ -964,11 +961,11 @@ 35FE941122EFB70B0051C455 /* unrecognizedEndpointError.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = unrecognizedEndpointError.json; sourceTree = ""; }; 35FE941322EFB7C10051C455 /* unknownEndpointError.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = unknownEndpointError.json; sourceTree = ""; }; 35FE941E22F0929A0051C455 /* RequestRetrierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestRetrierTests.swift; sourceTree = ""; }; + 3D34D1C32B989B440055A7FA /* subscription_invalid_json.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscription_invalid_json.json; sourceTree = ""; }; 3D389FC32B35AF4A006928E7 /* TransitionProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionProtocol.swift; sourceTree = ""; }; 3D389FC42B35AF4A006928E7 /* Dispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dispatcher.swift; sourceTree = ""; }; 3D389FC52B35AF4A006928E7 /* EffectHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EffectHandler.swift; sourceTree = ""; }; 3D389FC62B35AF4A006928E7 /* EventEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventEngine.swift; sourceTree = ""; }; - 3D389FC72B35AF4A006928E7 /* EventEngineFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventEngineFactory.swift; sourceTree = ""; }; 3D389FCA2B35AF4A006928E7 /* EmitMessagesEffect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmitMessagesEffect.swift; sourceTree = ""; }; 3D389FCB2B35AF4A006928E7 /* EmitStatusEffect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmitStatusEffect.swift; sourceTree = ""; }; 3D389FCC2B35AF4A006928E7 /* SubscribeEffects.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscribeEffects.swift; sourceTree = ""; }; @@ -1021,6 +1018,7 @@ 3D758DD12AB0A91C005D2B36 /* AESCBCCryptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AESCBCCryptor.swift; sourceTree = ""; }; 3D758DD32AB48A6A005D2B36 /* CryptorHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptorHeader.swift; sourceTree = ""; }; 3D758DD42AB48A6A005D2B36 /* CryptorHeaderWithinStreamFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptorHeaderWithinStreamFinder.swift; sourceTree = ""; }; + 3D8BAC0F2B8C96D70059A5C3 /* DependencyContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyContainer.swift; sourceTree = ""; }; 3D9134962A1216F7000A5124 /* PubNubPushTargetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubNubPushTargetTests.swift; sourceTree = ""; }; 3DACC7F62AB88F8E00210B14 /* Data+CommonCrypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+CommonCrypto.swift"; sourceTree = ""; }; 3DB925592B7A2B89001B7E90 /* SubscriptionStreamTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionStreamTests.swift; sourceTree = ""; }; @@ -1315,7 +1313,6 @@ 35FE941E22F0929A0051C455 /* RequestRetrierTests.swift */, 3580A59322F0C74100B12E5E /* RequestMutatorTests.swift */, 35580684230F4771005CDD92 /* RequestIdOperatorTests.swift */, - 35580687230F4B75005CDD92 /* InstanceIdOperatorTests.swift */, 3558069B231303D9005CDD92 /* AutomaticRetryTests.swift */, ); path = Operators; @@ -1423,6 +1420,7 @@ 357AEB7E22E693DD00C18250 /* Subscribe */ = { isa = PBXGroup; children = ( + 3D34D1C32B989B440055A7FA /* subscription_invalid_json.json */, 3D38A02F2B35B208006928E7 /* subscription_handshake_success.json */, 359287C423185EEE0046F7A2 /* subscription_success.json */, 3DFB01932B0E30EE00146B57 /* subscription_encrypted_message_success.json */, @@ -1686,7 +1684,6 @@ 35A66A8522F8DB2E00AC67A9 /* Subscription */ = { isa = PBXGroup; children = ( - 35A66A8D22F911DB00AC67A9 /* SubscribeSessionFactory.swift */, 35A66A7422F861BA00AC67A9 /* SubscriptionSession.swift */, 35C829DB23147AC000F59D3C /* SubscriptionState.swift */, 35A66A8F22F913B200AC67A9 /* ConnectionStatus.swift */, @@ -2023,7 +2020,6 @@ 3D389FC42B35AF4A006928E7 /* Dispatcher.swift */, 3D389FC52B35AF4A006928E7 /* EffectHandler.swift */, 3D389FC62B35AF4A006928E7 /* EventEngine.swift */, - 3D389FC72B35AF4A006928E7 /* EventEngineFactory.swift */, ); path = Core; sourceTree = ""; @@ -2199,6 +2195,14 @@ path = Header; sourceTree = ""; }; + 3D8BAC0E2B8C96A40059A5C3 /* DependencyContainer */ = { + isa = PBXGroup; + children = ( + 3D8BAC0F2B8C96D70059A5C3 /* DependencyContainer.swift */, + ); + path = DependencyContainer; + sourceTree = ""; + }; 3D9134952A12161A000A5124 /* Push */ = { isa = PBXGroup; children = ( @@ -2511,6 +2515,7 @@ children = ( OBJ_11 /* PubNub.swift */, 359152A022BA9AA30048842D /* PubNubConfiguration.swift */, + 3D8BAC0E2B8C96A40059A5C3 /* DependencyContainer */, 3D389FC12B35AF4A006928E7 /* EventEngine */, 35B0ACE4252BE37C00537A18 /* APIs */, 35DB0C49287475F9001E1F76 /* Core */, @@ -2979,6 +2984,7 @@ 35A6C79A22FBC2AD00E97CC5 /* groups_delete_success.json in Resources */, 3DFB01942B0E30EE00146B57 /* subscription_encrypted_message_success.json in Resources */, 35293A872369F0230049A71F /* addMessageAction_error_400.json in Resources */, + 3D34D1C42B989B440055A7FA /* subscription_invalid_json.json in Resources */, 35FE93F622EF93A90051C455 /* cannotParseResponse.json in Resources */, 35A6C79C22FBC2D100E97CC5 /* groups_channels_add_success.json in Resources */, 350BC412233952FE00011262 /* objects_error_404.json in Resources */, @@ -3426,7 +3432,6 @@ 35CDA4CC2510031E00218137 /* XMLDecoder.swift in Sources */, 35D0615F2304830600FDB2F9 /* GenericServicePayloadResponse.swift in Sources */, 3D758DD62AB48A6A005D2B36 /* CryptorHeaderWithinStreamFinder.swift in Sources */, - 35A66A8E22F911DB00AC67A9 /* SubscribeSessionFactory.swift in Sources */, 3D758DBF2AAA1C49005D2B36 /* CryptoModule.swift in Sources */, 3D389FEC2B35AF4A006928E7 /* SubscribeRequest.swift in Sources */, 3D389FE72B35AF4A006928E7 /* EmitStatusEffect.swift in Sources */, @@ -3554,12 +3559,12 @@ 3559978C230A02B7000BCFD1 /* PubNubLogger.swift in Sources */, 35AE6A3224FD6CEE00BBFA37 /* FileManagementRouter.swift in Sources */, 3D389FF32B35AF4A006928E7 /* WaitEffect.swift in Sources */, + 3D8BAC102B8C96D70059A5C3 /* DependencyContainer.swift in Sources */, 35089A0B22E56F1F002BCC94 /* Constants.swift in Sources */, 358C6421238C6787009CE354 /* PubNubPushMessage.swift in Sources */, 3DD1FB992B5A7804005A14E3 /* PubNubPresenceStateContainer.swift in Sources */, 3D758DC82AB06A12005D2B36 /* CryptoInputStream.swift in Sources */, 35E4604F234B8B9D005D04AE /* ErrorDescription.swift in Sources */, - 3D389FE52B35AF4A006928E7 /* EventEngineFactory.swift in Sources */, 35089A0922E3C08D002BCC94 /* Error+PubNub.swift in Sources */, 3534D4E422C57659008E89FA /* PublishRouter.swift in Sources */, 35EE358C22E26A4D00E3F081 /* HTTPURLResponse+PubNub.swift in Sources */, @@ -3631,7 +3636,6 @@ 3D38A00E2B35AF6A006928E7 /* SubscribeRequestTests.swift in Sources */, 3D38A0142B35AF6B006928E7 /* PresenceTransitionTests.swift in Sources */, OBJ_49 /* PubNubTests.swift in Sources */, - 3558068A230F4C99005CDD92 /* InstanceIdOperatorTests.swift in Sources */, 35CF549E248D913A0099FE81 /* ObjectsUUIDRouterTests.swift in Sources */, 3DB925642B7A2BF5001B7E90 /* SubscriptionSetTests.swift in Sources */, 35458BA3230CB3570085B502 /* SubscribeSessionFactoryTests.swift in Sources */, @@ -3851,7 +3855,7 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 12.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -3899,7 +3903,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -3928,7 +3932,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = NO; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 12.0; WATCHOS_DEPLOYMENT_TARGET = 4.0; }; @@ -3954,7 +3958,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.pubnub.swift.PubNubUserTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; WATCHOS_DEPLOYMENT_TARGET = 4.0; @@ -4007,7 +4011,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 12.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -4057,7 +4061,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -4093,7 +4097,7 @@ SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 12.0; WATCHOS_DEPLOYMENT_TARGET = 4.0; }; @@ -4125,7 +4129,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; WATCHOS_DEPLOYMENT_TARGET = 4.0; @@ -4177,7 +4181,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 12.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -4225,7 +4229,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -4339,7 +4343,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = NO; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 12.0; WATCHOS_DEPLOYMENT_TARGET = 4.0; }; @@ -4365,7 +4369,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.pubnub.swift.PubNubUserTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; WATCHOS_DEPLOYMENT_TARGET = 4.0; @@ -4399,7 +4403,7 @@ SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 12.0; WATCHOS_DEPLOYMENT_TARGET = 4.0; }; @@ -4431,7 +4435,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; WATCHOS_DEPLOYMENT_TARGET = 4.0; @@ -4543,7 +4547,7 @@ SWIFT_OBJC_INTERFACE_HEADER_NAME = "PubNubContractTests-Swift.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TARGET_NAME = PubNubContractTests; WATCHOS_DEPLOYMENT_TARGET = 4.0; }; @@ -4584,7 +4588,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "Tests/PubNubContractTest/PubNubContractTests-Bridging-Header.h"; SWIFT_OBJC_INTERFACE_HEADER_NAME = "PubNubContractTests-Swift.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TARGET_NAME = PubNubContractTests; WATCHOS_DEPLOYMENT_TARGET = 4.0; }; @@ -4627,7 +4631,7 @@ SWIFT_OBJC_INTERFACE_HEADER_NAME = "PubNubContractTests-Swift.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TARGET_NAME = PubNubContractTestsBeta; WATCHOS_DEPLOYMENT_TARGET = 4.0; }; @@ -4669,7 +4673,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "Tests/PubNubContractTest/PubNubContractTests-Bridging-Header.h"; SWIFT_OBJC_INTERFACE_HEADER_NAME = "PubNubContractTests-Swift.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; TARGET_NAME = PubNubContractTestsBeta; WATCHOS_DEPLOYMENT_TARGET = 4.0; }; diff --git a/Sources/PubNub/DependencyContainer/DependencyContainer.swift b/Sources/PubNub/DependencyContainer/DependencyContainer.swift new file mode 100644 index 00000000..53566bf1 --- /dev/null +++ b/Sources/PubNub/DependencyContainer/DependencyContainer.swift @@ -0,0 +1,338 @@ +// +// DependencyContainer.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation + +// A protocol that represents a unique key for each dependency. Each type conforming to `DependencyKey` +// represents a distinct dependency. +protocol DependencyKey { + // A value associated with a given `DependencyKey` + associatedtype Value + // Creates a value of the type Value for a given `DependencyKey` if no existing dependency + // is found in the `DependencyContainer`. The `container` parameter is used in case of + // nested dependencies, i.e., when the dependency being created depends on other objects in the `DependencyContainer`. + static func value(from container: DependencyContainer) -> Value +} + +// The class that serves as a registry for dependencies. Each dependency is associated with a unique key +// conforming to the `DependencyKey` protocol. +class DependencyContainer { + private var resolvedValues: [ObjectIdentifier: any Wrappable] = [:] + private var registeredKeys: [ObjectIdentifier: (key: any DependencyKey.Type, scope: Scope)] = [:] + + // Defines the lifecycle of the given dependency + enum Scope { + // The dependency is owned by the container. It lives as long as the container itself lives. + // The dependency is strongly referenced by the container. + case container + // The container does not own the dependency. The dependency could be deallocated even if the container + // is still alive, if there are no more strong references to it. + case weak + // Indicates that the DependencyContainer doesn't keep any reference (neither strong nor weak) to the dependency. + // Each time the dependency is requested, a new instance is created and returned + case transient + } + + init(instanceID: UUID = UUID(), configuration: PubNubConfiguration) { + register(value: configuration, forKey: PubNubConfigurationDependencyKey.self) + register(value: instanceID, forKey: PubNubInstanceIDDependencyKey.self) + register(key: FileURLSessionDependencyKey.self, scope: .weak) + register(key: DefaultHTTPSessionDependencyKey.self, scope: .weak) + register(key: HTTPSubscribeSessionDependencyKey.self, scope: .weak) + register(key: HTTPPresenceSessionDependencyKey.self, scope: .weak) + register(key: HTTPSubscribeSessionQueueDependencyKey.self, scope: .weak) + register(key: PresenceStateContainerDependencyKey.self, scope: .weak) + register(key: SubscribeEventEngineDependencyKey.self, scope: .weak) + register(key: PresenceEventEngineDependencyKey.self, scope: .weak) + register(key: SubscriptionSessionDependencyKey.self, scope: .weak) + } + + subscript(key: K.Type) -> K.Value where K: DependencyKey { + guard let underlyingKey = registeredKeys[ObjectIdentifier(key)] else { + preconditionFailure("Cannot find \(key). Ensure this key was registered before") + } + if underlyingKey.scope == .transient { + if let value = underlyingKey.key.value(from: self) as? K.Value { + return value + } else { + preconditionFailure("Cannot create value for key \(key)") + } + } + if let valueWrapper = resolvedValues[ObjectIdentifier(key)] { + if let underlyingValue = valueWrapper.value as? K.Value { + return underlyingValue + } + } + if let value = underlyingKey.key.value(from: self) as? K.Value { + if Mirror(reflecting: value).displayStyle == .class && underlyingKey.scope == .weak { + resolvedValues[ObjectIdentifier(key)] = WeakWrapper(value as AnyObject) + } else { + resolvedValues[ObjectIdentifier(key)] = ValueWrapper(value) + } + return value + } + preconditionFailure("Cannot create value for key \(key)") + } + + func register(key: K.Type, scope: Scope = .container) { + registeredKeys[ObjectIdentifier(key)] = (key: key, scope: scope) + } + + @discardableResult + func register(value: K.Value?, forKey key: K.Type, in scope: Scope = .container) -> DependencyContainer { + guard let value = value else { + return self + } + registeredKeys[ObjectIdentifier(key)] = (key: key, scope: scope) + + if Mirror(reflecting: value).displayStyle == .class && scope == .weak { + resolvedValues[ObjectIdentifier(key)] = WeakWrapper(value as AnyObject) + } else { + resolvedValues[ObjectIdentifier(key)] = ValueWrapper(value) + } + + return self + } +} + +typealias SubscribeEngine = EventEngine +typealias PresenceEngine = EventEngine + +extension DependencyContainer { + var configuration: PubNubConfiguration { + self[PubNubConfigurationDependencyKey.self] + } + + var instanceID: UUID { + self[PubNubInstanceIDDependencyKey.self] + } + + var fileURLSession: URLSessionReplaceable { + self[FileURLSessionDependencyKey.self] + } + + var subscriptionSession: SubscriptionSession { + self[SubscriptionSessionDependencyKey.self] + } + + var presenceStateContainer: PubNubPresenceStateContainer { + self[PresenceStateContainerDependencyKey.self] + } + + var defaultHTTPSession: SessionReplaceable { + resolveSession( + session: self[DefaultHTTPSessionDependencyKey.self], + with: [automaticRetry].compactMap { $0 } + ) + } + + fileprivate var httpSubscribeSession: SessionReplaceable { + resolveSession( + session: self[HTTPSubscribeSessionDependencyKey.self], + with: [instanceIDOperator].compactMap { $0 } + ) + } + + fileprivate var httpPresenceSession: SessionReplaceable { + resolveSession( + session: self[HTTPPresenceSessionDependencyKey.self], + with: [instanceIDOperator].compactMap { $0 } + ) + } + + fileprivate var automaticRetry: RequestOperator? { + configuration.automaticRetry + } + + fileprivate var instanceIDOperator: RequestOperator? { + configuration.useInstanceId ? InstanceIdOperator(instanceID: instanceID.uuidString) : nil + } + + fileprivate var httpSubscribeSessionQueue: DispatchQueue { + self[HTTPSubscribeSessionQueueDependencyKey.self] + } + + fileprivate var subscribeEngine: SubscribeEngine { + self[SubscribeEventEngineDependencyKey.self] + } + + fileprivate var presenceEngine: PresenceEngine { + self[PresenceEventEngineDependencyKey.self] + } +} + +private extension DependencyContainer { + func resolveSession(session: SessionReplaceable, with operators: [RequestOperator]) -> SessionReplaceable { + session.defaultRequestOperator == nil ? session.usingDefault(requestOperator: MultiplexRequestOperator( + operators: operators + )) : session.usingDefault(requestOperator: session.defaultRequestOperator?.merge( + operators: operators + )) + } +} + +// - MARK: PubNubConfiguration + +struct PubNubConfigurationDependencyKey: DependencyKey { + static func value(from container: DependencyContainer) -> PubNubConfiguration { + container.configuration + } +} + +struct PubNubInstanceIDDependencyKey: DependencyKey { + static func value(from container: DependencyContainer) -> UUID { + container.instanceID + } +} + +// MARK: - HTTPSessions + +struct DefaultHTTPSessionDependencyKey: DependencyKey { + static func value(from container: DependencyContainer) -> SessionReplaceable { + HTTPSession(configuration: .pubnub) + } +} + +struct HTTPSubscribeSessionDependencyKey: DependencyKey { + static func value(from container: DependencyContainer) -> SessionReplaceable { + HTTPSession( + configuration: .subscription, + sessionQueue: container.httpSubscribeSessionQueue, + sessionStream: SessionListener(queue: container.httpSubscribeSessionQueue) + ) + } +} + +struct HTTPSubscribeSessionQueueDependencyKey: DependencyKey { + static func value(from container: DependencyContainer) -> DispatchQueue { + DispatchQueue(label: "Subscribe Response Queue") + } +} + +struct HTTPPresenceSessionDependencyKey: DependencyKey { + static func value(from container: DependencyContainer) -> HTTPSession { + HTTPSession( + configuration: .pubnub, + sessionQueue: container.httpSubscribeSessionQueue, + sessionStream: SessionListener(queue: container.httpSubscribeSessionQueue) + ) + } +} + +// MARK: - FileURLSession + +struct FileURLSessionDependencyKey: DependencyKey { + static func value(from container: DependencyContainer) -> URLSessionReplaceable { + URLSession( + configuration: .pubnubBackground, + delegate: FileSessionManager(), + delegateQueue: .main + ) + } +} + +// MARK: - PresenceStateContainer + +struct PresenceStateContainerDependencyKey: DependencyKey { + static func value(from container: DependencyContainer) -> PubNubPresenceStateContainer { + PubNubPresenceStateContainer.shared + } +} + +// MARK: SubscribeEventEngine + +struct SubscribeEventEngineDependencyKey: DependencyKey { + static func value(from container: DependencyContainer) -> SubscribeEngine { + let effectHandlerFactory = SubscribeEffectFactory( + session: container.httpSubscribeSession, + presenceStateContainer: container.presenceStateContainer + ) + let subscribeEngine = SubscribeEngine( + state: Subscribe.UnsubscribedState(), + transition: SubscribeTransition(), + dispatcher: EffectDispatcher(factory: effectHandlerFactory), + dependencies: EventEngineDependencies(value: Subscribe.Dependencies(configuration: container.configuration)) + ) + return subscribeEngine + } +} + +// MARK: PresenceEventEngine + +struct PresenceEventEngineDependencyKey: DependencyKey { + static func value(from container: DependencyContainer) -> PresenceEngine { + let effectHandlerFactory = PresenceEffectFactory( + session: container.httpPresenceSession, + presenceStateContainer: container.presenceStateContainer + ) + let presenceEngine = PresenceEngine( + state: Presence.HeartbeatInactive(), + transition: PresenceTransition(configuration: container.configuration), + dispatcher: EffectDispatcher(factory: effectHandlerFactory), + dependencies: EventEngineDependencies(value: Presence.Dependencies(configuration: container.configuration)) + ) + return presenceEngine + } +} + +// MARK: - SubscriptionSession + +struct SubscriptionSessionDependencyKey: DependencyKey { + static func value(from container: DependencyContainer) -> SubscriptionSession { + if container.configuration.enableEventEngine { + return SubscriptionSession( + strategy: EventEngineSubscriptionSessionStrategy( + configuration: container.configuration, + subscribeEngine: container.subscribeEngine, + presenceEngine: container.presenceEngine, + presenceStateContainer: container.presenceStateContainer + ) + ) + } else { + return SubscriptionSession( + strategy: LegacySubscriptionSessionStrategy( + configuration: container.configuration, + network: container.httpSubscribeSession, + presenceSession: container.httpPresenceSession + ) + ) + } + } +} + +// Provides a standard interface for objects that wrap or encapsulate other objects in a dependency container context. +protocol Wrappable { + associatedtype T + var value: T? { get } +} + +// A concrete implementation of the `Wrappable` protocol, designed to hold a weak reference to the object it wraps. +// It only accepts classes (reference types) as its generic parameter, because weak references +// can only be made to reference types. +private class WeakWrapper: Wrappable { + private weak var optionalValue: T? + + var value: T? { + optionalValue + } + + init(_ value: T) { + self.optionalValue = value + } +} + +// Holds a strong reference to the object it wraps +private class ValueWrapper: Wrappable { + let value: T? + + init(_ value: T) { + self.value = value + } +} diff --git a/Sources/PubNub/EventEngine/Core/EventEngineFactory.swift b/Sources/PubNub/EventEngine/Core/EventEngineFactory.swift deleted file mode 100644 index 0efb70ec..00000000 --- a/Sources/PubNub/EventEngine/Core/EventEngineFactory.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// EventEngineFactory.swift -// -// Copyright (c) PubNub Inc. -// All rights reserved. -// -// This source code is licensed under the license found in the -// LICENSE file in the root directory of this source tree. -// - -import Foundation - -typealias SubscribeEngine = EventEngine<(any SubscribeState), Subscribe.Event, Subscribe.Invocation, Subscribe.Dependencies> -typealias PresenceEngine = EventEngine<(any PresenceState), Presence.Event, Presence.Invocation, Presence.Dependencies> - -typealias SubscribeTransitions = TransitionProtocol<(any SubscribeState), Subscribe.Event, Subscribe.Invocation> -typealias PresenceTransitions = TransitionProtocol<(any PresenceState), Presence.Event, Presence.Invocation> -typealias SubscribeDispatcher = Dispatcher -typealias PresenceDispatcher = Dispatcher - -class EventEngineFactory { - func subscribeEngine( - with configuration: PubNubConfiguration, - dispatcher: some SubscribeDispatcher, - transition: some SubscribeTransitions - ) -> SubscribeEngine { - EventEngine( - state: Subscribe.UnsubscribedState(), - transition: transition, - dispatcher: dispatcher, - dependencies: EventEngineDependencies(value: Subscribe.Dependencies(configuration: configuration)) - ) - } - - func presenceEngine( - with configuration: PubNubConfiguration, - dispatcher: some PresenceDispatcher, - transition: some PresenceTransitions - ) -> PresenceEngine { - EventEngine( - state: Presence.HeartbeatInactive(), - transition: transition, - dispatcher: dispatcher, - dependencies: EventEngineDependencies(value: Presence.Dependencies(configuration: configuration)) - ) - } -} diff --git a/Sources/PubNub/EventEngine/Presence/Effects/WaitEffect.swift b/Sources/PubNub/EventEngine/Presence/Effects/WaitEffect.swift index b1103395..c63ca28c 100644 --- a/Sources/PubNub/EventEngine/Presence/Effects/WaitEffect.swift +++ b/Sources/PubNub/EventEngine/Presence/Effects/WaitEffect.swift @@ -12,15 +12,15 @@ import Foundation class WaitEffect: EffectHandler { private let timerEffect: TimerEffect? - - init(configuration: SubscriptionConfiguration) { + + init(configuration: PubNubConfiguration) { if configuration.heartbeatInterval > 0 { self.timerEffect = TimerEffect(interval: TimeInterval(configuration.heartbeatInterval)) } else { self.timerEffect = nil } } - + func performTask(completionBlock: @escaping ([Presence.Event]) -> Void) { guard let timerEffect = timerEffect else { completionBlock([]); return @@ -29,7 +29,7 @@ class WaitEffect: EffectHandler { completionBlock([.timesUp]) }) } - + func cancelTask() { timerEffect?.cancelTask() } diff --git a/Sources/PubNub/EventEngine/Presence/Helpers/PresenceHeartbeatRequest.swift b/Sources/PubNub/EventEngine/Presence/Helpers/PresenceHeartbeatRequest.swift index 5d1cf160..d4d14865 100644 --- a/Sources/PubNub/EventEngine/Presence/Helpers/PresenceHeartbeatRequest.swift +++ b/Sources/PubNub/EventEngine/Presence/Helpers/PresenceHeartbeatRequest.swift @@ -13,7 +13,7 @@ import Foundation class PresenceHeartbeatRequest { let channels: [String] let groups: [String] - let configuration: SubscriptionConfiguration + let configuration: PubNubConfiguration private let session: SessionReplaceable private let sessionResponseQueue: DispatchQueue @@ -24,7 +24,7 @@ class PresenceHeartbeatRequest { channels: [String], groups: [String], channelStates: [String: JSONCodable], - configuration: SubscriptionConfiguration, + configuration: PubNubConfiguration, session: SessionReplaceable, sessionResponseQueue: DispatchQueue ) { @@ -49,7 +49,7 @@ class PresenceHeartbeatRequest { ) request?.validate().response(on: sessionResponseQueue, decoder: GenericServiceResponseDecoder()) { result in switch result { - case .success(_): + case .success: completionBlock(.success(())) case .failure(let error): completionBlock(.failure(error as? PubNubError ?? PubNubError(.unknown, underlying: error))) diff --git a/Sources/PubNub/EventEngine/Presence/Helpers/PresenceLeaveRequest.swift b/Sources/PubNub/EventEngine/Presence/Helpers/PresenceLeaveRequest.swift index 4c08ba73..9e7f44e3 100644 --- a/Sources/PubNub/EventEngine/Presence/Helpers/PresenceLeaveRequest.swift +++ b/Sources/PubNub/EventEngine/Presence/Helpers/PresenceLeaveRequest.swift @@ -13,7 +13,7 @@ import Foundation class PresenceLeaveRequest { let channels: [String] let groups: [String] - let configuration: SubscriptionConfiguration + let configuration: PubNubConfiguration private let session: SessionReplaceable private let sessionResponseQueue: DispatchQueue @@ -22,7 +22,7 @@ class PresenceLeaveRequest { init( channels: [String], groups: [String], - configuration: SubscriptionConfiguration, + configuration: PubNubConfiguration, session: SessionReplaceable, sessionResponseQueue: DispatchQueue ) { diff --git a/Sources/PubNub/EventEngine/Presence/Presence.swift b/Sources/PubNub/EventEngine/Presence/Presence.swift index a94a053a..553f7a15 100644 --- a/Sources/PubNub/EventEngine/Presence/Presence.swift +++ b/Sources/PubNub/EventEngine/Presence/Presence.swift @@ -20,6 +20,7 @@ extension PresenceState { var channels: [String] { input.channels } + var groups: [String] { input.groups } @@ -36,7 +37,7 @@ extension Presence { struct Heartbeating: PresenceState { let input: PresenceInput } - + struct HeartbeatCooldown: PresenceState { let input: PresenceInput } @@ -46,18 +47,18 @@ extension Presence { let retryAttempt: Int let error: PubNubError } - + struct HeartbeatFailed: PresenceState { let input: PresenceInput let error: PubNubError } - + struct HeartbeatStopped: PresenceState { let input: PresenceInput } - + struct HeartbeatInactive: PresenceState { - let input: PresenceInput = PresenceInput() + let input: PresenceInput = .init() } } @@ -79,7 +80,7 @@ extension Presence { extension Presence { struct Dependencies { - let configuration: SubscriptionConfiguration + let configuration: PubNubConfiguration } } @@ -91,11 +92,11 @@ extension Presence { case leave(channels: [String], groups: [String]) case delayedHeartbeat(channels: [String], groups: [String], retryAttempt: Int, error: PubNubError) case wait - + enum Cancellable: AnyCancellableInvocation { case wait case delayedHeartbeat - + var id: String { switch self { case .wait: @@ -105,16 +106,16 @@ extension Presence { } } } - + var id: String { switch self { - case .heartbeat(_,_): + case .heartbeat: return "Presence.Heartbeat" case .wait: return Cancellable.wait.id case .delayedHeartbeat: return Cancellable.delayedHeartbeat.id - case .leave(_,_): + case .leave: return "Presence.Leave" } } diff --git a/Sources/PubNub/EventEngine/Subscribe/Helpers/SubscribeRequest.swift b/Sources/PubNub/EventEngine/Subscribe/Helpers/SubscribeRequest.swift index c4a7f93e..84046296 100644 --- a/Sources/PubNub/EventEngine/Subscribe/Helpers/SubscribeRequest.swift +++ b/Sources/PubNub/EventEngine/Subscribe/Helpers/SubscribeRequest.swift @@ -16,7 +16,7 @@ class SubscribeRequest { let timetoken: Timetoken? let region: Int? - private let configuration: SubscriptionConfiguration + private let configuration: PubNubConfiguration private let session: SessionReplaceable private let sessionResponseQueue: DispatchQueue private let channelStates: [String: JSONCodable] @@ -27,7 +27,7 @@ class SubscribeRequest { var onAuthChallengeReceived: (() -> Void)? init( - configuration: SubscriptionConfiguration, + configuration: PubNubConfiguration, channels: [String], groups: [String], channelStates: [String: JSONCodable], diff --git a/Sources/PubNub/EventEngine/Subscribe/Subscribe.swift b/Sources/PubNub/EventEngine/Subscribe/Subscribe.swift index 62ea5499..43d1065e 100644 --- a/Sources/PubNub/EventEngine/Subscribe/Subscribe.swift +++ b/Sources/PubNub/EventEngine/Subscribe/Subscribe.swift @@ -37,13 +37,13 @@ extension Subscribe { let cursor: SubscribeCursor let connectionStatus = ConnectionStatus.connecting } - + struct HandshakeStoppedState: SubscribeState { let input: SubscribeInput let cursor: SubscribeCursor let connectionStatus = ConnectionStatus.disconnected } - + struct HandshakeReconnectingState: SubscribeState { let input: SubscribeInput let cursor: SubscribeCursor @@ -51,13 +51,13 @@ extension Subscribe { let reason: PubNubError let connectionStatus = ConnectionStatus.connecting } - + struct HandshakeFailedState: SubscribeState { let input: SubscribeInput let cursor: SubscribeCursor let error: PubNubError let connectionStatus: ConnectionStatus - + init(input: SubscribeInput, cursor: SubscribeCursor, error: PubNubError) { self.input = input self.cursor = cursor @@ -65,13 +65,13 @@ extension Subscribe { self.connectionStatus = .connectionError(error) } } - + struct ReceivingState: SubscribeState { let input: SubscribeInput let cursor: SubscribeCursor let connectionStatus = ConnectionStatus.connected } - + struct ReceiveReconnectingState: SubscribeState { let input: SubscribeInput let cursor: SubscribeCursor @@ -79,19 +79,19 @@ extension Subscribe { let reason: PubNubError let connectionStatus = ConnectionStatus.connected } - + struct ReceiveStoppedState: SubscribeState { let input: SubscribeInput let cursor: SubscribeCursor let connectionStatus = ConnectionStatus.disconnected } - + struct ReceiveFailedState: SubscribeState { let input: SubscribeInput let cursor: SubscribeCursor let error: PubNubError let connectionStatus: ConnectionStatus - + init(input: SubscribeInput, cursor: SubscribeCursor, error: PubNubError) { self.input = input self.cursor = cursor @@ -99,10 +99,10 @@ extension Subscribe { self.connectionStatus = .disconnectedUnexpectedly(error) } } - + struct UnsubscribedState: SubscribeState { - let cursor: SubscribeCursor = SubscribeCursor(timetoken: 0)! - let input: SubscribeInput = SubscribeInput() + let cursor: SubscribeCursor = .init(timetoken: 0)! + let input: SubscribeInput = .init() let connectionStatus = ConnectionStatus.disconnected } } @@ -139,10 +139,10 @@ extension Subscribe { extension Subscribe { struct Dependencies { - let configuration: SubscriptionConfiguration + let configuration: PubNubConfiguration let listeners: [BaseSubscriptionListener] - - init(configuration: SubscriptionConfiguration, listeners: [BaseSubscriptionListener] = []) { + + init(configuration: PubNubConfiguration, listeners: [BaseSubscriptionListener] = []) { self.configuration = configuration self.listeners = listeners } @@ -159,7 +159,7 @@ extension Subscribe { case receiveReconnect(channels: [String], groups: [String], cursor: SubscribeCursor, retryAttempt: Int, reason: PubNubError) case emitStatus(change: Subscribe.ConnectionStatusChange) case emitMessages(events: [SubscribeMessagePayload], forCursor: SubscribeCursor) - + enum Cancellable: AnyCancellableInvocation { case handshakeRequest case handshakeReconnect @@ -182,17 +182,17 @@ extension Subscribe { var id: String { switch self { - case .handshakeRequest(_, _): + case .handshakeRequest: return Cancellable.handshakeRequest.id - case .handshakeReconnect(_, _, _, _): + case .handshakeReconnect: return Cancellable.handshakeReconnect.id - case .receiveMessages(_, _, _): + case .receiveMessages: return Cancellable.receiveMessages.id - case .receiveReconnect(_, _, _, _, _): + case .receiveReconnect: return Cancellable.receiveReconnect.id - case .emitMessages(_,_): + case .emitMessages: return "Subscribe.EmitMessages" - case .emitStatus(_): + case .emitStatus: return "Subscribe.EmitStatus" } } diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift index e8c1d983..edd95574 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift @@ -8,8 +8,8 @@ // LICENSE file in the root directory of this source tree. // -import Foundation import CommonCrypto +import Foundation /// Represents the result of encrypted `Data` public struct EncryptedData { @@ -38,34 +38,42 @@ public protocol Cryptor: Hashable { /// /// - Important: `[0x41, 0x43, 0x52, 0x48]` and `[0x00, 0x00, 0x00, 0x00]` values are reserved var id: CryptorId { get } - + /// Encrypts the given `Data` object /// /// - Parameters: /// - data: Data to encrypt - /// - Returns: A success, storing an ``EncryptedData`` value if operation succeeds. Otherwise, a failure storing `PubNubError` is returned + /// - Returns: + /// - **Success**: ``EncryptedData`` representing encrypted content + /// - **Failure**: `Error` describing the reason of failure func encrypt(data: Data) -> Result - + /// Decrypts the given `Data` object /// /// - Parameters: /// - data: Data to encrypt - /// - Returns: A success, storing decrypted `Data` if operation succeeds. Otherwise, a failure storing `PubNubError` is returned + /// - Returns: + /// - **Success**: ``Data`` representing decrypted content + /// - **Failure**: `Error` describing the reason of failure func decrypt(data: EncryptedData) -> Result - + /// Encrypts the given `InputStream` object /// /// - Parameters: /// - stream: Stream to encrypt /// - contentLength: Content length of encoded stream - /// - Returns: A success, storing an ``EncryptedStreamData`` value if operation succeeds. Otherwise, a failure storing `PubNubError` is returned + /// - Returns: + /// - **Success**: ``EncryptedStreamData`` representing encrypted content + /// - **Failure**: `Error` describing the reason of failure func encrypt(stream: InputStream, contentLength: Int) -> Result - + /// Decrypts the given `InputStream` object - /// + /// /// - Parameters: /// - data: A value describing encrypted stream /// - outputPath: URL where the stream should be decrypted to - /// - Returns: A success, storing a decrypted `InputStream` value at the given path if operation succeeds. Otherwise, a failure storing `PubNubError` is returned + /// - Returns: + /// - **Success**: ``InputStream`` representing decrypted content + /// - **Failure**: `Error` describing the reason of failure func decrypt(data: EncryptedStreamData, outputPath: URL) -> Result } diff --git a/Sources/PubNub/Networking/HTTPSession.swift b/Sources/PubNub/Networking/HTTPSession.swift index dbdc97f4..d57f08f7 100644 --- a/Sources/PubNub/Networking/HTTPSession.swift +++ b/Sources/PubNub/Networking/HTTPSession.swift @@ -32,7 +32,6 @@ public final class HTTPSession { public var defaultRequestOperator: RequestOperator? /// The collection of associations between `URLSessionTask` and their corresponding `Request` var taskToRequest: [URLSessionTask: RequestReplaceable] = [:] - /// Default HTTPSession configuration for PubNub REST endpoints static var pubnub = HTTPSession(configuration: .pubnub) @@ -43,13 +42,14 @@ public final class HTTPSession { requestQueue: DispatchQueue? = nil, sessionStream: SessionStream? = nil ) { - precondition(session.delegateQueue.underlyingQueue === sessionQueue, - "Session.sessionQueue must be the same DispatchQueue used as the URLSession.delegate underlyingQueue") + precondition( + session.delegateQueue.underlyingQueue === sessionQueue, + "Session.sessionQueue must be the same DispatchQueue used as the URLSession.delegate underlyingQueue" + ) self.session = session self.sessionQueue = sessionQueue self.requestQueue = requestQueue ?? DispatchQueue(label: "com.pubnub.session.requestQueue", target: sessionQueue) - self.delegate = delegate self.sessionStream = sessionStream @@ -65,27 +65,34 @@ public final class HTTPSession { requestQueue: DispatchQueue? = nil, sessionStream: SessionStream? = nil ) { - let delegateQueue = OperationQueue(maxConcurrentOperationCount: 1, - underlyingQueue: sessionQueue, - name: "org.pubnub.httpClient.URLSessionReplaceableDelegate") - - let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue) + let delegateQueue = OperationQueue( + maxConcurrentOperationCount: 1, + underlyingQueue: sessionQueue, + name: "org.pubnub.httpClient.URLSessionReplaceableDelegate" + ) + + let session = URLSession( + configuration: configuration, + delegate: delegate, + delegateQueue: delegateQueue + ) session.sessionDescription = "Underlying URLSession for: com.pubnub.session" - self.init(session: session, - delegate: delegate, - sessionQueue: sessionQueue, - requestQueue: requestQueue, - sessionStream: sessionStream) + self.init( + session: session, + delegate: delegate, + sessionQueue: sessionQueue, + requestQueue: requestQueue, + sessionStream: sessionStream + ) } deinit { PubNub.log.debug("Session Destroyed \(sessionID) with active requests \(taskToRequest.values.map { $0.requestID })") - taskToRequest.values.forEach { - $0.cancel(PubNubError(.sessionDeinitialized, router: $0.router)) + for value in taskToRequest.values { + value.cancel(PubNubError(.sessionDeinitialized, router: value.router)) } - invalidateAndCancel() } @@ -112,12 +119,14 @@ public final class HTTPSession { with router: HTTPRouter, requestOperator: RequestOperator? = nil ) -> RequestReplaceable { - let request = Request(with: router, - requestQueue: sessionQueue, - sessionStream: sessionStream, - requestOperator: requestOperator, - delegate: self, - createdBy: sessionID) + let request = Request( + with: router, + requestQueue: sessionQueue, + sessionStream: sessionStream, + requestOperator: requestOperator, + delegate: self, + createdBy: sessionID + ) perform(request) @@ -225,7 +234,6 @@ public final class HTTPSession { public func invalidateAndCancel() { // Ensure that we lock out task creation prior to invalidating isInvalidated = true - session.invalidateAndCancel() } } diff --git a/Sources/PubNub/Networking/Routers/PresenceRouter.swift b/Sources/PubNub/Networking/Routers/PresenceRouter.swift index 6d0abc0a..594f7748 100644 --- a/Sources/PubNub/Networking/Routers/PresenceRouter.swift +++ b/Sources/PubNub/Networking/Routers/PresenceRouter.swift @@ -85,7 +85,7 @@ struct PresenceRouter: HTTPRouter { var endpoint: Endpoint var configuration: RouterConfiguration - + // Protocol Properties var service: PubNubService { return .presence @@ -233,7 +233,7 @@ struct HereNowResponseDecoder: ResponseDecoder { // Single Channel w/o Groups if channels.count == 1, groups.isEmpty, let channel = channels.first { - hereNowPayload = [channel: try Constant.jsonDecoder.decode(HereNowChannelsPayload.self, from: response.payload)] + hereNowPayload = try [channel: Constant.jsonDecoder.decode(HereNowChannelsPayload.self, from: response.payload)] } else { // Multi-Channel HereNow hereNowPayload = try Constant.jsonDecoder.decode( @@ -241,11 +241,13 @@ struct HereNowResponseDecoder: ResponseDecoder { ).payload.channels } - let decodedResponse = EndpointResponse(router: response.router, - request: response.request, - response: response.response, - data: response.data, - payload: hereNowPayload) + let decodedResponse = EndpointResponse( + router: response.router, + request: response.request, + response: response.response, + data: response.data, + payload: hereNowPayload + ) return .success(decodedResponse) } catch { diff --git a/Sources/PubNub/Networking/Routers/SubscribeRouter.swift b/Sources/PubNub/Networking/Routers/SubscribeRouter.swift index 41004f9d..7c8b6022 100644 --- a/Sources/PubNub/Networking/Routers/SubscribeRouter.swift +++ b/Sources/PubNub/Networking/Routers/SubscribeRouter.swift @@ -119,11 +119,13 @@ struct SubscribeDecoder: ResponseDecoder { do { let decodedPayload = try Constant.jsonDecoder.decode(Payload.self, from: response.payload) - let decodedResponse = EndpointResponse(router: response.router, - request: response.request, - response: response.response, - data: response.data, - payload: decodedPayload) + let decodedResponse = EndpointResponse( + router: response.router, + request: response.request, + response: response.response, + data: response.data, + payload: decodedPayload + ) return .success(decodedResponse) } catch { diff --git a/Sources/PubNub/PubNub.swift b/Sources/PubNub/PubNub.swift index 98a9350b..e9426e66 100644 --- a/Sources/PubNub/PubNub.swift +++ b/Sources/PubNub/PubNub.swift @@ -16,26 +16,21 @@ public class PubNub { public let instanceID: UUID /// A copy of the configuration object used for this session public private(set) var configuration: PubNubConfiguration - /// Session used for performing request/response REST calls public let networkSession: SessionReplaceable - /// Session used for performing subscription calls - public let subscription: SubscriptionSession - /// The URLSession used when making File upload/download requests public var fileURLSession: URLSessionReplaceable /// The URLSessionDelegate used by the `fileSession` to handle file responses - public var fileSessionManager: FileSessionManager? { - return fileURLSession.delegate as? FileSessionManager - } - + public var fileSessionManager: FileSessionManager? { fileURLSession.delegate as? FileSessionManager } /// Global log instance for the PubNub SDK public static var log = PubNubLogger(levels: [.event, .warn, .error], writers: [ConsoleLogWriter(), FileLogWriter()]) // Global log instance for Logging issues/events public static var logLog = PubNubLogger(levels: [.log], writers: [ConsoleLogWriter()]) + /// Session used for performing subscription calls + let subscription: SubscriptionSession // Container that holds current Presence states for given channels/channel groups - internal let presenceStateContainer = PubNubPresenceStateContainer.shared - + let presenceStateContainer: PubNubPresenceStateContainer + /// Creates a PubNub session with the specified configuration /// /// - Parameters: @@ -49,64 +44,21 @@ public class PubNub { subscribeSession: SessionReplaceable? = nil, fileSession: URLSessionReplaceable? = nil ) { - let instanceID = UUID() - - // Default operators based on config - var operators = [RequestOperator]() - if let retryOperator = configuration.automaticRetry { - operators.append(retryOperator) - } - if configuration.useInstanceId { - let instanceIdOperator = InstanceIdOperator(instanceID: instanceID.description) - operators.append(instanceIdOperator) - } + let container = DependencyContainer(instanceID: UUID(), configuration: configuration) + container.register(value: session, forKey: DefaultHTTPSessionDependencyKey.self) + container.register(value: subscribeSession, forKey: HTTPSubscribeSessionDependencyKey.self) + container.register(value: fileSession, forKey: FileURLSessionDependencyKey.self) - // Mutable session - var networkSession = session ?? HTTPSession(configuration: configuration.urlSessionConfiguration) - - // Configure the default request operators - if networkSession.defaultRequestOperator == nil { - networkSession.defaultRequestOperator = MultiplexRequestOperator(operators: operators) - } else { - networkSession.defaultRequestOperator = networkSession - .defaultRequestOperator? - .merge(requestOperator: MultiplexRequestOperator(operators: operators)) - } - - let fileSession = fileSession ?? URLSession( - configuration: .pubnubBackground, - delegate: FileSessionManager(), - delegateQueue: .main - ) - - // Set initial session also based on configuration - let subscriptionSession = SubscribeSessionFactory.shared.getSession( - from: configuration, - with: subscribeSession, - presenceSession: session - ) - - self.init( - instanceID: instanceID, - configuration: configuration, - session: networkSession, - fileSession: fileSession, - subscriptionSession: subscriptionSession - ) + self.init(container: container) } - - init( - instanceID: UUID = UUID(), - configuration: PubNubConfiguration, - session: SessionReplaceable, - fileSession: URLSessionReplaceable, - subscriptionSession: SubscriptionSession - ) { - self.instanceID = instanceID - self.configuration = configuration - self.subscription = subscriptionSession - self.networkSession = session - self.fileURLSession = fileSession + + init(container: DependencyContainer) { + self.instanceID = container.instanceID + self.configuration = container.configuration + self.subscription = container.subscriptionSession + self.networkSession = container.defaultHTTPSession + self.fileURLSession = container.fileURLSession + self.presenceStateContainer = container.presenceStateContainer } func route( @@ -159,15 +111,14 @@ public extension PubNub { public var customSession: SessionReplaceable? /// The endpoint configuration used by the request public var customConfiguration: RouterConfiguration? - /// The response queue that will + /// The queue that will be used for dispatching a response public var responseQueue: DispatchQueue /// Default init for all fields /// - Parameters: /// - customSession: The custom Network session that that will be used to make the request /// - customConfiguration: The endpoint configuration used by the request - /// - responseQueue: The response queue that will - /// + /// - responseQueue: The queue that will be used for dispatching a response public init( customSession: SessionReplaceable? = nil, customConfiguration: RouterConfiguration? = nil, @@ -189,6 +140,7 @@ public extension PubNub { /// - Parameters: /// - start: The value of the start of a next page /// - end: The value of the end of a slice of paged data + /// - totalCount: Number of items to fetch public init(start: String? = nil, end: String? = nil, totalCount: Int? = nil) { self.start = start self.end = end @@ -340,7 +292,6 @@ public extension PubNub { /// - Parameters: /// - channel: The destination of the message /// - message: The message to publish - /// - shouldCompress: Whether the message needs to be compressed before transmission /// - custom: Custom configuration overrides for this request /// - completion: The async `Result` of the method call /// - **Success**: The `Timetoken` of the published Message @@ -375,7 +326,6 @@ public extension PubNub { /// - and: List of channel groups to subscribe on /// - at: The initial timetoken to subscribe with /// - withPresence: If true it also subscribes to presence events on the specified channels. - /// - region: The region code from a previous `SubscribeCursor` func subscribe( to channels: [String], and channelGroups: [String] = [], @@ -406,14 +356,12 @@ public extension PubNub { } /// Stops the subscriptions in progress - /// - Important: This subscription might be shared with multiple `PubNub` instances. func disconnect() { subscription.disconnect() } /// Reconnets to a stopped subscription with the previous subscribed channels and channel groups /// - Parameter at: The timetoken value used to reconnect or nil to use the previous stored value - /// - Important: This subscription might be shared with multiple `PubNub` instances. func reconnect(at timetoken: Timetoken? = nil) { subscription.reconnect(at: SubscribeCursor(timetoken: timetoken)) } @@ -448,7 +396,7 @@ public extension PubNub { var connectionStatus: ConnectionStatus { return subscription.connectionStatus } - + /// An override for the default filter expression set during initialization var subscribeFilterExpression: String? { get { @@ -465,11 +413,11 @@ extension PubNub: SubscribeReceiver { func registerAdapter(_ adapter: BaseSubscriptionListenerAdapter) { subscription.registerAdapter(adapter) } - + func hasRegisteredAdapter(with uuid: UUID) -> Bool { subscription.hasRegisteredAdapter(with: uuid) } - + func internalSubscribe( with channels: [Subscription], and groups: [Subscription], @@ -481,7 +429,7 @@ extension PubNub: SubscribeReceiver { at: timetoken ) } - + func internalUnsubscribe( from channels: [Subscription], and groups: [Subscription], @@ -501,15 +449,15 @@ extension PubNub: EntityCreator { public func channel(_ name: String) -> ChannelRepresentation { subscription.channel(name) } - + public func channelGroup(_ name: String) -> ChannelGroupRepresentation { subscription.channelGroup(name) } - + public func userMetadata(_ name: String) -> UserMetadataRepresentation { subscription.userMetadata(name) } - + public func channelMetadata(_ name: String) -> ChannelMetadataRepresentation { subscription.channelMetadata(name) } @@ -523,6 +471,7 @@ public extension PubNub { /// - state: The UUID for which to query the subscribed channels of /// - on: Additional network configuration to use on the request /// - and: The queue the completion handler should be returned on + /// - custom: Custom configuration overrides for this request /// - completion: The async `Result` of the method call /// - **Success**: The presence State set as a `JSONCodable` /// - **Failure**: An `Error` describing the failure @@ -538,14 +487,14 @@ public extension PubNub { configuration: requestConfig.customConfiguration ?? configuration ) let shouldMaintainPresenceState = configuration.enableEventEngine && configuration.maintainPresenceState - + route( router, requestOperator: configuration.automaticRetry?.retryOperator(for: .presence), responseDecoder: PresenceResponseDecoder>(), custom: requestConfig ) { [weak self] result in - if case .success(_) = result { + if case .success = result { if shouldMaintainPresenceState { self?.presenceStateContainer.registerState(AnyJSON(state), forChannels: channels) } @@ -559,6 +508,7 @@ public extension PubNub { /// - for: The UUID for which to query the subscribed channels of /// - on: Additional network configuration to use on the request /// - and: The queue the completion handler should be returned on + /// - custom: Custom configuration overrides for this request /// - completion: The async `Result` of the method call /// - **Success**: A `Tuple` containing the UUID that set the State and a `Dictionary` of channels mapped to their respective State /// - **Failure**: An `Error` describing the failure @@ -685,7 +635,6 @@ public extension PubNub { /// - completion: The async `Result` of the method call /// - **Success**: The channel-group that was removed /// - **Failure**: An `Error` describing the failure - /// - result: A `Result` containing either the removed channel-group **or** an `Error` func remove( channelGroup: String, custom requestConfig: RequestConfiguration = RequestConfiguration(), @@ -1099,7 +1048,7 @@ public extension PubNub { /// - page: The paging object used for pagination /// - custom: Custom configuration overrides for this request /// - completion: The async `Result` of the method call - /// - **Success**: A `Tuple` of a `Dictionary` of channels mapped to an `Array` their respective `PubNubMessages`, and the next request `PubNubBoundedPage` (if one exists) + /// - **Success**: A `Tuple` containing a `Dictionary` mapping channels to `PubNubMessage` arrays, and an optional next `PubNubBoundedPage`. /// - **Failure**: An `Error` describing the failure func fetchMessageHistory( for channels: [String], @@ -1275,9 +1224,7 @@ public extension PubNub { case let .success(response): completion?(.success(( actions: response.payload.actions.map { PubNubMessageActionBase(from: $0, on: channel) }, - next: PubNubBoundedPageBase( - start: response.payload.start, end: response.payload.end, limit: response.payload.limit - ) + next: PubNubBoundedPageBase(start: response.payload.start, end: response.payload.end, limit: response.payload.limit) ))) case let .failure(error): completion?(.failure(error)) @@ -1380,11 +1327,11 @@ public extension PubNub { // MARK: - Crypto -extension PubNub { +public extension PubNub { /// Encrypts the `Data` object using `CryptoModule` provided in configuration /// - Parameter message: The plain text message to be encrypted /// - Returns: A `Result` containing either the encryped `Data` (mapped to Base64-encoded data) or the `CryptoError` - public func encrypt(message: String) -> Result { + func encrypt(message: String) -> Result { guard let cryptoModule = configuration.cryptoModule else { PubNub.log.error(ErrorDescription.missingCryptoKey) return .failure(CryptoError.invalidKey) @@ -1392,7 +1339,7 @@ extension PubNub { guard let dataMessage = message.data(using: .utf8) else { return .failure(CryptoError.decodeError) } - + return cryptoModule.encrypt(data: dataMessage).map { $0.base64EncodedData() }.mapError { @@ -1401,9 +1348,9 @@ extension PubNub { } /// Decrypts the given `Data` object using `CryptoModule` provided in `configuration` - /// - Parameter message: The encrypted `Data` to decrypt - /// - Returns: A `Result` containing either the decrypted plain text message or the `CryptoError` - public func decrypt(data: Data) -> Result { + /// - Parameter data: The encrypted `Data` to decrypt + /// - Returns: A `Result` containing either the decrypted plain text message or the `CryptoError` + func decrypt(data: Data) -> Result { guard let cryptoModule = configuration.cryptoModule else { PubNub.log.error(ErrorDescription.missingCryptoKey) return .failure(CryptoError.invalidKey) @@ -1412,7 +1359,7 @@ extension PubNub { PubNub.log.error("Cannot create Base64-encoded data") return .failure(CryptoError.decodeError) } - + return cryptoModule.decrypt(data: base64EncodedData) .flatMap { guard let string = String(data: $0, encoding: .utf8) else { @@ -1466,45 +1413,46 @@ extension PubNub: EventEmitter { public var queue: DispatchQueue { subscription.queue } + public var uuid: UUID { subscription.uuid } - + public var onEvent: ((PubNubEvent) -> Void)? { get { subscription.onEvent } set { subscription.onEvent = newValue } } - + public var onEvents: (([PubNubEvent]) -> Void)? { get { subscription.onEvents } set { subscription.onEvents = newValue } } - + public var onMessage: ((PubNubMessage) -> Void)? { get { subscription.onMessage } set { subscription.onMessage = newValue } } - + public var onSignal: ((PubNubMessage) -> Void)? { get { subscription.onSignal } set { subscription.onSignal = newValue } } - + public var onPresence: ((PubNubPresenceChange) -> Void)? { get { subscription.onPresence } set { subscription.onPresence = newValue } } - + public var onMessageAction: ((PubNubMessageActionEvent) -> Void)? { get { subscription.onMessageAction } set { subscription.onMessageAction = newValue } } - + public var onFileEvent: ((PubNubFileChangeEvent) -> Void)? { get { subscription.onFileEvent } set { subscription.onFileEvent = newValue } } - + public var onAppContext: ((PubNubAppContextEvent) -> Void)? { get { subscription.onAppContext } set { subscription.onAppContext = newValue } @@ -1519,3 +1467,5 @@ extension PubNub: StatusEmitter { set { subscription.onConnectionStateChange = newValue } } } + +// swiftlint:disable:this file_length diff --git a/Sources/PubNub/Subscription/Strategy/EventEngineSubscriptionSessionStrategy.swift b/Sources/PubNub/Subscription/Strategy/EventEngineSubscriptionSessionStrategy.swift index 3ce0aa1e..d3be17da 100644 --- a/Sources/PubNub/Subscription/Strategy/EventEngineSubscriptionSessionStrategy.swift +++ b/Sources/PubNub/Subscription/Strategy/EventEngineSubscriptionSessionStrategy.swift @@ -17,7 +17,7 @@ class EventEngineSubscriptionSessionStrategy: SubscriptionSessionStrategy { let presenceStateContainer: PubNubPresenceStateContainer var listeners: WeakSet = WeakSet([]) - var configuration: SubscriptionConfiguration + var configuration: PubNubConfiguration var previousTokenResponse: SubscribeCursor? var filterExpression: String? { didSet { @@ -26,7 +26,7 @@ class EventEngineSubscriptionSessionStrategy: SubscriptionSessionStrategy { } internal init( - configuration: SubscriptionConfiguration, + configuration: PubNubConfiguration, subscribeEngine: SubscribeEngine, presenceEngine: PresenceEngine, presenceStateContainer: PubNubPresenceStateContainer @@ -57,8 +57,6 @@ class EventEngineSubscriptionSessionStrategy: SubscriptionSessionStrategy { deinit { PubNub.log.debug("SubscriptionSession Destroyed") - // Poke the session factory to clean up nil values - SubscribeSessionFactory.shared.sessionDestroyed() } private func listenForStateUpdates() { @@ -222,10 +220,6 @@ class EventEngineSubscriptionSessionStrategy: SubscriptionSessionStrategy { sendPresenceEvent(event: .leftAll) } - func onListenerAdded(_ listener: BaseSubscriptionListener) { - updateSubscribeEngineDependencies() - } - private func notify(listeners closure: (BaseSubscriptionListener) -> Void) { listeners.allObjects.forEach { closure($0) } } diff --git a/Sources/PubNub/Subscription/Strategy/LegacySubscriptionSessionStrategy.swift b/Sources/PubNub/Subscription/Strategy/LegacySubscriptionSessionStrategy.swift index 071312e8..da2c4ac7 100644 --- a/Sources/PubNub/Subscription/Strategy/LegacySubscriptionSessionStrategy.swift +++ b/Sources/PubNub/Subscription/Strategy/LegacySubscriptionSessionStrategy.swift @@ -17,7 +17,7 @@ class LegacySubscriptionSessionStrategy: SubscriptionSessionStrategy { let sessionStream: SessionListener let responseQueue: DispatchQueue - var configuration: SubscriptionConfiguration + var configuration: PubNubConfiguration var listeners: WeakSet = WeakSet([]) var filterExpression: String? var messageCache = [SubscribeMessagePayload?].init(repeating: nil, count: 100) @@ -68,7 +68,7 @@ class LegacySubscriptionSessionStrategy: SubscriptionSessionStrategy { var internalState = Atomic(SubscriptionState()) internal init( - configuration: SubscriptionConfiguration, + configuration: PubNubConfiguration, network subscribeSession: SessionReplaceable, presenceSession: SessionReplaceable ) { @@ -103,8 +103,6 @@ class LegacySubscriptionSessionStrategy: SubscriptionSessionStrategy { PubNub.log.debug("SubscriptionSession Destroyed") longPollingSession.invalidateAndCancel() nonSubscribeSession.invalidateAndCancel() - // Poke the session factory to clean up nil values - SubscribeSessionFactory.shared.sessionDestroyed() } // MARK: - Subscription Loop @@ -381,10 +379,6 @@ class LegacySubscriptionSessionStrategy: SubscriptionSessionStrategy { } } - func onListenerAdded(_ listener: BaseSubscriptionListener) { - - } - private func notify(listeners closure: (BaseSubscriptionListener) -> Void) { listeners.allObjects.forEach { closure($0) } } diff --git a/Sources/PubNub/Subscription/Strategy/SubscriptionSessionStrategy.swift b/Sources/PubNub/Subscription/Strategy/SubscriptionSessionStrategy.swift index 274085e1..7cc8b888 100644 --- a/Sources/PubNub/Subscription/Strategy/SubscriptionSessionStrategy.swift +++ b/Sources/PubNub/Subscription/Strategy/SubscriptionSessionStrategy.swift @@ -12,7 +12,7 @@ import Foundation protocol SubscriptionSessionStrategy: AnyObject { var uuid: UUID { get } - var configuration: SubscriptionConfiguration { get set } + var configuration: PubNubConfiguration { get set } var subscribedChannels: [String] { get } var subscribedChannelGroups: [String] { get } var subscriptionCount: Int { get } @@ -32,8 +32,7 @@ protocol SubscriptionSessionStrategy: AnyObject { mainGroups: [PubNubChannel], presenceGroupsOnly: [PubNubChannel] ) - - func onListenerAdded(_ listener: BaseSubscriptionListener) + func reconnect(at cursor: SubscribeCursor?) func disconnect() func unsubscribeAll() diff --git a/Sources/PubNub/Subscription/SubscribeSessionFactory.swift b/Sources/PubNub/Subscription/SubscribeSessionFactory.swift deleted file mode 100644 index caad6a3a..00000000 --- a/Sources/PubNub/Subscription/SubscribeSessionFactory.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// SubscribeSessionFactory.swift -// -// Copyright (c) PubNub Inc. -// All rights reserved. -// -// This source code is licensed under the license found in the -// LICENSE file in the root directory of this source tree. -// - -import Foundation - -/// A factory that manages instances of `SubscriptionSession` -/// -/// This factory attempts to ensure that regardless of how many `PubNub` -/// instances you will only create a single `SubscriptionSession`. -/// -/// You should only use one instance of a `SubscriptionSession` unless you have a very specialized workflow. -/// Such as one of the following: -/// * Subscribe using multiple sets of subscribe keys -/// * Need separate network configurations on a per channel/group basis -/// -/// - Important: Having multiple `SubscriptionSession` instances will result in -/// increase network usage and battery drain. -@available(*, deprecated, message: "Use methods from a PubNub object to subscribe/unsubscribe") -public class SubscribeSessionFactory { - private typealias SessionMap = [Int: WeakBox] - - /// The singleton instance for this factory - public let subscribeQueue = DispatchQueue(label: "Subscribe Response Queue") - public static var shared = SubscribeSessionFactory() - private let sessions = Atomic([:]) - private init() {} - - /// Retrieve a session matching the hash value of the configuration or creates a new one if no match was found - /// - /// The `session` parameter will only be injected into the `SubscriptionSession` in the event - /// that a new `SubscriptionSession` is created - /// - /// - Parameters: - /// - from: A configuration that will be used to fetch an existing SubscriptionSession or create a new one - /// - with: `SessionReplaceable` that will be used as the underlying `Session` - /// - Returns: A `SubscriptionSession` that can be used to make PubNub subscribe and presence API calls with - public func getSession( - from config: SubscriptionConfiguration, - with subscribeSession: SessionReplaceable? = nil, - presenceSession: SessionReplaceable? = nil - ) -> SubscriptionSession { - // The hash value for the given configuration - let configHash = config.subscriptionHashValue - // Returns a session (if any) that matches the hash value - if let session = sessions.lockedRead({ $0[configHash]?.underlying }) { - PubNub.log.debug("Found existing session for config hash \(config.subscriptionHashValue)") - return session - } - - PubNub.log.debug("Creating new session for with hash value \(config.subscriptionHashValue)") - - return sessions.lockedWrite { dictionary in - let subscriptionSession = SubscriptionSession( - strategy: resolveStrategy( - configuration: config, - subscribeSession: subscribeSession, - presenceSession: presenceSession - ) - ) - dictionary.updateValue( - WeakBox(subscriptionSession), - forKey: configHash - ) - return subscriptionSession - } - - func resolveStrategy( - configuration: SubscriptionConfiguration, - subscribeSession: SessionReplaceable?, - presenceSession: SessionReplaceable? - ) -> any SubscriptionSessionStrategy { - // Creates default network session objects if they're not provided - let subscribeSession = subscribeSession ?? HTTPSession( - configuration: URLSessionConfiguration.subscription, - sessionQueue: subscribeQueue, - sessionStream: SessionListener(queue: subscribeQueue) - ) - let presenceSession = presenceSession ?? HTTPSession( - configuration: URLSessionConfiguration.pubnub, - sessionQueue: subscribeQueue, - sessionStream: SessionListener(queue: subscribeQueue) - ) - - if let config = config as? PubNubConfiguration, config.enableEventEngine { - let subscribeEffectFactory = SubscribeEffectFactory( - session: subscribeSession, - presenceStateContainer: .shared - ) - let subscribeEngine = EventEngineFactory().subscribeEngine( - with: config, - dispatcher: EffectDispatcher(factory: subscribeEffectFactory), - transition: SubscribeTransition() - ) - let presenceEffectFactory = PresenceEffectFactory( - session: presenceSession, - presenceStateContainer: .shared - ) - let presenceEngine = EventEngineFactory().presenceEngine( - with: config, - dispatcher: EffectDispatcher(factory: presenceEffectFactory), - transition: PresenceTransition(configuration: config) - ) - return EventEngineSubscriptionSessionStrategy( - configuration: config, - subscribeEngine: subscribeEngine, - presenceEngine: presenceEngine, - presenceStateContainer: .shared - ) - } - - return LegacySubscriptionSessionStrategy( - configuration: configuration, - network: subscribeSession, - presenceSession: presenceSession - ) - } - } - - /// Clean-up method that can be used to poke each weakbox to see if its nil - func sessionDestroyed() { - sessions.lockedWrite { sessionMap in - sessionMap.keys.forEach { if sessionMap[$0]?.underlying == nil { sessionMap.removeValue(forKey: $0) } } - } - } -} - -// MARK: - SubscriptionConfiguration - -/// The configuration used to determine the uniqueness of a `SubscriptionSession` -@available(*, deprecated, message: "Use a PubNub object with PubNubConfiguration that matches the parameters below") -public protocol SubscriptionConfiguration: RouterConfiguration { - /// Reconnection policy which will be used if/when a request fails - var automaticRetry: AutomaticRetry? { get } - /// How long (in seconds) the server will consider the client alive for presence - /// - /// - NOTE: The minimum value this field can be is 20 - var durationUntilTimeout: UInt { get } - /// How often (in seconds) the client will announce itself to server - /// - /// - NOTE: The minimum value this field can be is 0 - var heartbeatInterval: UInt { get } - /// Whether to send out the leave requests - var supressLeaveEvents: Bool { get } - /// The number of messages into the payload before emitting `RequestMessageCountExceeded` - var requestMessageCountThreshold: UInt { get } - /// PSV2 feature to subscribe with a custom filter expression. - var filterExpression: String? { get } - /// If Access Manager (PAM) is enabled, client will use `authToken` instead of `authKey` on all requests - override var authToken: String? { get set } -} - -extension SubscriptionConfiguration { - /// The hash value. - /// - /// Hash values are not guaranteed to be equal across different executions of your program. - /// Do not save hash values to use during a future execution. - var subscriptionHashValue: Int { - var hasher = Hasher() - hasher.combine(durationUntilTimeout.hashValue) - hasher.combine(heartbeatInterval.hashValue) - hasher.combine(supressLeaveEvents.hashValue) - hasher.combine(requestMessageCountThreshold.hashValue) - hasher.combine(filterExpression.hashValue) - hasher.combine(subscribeKey.hashValue) - hasher.combine(uuid.hashValue) - hasher.combine(useSecureConnections.hashValue) - hasher.combine(origin.hashValue) - hasher.combine(authKey.hashValue) - hasher.combine(cryptoModule.hashValue) - return hasher.finalize() - } -} - -extension PubNubConfiguration: SubscriptionConfiguration {} diff --git a/Sources/PubNub/Subscription/SubscriptionSession.swift b/Sources/PubNub/Subscription/SubscriptionSession.swift index cc356fd8..5e910a85 100644 --- a/Sources/PubNub/Subscription/SubscriptionSession.swift +++ b/Sources/PubNub/Subscription/SubscriptionSession.swift @@ -10,44 +10,24 @@ import Foundation -@available(*, deprecated, message: "Subscribe and unsubscribe using methods from a PubNub object") -public class SubscriptionSession: EventEmitter, StatusEmitter { - /// A unique identifier for subscription session - public var uuid: UUID { - strategy.uuid - } - - /// An underlying queue to dispatch events - public let queue: DispatchQueue +class SubscriptionSession: EventEmitter, StatusEmitter { + // An underlying queue to dispatch events + let queue: DispatchQueue + // A unique identifier for subscription session + var uuid: UUID { strategy.uuid } + // The `Timetoken` used for the last successful subscription request + var previousTokenResponse: SubscribeCursor? { strategy.previousTokenResponse } - /// PSV2 feature to subscribe with a custom filter expression. - @available(*, deprecated, message: "Use `subscribeFilterExpression` from a PubNub object") - public var filterExpression: String? { + // PSV2 feature to subscribe with a custom filter expression. + var filterExpression: String? { get { strategy.filterExpression } set { strategy.filterExpression = newValue } } - - /// `EventEmitter` conformance - public var onEvent: ((PubNubEvent) -> Void)? - public var onEvents: (([PubNubEvent]) -> Void)? - public var onMessage: ((PubNubMessage) -> Void)? - public var onSignal: ((PubNubMessage) -> Void)? - public var onPresence: ((PubNubPresenceChange) -> Void)? - public var onMessageAction: ((PubNubMessageActionEvent) -> Void)? - public var onFileEvent: ((PubNubFileChangeEvent) -> Void)? - public var onAppContext: ((PubNubAppContextEvent) -> Void)? - - /// `StatusEmitter` conformance - public var onConnectionStateChange: ((ConnectionStatus) -> Void)? - var previousTokenResponse: SubscribeCursor? { - strategy.previousTokenResponse - } - - var configuration: SubscriptionConfiguration { + var configuration: PubNubConfiguration { get { strategy.configuration } set { @@ -55,6 +35,16 @@ public class SubscriptionSession: EventEmitter, StatusEmitter { } } + var onEvent: ((PubNubEvent) -> Void)? + var onEvents: (([PubNubEvent]) -> Void)? + var onMessage: ((PubNubMessage) -> Void)? + var onSignal: ((PubNubMessage) -> Void)? + var onPresence: ((PubNubPresenceChange) -> Void)? + var onMessageAction: ((PubNubMessageActionEvent) -> Void)? + var onFileEvent: ((PubNubFileChangeEvent) -> Void)? + var onAppContext: ((PubNubAppContextEvent) -> Void)? + var onConnectionStateChange: ((ConnectionStatus) -> Void)? + private lazy var globalEventsListener: BaseSubscriptionListenerAdapter = .init( receiver: self, uuid: uuid, @@ -88,38 +78,31 @@ public class SubscriptionSession: EventEmitter, StatusEmitter { add(globalStatusListener) } - /// Names of all subscribed channels - /// - /// This list includes both regular and presence channel names - public var subscribedChannels: [String] { + // Names of all subscribed channels + // + // This list includes both regular and presence channel names + var subscribedChannels: [String] { strategy.subscribedChannels } - /// List of actively subscribed groups - public var subscribedChannelGroups: [String] { + // List of actively subscribed groups + var subscribedChannelGroups: [String] { strategy.subscribedChannelGroups } - /// Combined value of all subscribed channels and groups - public var subscriptionCount: Int { + // Combined value of all subscribed channels and groups + var subscriptionCount: Int { strategy.subscriptionCount } - /// Current connection status - public var connectionStatus: ConnectionStatus { + // Current connection status + var connectionStatus: ConnectionStatus { strategy.connectionStatus } // MARK: - Subscription Loop - /// Subscribe to channels and/or channel groups - /// - /// - Parameters: - /// - to: List of channels to subscribe on - /// - and: List of channel groups to subscribe on - /// - at: The timetoken to subscribe with - /// - withPresence: If true it also subscribes to presence events on the specified channels. - public func subscribe( + func subscribe( to channels: [String], and groups: [String] = [], at cursor: SubscribeCursor? = nil, @@ -143,37 +126,32 @@ public class SubscriptionSession: EventEmitter, StatusEmitter { at: cursor?.timetoken ) for subscription in channelSubscriptions { - subscription.subscriptionNames.flatMap { $0 }.forEach { + subscription.subscriptionNames.compactMap { $0 }.forEach { globalChannelSubscriptions[$0] = subscription } } for subscription in channelGroupSubscriptions { - subscription.subscriptionNames.flatMap { $0 }.forEach { + subscription.subscriptionNames.compactMap { $0 }.forEach { globalGroupSubscriptions[$0] = subscription } } } - /// Reconnect a disconnected subscription stream - /// - parameter timetoken: The timetoken to subscribe with - public func reconnect(at cursor: SubscribeCursor? = nil) { + // MARK: - Reconnect + + func reconnect(at cursor: SubscribeCursor? = nil) { strategy.reconnect(at: cursor) } - /// Disconnect the subscription stream - public func disconnect() { + // MARK: - Disconnect + + func disconnect() { strategy.disconnect() } // MARK: - Unsubscribe - /// Unsubscribe from channels and/or channel groups - /// - /// - Parameters: - /// - from: List of channels to unsubscribe from - /// - and: List of channel groups to unsubscribe from - /// - presenceOnly: If true, it only unsubscribes from presence events on the specified channels. - public func unsubscribe( + func unsubscribe( from channels: [String], and groups: [String] = [], presenceOnly: Bool = false @@ -195,8 +173,7 @@ public class SubscriptionSession: EventEmitter, StatusEmitter { } } - /// Unsubscribe from all channels and channel groups - public func unsubscribeAll() { + func unsubscribeAll() { strategy.unsubscribeAll() } } @@ -429,15 +406,15 @@ extension SubscriptionSession: EventStreamEmitter { // MARK: - Hashable & CustomStringConvertible extension SubscriptionSession: Hashable, CustomStringConvertible { - public static func == (lhs: SubscriptionSession, rhs: SubscriptionSession) -> Bool { + static func == (lhs: SubscriptionSession, rhs: SubscriptionSession) -> Bool { lhs.uuid == rhs.uuid } - public func hash(into hasher: inout Hasher) { + func hash(into hasher: inout Hasher) { hasher.combine(uuid) } - public var description: String { + var description: String { uuid.uuidString } } diff --git a/Tests/PubNubContractTest/Steps/EventEngine/PubNubEventEngineTestsHelpers.swift b/Tests/PubNubContractTest/Steps/EventEngine/PubNubEventEngineTestsHelpers.swift index 04259833..321266aa 100644 --- a/Tests/PubNubContractTest/Steps/EventEngine/PubNubEventEngineTestsHelpers.swift +++ b/Tests/PubNubContractTest/Steps/EventEngine/PubNubEventEngineTestsHelpers.swift @@ -16,7 +16,10 @@ protocol ContractTestIdentifiable { var contractTestIdentifier: String { get } } -extension EffectInvocation: ContractTestIdentifiable where Invocation: ContractTestIdentifiable, Invocation.Cancellable: ContractTestIdentifiable { +extension EffectInvocation: ContractTestIdentifiable where + Invocation: ContractTestIdentifiable, + Invocation.Cancellable: ContractTestIdentifiable +{ var contractTestIdentifier: String { switch self { case .managed(let invocation): @@ -37,7 +40,7 @@ class DispatcherDecorator: Dispat self.wrappedInstance = wrappedInstance self.recordedInvocations = [] } - + func dispatch( invocations: [EffectInvocation], with dependencies: EventEngineDependencies, @@ -51,16 +54,16 @@ class DispatcherDecorator: Dispat class TransitionDecorator: TransitionProtocol { private let wrappedInstance: any TransitionProtocol private(set) var recordedEvents: [Event] - + init(wrappedInstance: some TransitionProtocol) { self.wrappedInstance = wrappedInstance self.recordedEvents = [] } - + func canTransition(from state: State, dueTo event: Event) -> Bool { wrappedInstance.canTransition(from: state, dueTo: event) } - + func transition(from state: State, event: Event) -> TransitionResult { recordedEvents.append(event) return wrappedInstance.transition(from: state, event: event) diff --git a/Tests/PubNubContractTest/Steps/EventEngine/PubNubPresenceEngineContractTestSteps.swift b/Tests/PubNubContractTest/Steps/EventEngine/PubNubPresenceEngineContractTestSteps.swift index c876c444..5bc4c671 100644 --- a/Tests/PubNubContractTest/Steps/EventEngine/PubNubPresenceEngineContractTestSteps.swift +++ b/Tests/PubNubContractTest/Steps/EventEngine/PubNubPresenceEngineContractTestSteps.swift @@ -8,19 +8,19 @@ // LICENSE file in the root directory of this source tree. // -import Foundation import Cucumberish +import Foundation @testable import PubNub extension Presence.Invocation: ContractTestIdentifiable { var contractTestIdentifier: String { switch self { - case .heartbeat(_, _): + case .heartbeat: return "HEARTBEAT" - case .leave(_, _): + case .leave: return "LEAVE" - case .delayedHeartbeat(_, _, _, _): + case .delayedHeartbeat: return "DELAYED_HEARTBEAT" case .wait: return "WAIT" @@ -42,9 +42,9 @@ extension Presence.Invocation.Cancellable: ContractTestIdentifiable { extension Presence.Event: ContractTestIdentifiable { var contractTestIdentifier: String { switch self { - case .joined(_, _): + case .joined: return "JOINED" - case .left(_, _): + case .left: return "LEFT" case .leftAll: return "LEFT_ALL" @@ -56,9 +56,9 @@ extension Presence.Event: ContractTestIdentifiable { return "TIMES_UP" case .heartbeatSuccess: return "HEARTBEAT_SUCCESS" - case .heartbeatFailed(_): + case .heartbeatFailed: return "HEARTBEAT_FAILURE" - case .heartbeatGiveUp(_): + case .heartbeatGiveUp: return "HEARTBEAT_GIVEUP" } } @@ -66,9 +66,17 @@ extension Presence.Event: ContractTestIdentifiable { class PubNubPresenceEngineContractTestsSteps: PubNubEventEngineContractTestsSteps { // A decorator that records Invocations and forwards all calls to the original instance - private var dispatcherDecorator: DispatcherDecorator! + private var dispatcherDecorator: DispatcherDecorator< + Presence.Invocation, + Presence.Event, + Presence.Dependencies + >! // A decorator that records Events and forwards all calls to the original instance - private var transitionDecorator: TransitionDecorator! + private var transitionDecorator: TransitionDecorator< + any PresenceState, + Presence.Event, + Presence.Invocation + >! override func handleAfterHook() { dispatcherDecorator = nil @@ -77,63 +85,36 @@ class PubNubPresenceEngineContractTestsSteps: PubNubEventEngineContractTestsStep } override func createPubNubClient() -> PubNub { - let configuration = self.configuration - let factory = EventEngineFactory() - - /// Wraps original EffectDispatcher with Decorator that allows recording incoming Invocations - dispatcherDecorator = DispatcherDecorator( - wrappedInstance: EffectDispatcher( - factory: PresenceEffectFactory( - session: HTTPSession( - configuration: .pubnub, - sessionQueue: .global(qos: .default), - sessionStream: SessionListener(queue: .global(qos: .default)) - ), presenceStateContainer: .shared - ) + let container = DependencyContainer(configuration: self.configuration) + let key = PresenceEventEngineDependencyKey.self + + self.dispatcherDecorator = DispatcherDecorator(wrappedInstance: EffectDispatcher( + factory: PresenceEffectFactory( + session: container[HTTPPresenceSessionDependencyKey.self], + presenceStateContainer: container[PresenceStateContainerDependencyKey.self] ) - ) - /// Wraps original Transition with Decorator that allows recording incoming Events - transitionDecorator = TransitionDecorator( + )) + self.transitionDecorator = TransitionDecorator( wrappedInstance: PresenceTransition(configuration: configuration) ) - - let subscribeEffectFactory = SubscribeEffectFactory( - session: HTTPSession( - configuration: URLSessionConfiguration.subscription, - sessionQueue: .global(qos: .default), - sessionStream: SessionListener(queue: .global(qos: .default)) - ), presenceStateContainer: .shared - ) - let subscribeEngine = EventEngineFactory().subscribeEngine( - with: configuration, - dispatcher: EffectDispatcher(factory: subscribeEffectFactory), - transition: SubscribeTransition() - ) - let presenceEngine = factory.presenceEngine( - with: configuration, - dispatcher: dispatcherDecorator, - transition: transitionDecorator - ) - let subscriptionSession = SubscriptionSession( - strategy: EventEngineSubscriptionSessionStrategy( - configuration: configuration, - subscribeEngine: subscribeEngine, - presenceEngine: presenceEngine, - presenceStateContainer: .shared - ) - ) - return PubNub( - configuration: configuration, - session: HTTPSession(configuration: configuration.urlSessionConfiguration), - fileSession: URLSession(configuration: .pubnubBackground), - subscriptionSession: subscriptionSession + + container.register( + value: PresenceEngine( + state: Presence.HeartbeatInactive(), + transition: self.transitionDecorator, + dispatcher: self.dispatcherDecorator, + dependencies: EventEngineDependencies(value: Presence.Dependencies(configuration: configuration)) + ), + forKey: PresenceEventEngineDependencyKey.self ) + + return PubNub(container: container) } override public func setup() { startCucumberHookEventsListening() - Given("^the demo keyset with Presence Event Engine enabled$") { args, _ in + Given("^the demo keyset with Presence Event Engine enabled$") { _, _ in self.replacePubNubConfiguration(with: PubNubConfiguration( publishKey: self.configuration.publishKey, subscribeKey: self.configuration.subscribeKey, @@ -144,7 +125,7 @@ class PubNubPresenceEngineContractTestsSteps: PubNubEventEngineContractTestsStep )) } - Given("a linear reconnection policy with 3 retries") { args, _ in + Given("a linear reconnection policy with 3 retries") { _, _ in self.replacePubNubConfiguration(with: PubNubConfiguration( publishKey: self.configuration.publishKey, subscribeKey: self.configuration.subscribeKey, @@ -188,7 +169,7 @@ class PubNubPresenceEngineContractTestsSteps: PubNubEventEngineContractTestsStep self.subscribeSynchronously(self.client, to: [firstChannel, secondChannel, thirdChannel], with: true) } - Then("^I wait for getting Presence joined events$") { args, _ in + Then("^I wait for getting Presence joined events$") { _, _ in XCTAssertNotNil(self.waitForPresenceChanges(self.client, count: 3)) } @@ -196,7 +177,7 @@ class PubNubPresenceEngineContractTestsSteps: PubNubEventEngineContractTestsStep self.waitFor(delay: TimeInterval(args!.first!)!) } - Then("^I wait for getting Presence left events$") { args, _ in + Then("^I wait for getting Presence left events$") { _, _ in XCTAssertNotNil(self.waitForPresenceChanges(self.client, count: 2)) } @@ -211,7 +192,7 @@ class PubNubPresenceEngineContractTestsSteps: PubNubEventEngineContractTestsStep self.waitFor(delay: 9.5) } - Match(["And", "Then"], "^I observe the following Events and Invocations of the Presence EE:$") { args, value in + Match(["And", "Then"], "^I observe the following Events and Invocations of the Presence EE:$") { _, value in let recordedEvents = self.transitionDecorator.recordedEvents.map { $0.contractTestIdentifier } let recordedInvocations = self.dispatcherDecorator.recordedInvocations.map { $0.contractTestIdentifier } @@ -219,7 +200,7 @@ class PubNubPresenceEngineContractTestsSteps: PubNubEventEngineContractTestsStep XCTAssertTrue(recordedInvocations.elementsEqual(self.extractExpectedResults(from: value).invocations)) } - Then("^I don't observe any Events and Invocations of the Presence EE") { args, value in + Then("^I don't observe any Events and Invocations of the Presence EE") { _, _ in XCTAssertTrue(self.transitionDecorator.recordedEvents.isEmpty) XCTAssertTrue(self.dispatcherDecorator.recordedInvocations.isEmpty) } diff --git a/Tests/PubNubContractTest/Steps/EventEngine/PubNubSubscribeEngineContractTestsSteps.swift b/Tests/PubNubContractTest/Steps/EventEngine/PubNubSubscribeEngineContractTestsSteps.swift index 64263bde..75dfb278 100644 --- a/Tests/PubNubContractTest/Steps/EventEngine/PubNubSubscribeEngineContractTestsSteps.swift +++ b/Tests/PubNubContractTest/Steps/EventEngine/PubNubSubscribeEngineContractTestsSteps.swift @@ -8,25 +8,25 @@ // LICENSE file in the root directory of this source tree. // -import Foundation import Cucumberish +import Foundation @testable import PubNub extension Subscribe.Invocation: ContractTestIdentifiable { var contractTestIdentifier: String { switch self { - case .handshakeRequest(_, _): + case .handshakeRequest: return "HANDSHAKE" - case .handshakeReconnect(_, _, _, _): + case .handshakeReconnect: return "HANDSHAKE_RECONNECT" - case .receiveMessages(_, _, _): + case .receiveMessages: return "RECEIVE_MESSAGES" - case .receiveReconnect(_, _, _, _, _): + case .receiveReconnect: return "RECEIVE_RECONNECT" - case .emitMessages(_,_): + case .emitMessages: return "EMIT_MESSAGES" - case .emitStatus(_): + case .emitStatus: return "EMIT_STATUS" } } @@ -50,29 +50,29 @@ extension Subscribe.Invocation.Cancellable: ContractTestIdentifiable { extension Subscribe.Event: ContractTestIdentifiable { var contractTestIdentifier: String { switch self { - case .handshakeSuccess(_): + case .handshakeSuccess: return "HANDSHAKE_SUCCESS" - case .handshakeFailure(_): + case .handshakeFailure: return "HANDSHAKE_FAILURE" - case .handshakeReconnectSuccess(_): + case .handshakeReconnectSuccess: return "HANDSHAKE_RECONNECT_SUCCESS" - case .handshakeReconnectFailure(_): + case .handshakeReconnectFailure: return "HANDSHAKE_RECONNECT_FAILURE" - case .handshakeReconnectGiveUp(_): + case .handshakeReconnectGiveUp: return "HANDSHAKE_RECONNECT_GIVEUP" - case .receiveSuccess(_,_): + case .receiveSuccess: return "RECEIVE_SUCCESS" - case .receiveFailure(_): + case .receiveFailure: return "RECEIVE_FAILURE" - case .receiveReconnectSuccess(_,_): + case .receiveReconnectSuccess: return "RECEIVE_RECONNECT_SUCCESS" - case .receiveReconnectFailure(_): + case .receiveReconnectFailure: return "RECEIVE_RECONNECT_FAILURE" - case .receiveReconnectGiveUp(_): + case .receiveReconnectGiveUp: return "RECEIVE_RECONNECT_GIVEUP" - case .subscriptionChanged(_, _): + case .subscriptionChanged: return "SUBSCRIPTION_CHANGED" - case .subscriptionRestored(_, _, _): + case .subscriptionRestored: return "SUBSCRIPTION_RESTORED" case .unsubscribeAll: return "UNSUBSCRIBE_ALL" @@ -86,9 +86,17 @@ extension Subscribe.Event: ContractTestIdentifiable { class PubNubSubscribeEngineContractTestsSteps: PubNubEventEngineContractTestsSteps { // A decorator that records Invocations and forwards all calls to the original instance - private var dispatcherDecorator: DispatcherDecorator! + private var dispatcherDecorator: DispatcherDecorator< + Subscribe.Invocation, + Subscribe.Event, + Subscribe.Dependencies + >! // A decorator that records Events and forwards all calls to the original instance - private var transitionDecorator: TransitionDecorator! + private var transitionDecorator: TransitionDecorator< + any SubscribeState, + Subscribe.Event, + Subscribe.Invocation + >! override func handleAfterHook() { dispatcherDecorator = nil @@ -106,63 +114,36 @@ class PubNubSubscribeEngineContractTestsSteps: PubNubEventEngineContractTestsSte } override func createPubNubClient() -> PubNub { - /// Wraps original EffectDispatcher with Decorator that allows recording incoming Invocations - dispatcherDecorator = DispatcherDecorator( - wrappedInstance: EffectDispatcher( - factory: SubscribeEffectFactory( - session: HTTPSession( - configuration: URLSessionConfiguration.subscription, - sessionQueue: .global(qos: .default), - sessionStream: SessionListener(queue: .global(qos: .default)) - ), presenceStateContainer: .shared - ) + let container = DependencyContainer(configuration: self.configuration) + let key = SubscribeEventEngineDependencyKey.self + + self.dispatcherDecorator = DispatcherDecorator(wrappedInstance: EffectDispatcher( + factory: SubscribeEffectFactory( + session: container[HTTPSubscribeSessionDependencyKey.self], + presenceStateContainer: container[PresenceStateContainerDependencyKey.self] ) - ) - /// Wraps original Transition with Decorator that allows recording incoming Events - transitionDecorator = TransitionDecorator( + )) + self.transitionDecorator = TransitionDecorator( wrappedInstance: SubscribeTransition() ) - let factory = EventEngineFactory() - let configuration = self.configuration - - let subscribeEngine = factory.subscribeEngine( - with: configuration, - dispatcher: self.dispatcherDecorator, - transition: self.transitionDecorator - ) - let presenceEffectFactory = PresenceEffectFactory( - session: HTTPSession( - configuration: .pubnub, - sessionQueue: .global(qos: .default), - sessionStream: SessionListener(queue: .global(qos: .default)) - ), presenceStateContainer: .shared - ) - let presenceEngine = factory.presenceEngine( - with: configuration, - dispatcher: EffectDispatcher(factory: presenceEffectFactory), - transition: PresenceTransition(configuration: configuration) - ) - let subscriptionSession = SubscriptionSession( - strategy: EventEngineSubscriptionSessionStrategy( - configuration: configuration, - subscribeEngine: subscribeEngine, - presenceEngine: presenceEngine, - presenceStateContainer: .shared - ) - ) - return PubNub( - configuration: configuration, - session: HTTPSession(configuration: configuration.urlSessionConfiguration), - fileSession: URLSession(configuration: .pubnubBackground), - subscriptionSession: subscriptionSession + container.register( + value: SubscribeEngine( + state: Subscribe.UnsubscribedState(), + transition: self.transitionDecorator, + dispatcher: self.dispatcherDecorator, + dependencies: EventEngineDependencies(value: Subscribe.Dependencies(configuration: configuration)) + ), + forKey: SubscribeEventEngineDependencyKey.self ) + + return PubNub(container: container) } override public func setup() { startCucumberHookEventsListening() - Given("a linear reconnection policy with 3 retries") { args, _ in + Given("a linear reconnection policy with 3 retries") { _, _ in self.replacePubNubConfiguration(with: PubNubConfiguration( publishKey: self.configuration.publishKey, subscribeKey: self.configuration.subscribeKey, @@ -201,12 +182,12 @@ class PubNubSubscribeEngineContractTestsSteps: PubNubEventEngineContractTestsSte XCTAssertNotNil(self.receivedErrorStatuses.first) } - Then("I receive the message in my subscribe response") { _, userInfo in + Then("I receive the message in my subscribe response") { _, _ in let messages = self.waitForMessages(self.client, count: 1) ?? [] XCTAssertNotNil(messages.first) } - Match(["And"], "I observe the following:") { args, value in + Match(["And"], "I observe the following:") { _, value in let recordedEvents = self.transitionDecorator.recordedEvents.map { $0.contractTestIdentifier } let recordedInvocations = self.dispatcherDecorator.recordedInvocations.map { $0.contractTestIdentifier } diff --git a/Tests/PubNubTests/EventEngine/Presence/DelayedHeartbeatEffectTests.swift b/Tests/PubNubTests/EventEngine/Presence/DelayedHeartbeatEffectTests.swift index d25fedeb..6ddabf03 100644 --- a/Tests/PubNubTests/EventEngine/Presence/DelayedHeartbeatEffectTests.swift +++ b/Tests/PubNubTests/EventEngine/Presence/DelayedHeartbeatEffectTests.swift @@ -28,9 +28,10 @@ class DelayedHeartbeatEffectTests: XCTestCase { } override func tearDown() { - mockUrlSession = nil delegate = nil + mockUrlSession = nil httpSession = nil + factory = nil super.tearDown() } @@ -43,7 +44,7 @@ class DelayedHeartbeatEffectTests: XCTestCase { let delayRange = 2.0...3.0 let automaticRetry = AutomaticRetry(retryLimit: 3, policy: .linear(delay: delayRange.lowerBound), excluded: []) - let effect = configureEffect(attempt: 0, automaticRetry: automaticRetry, error: PubNubError(.unknown)) + let effect = configureEffectToTest(retryAttempt: 0, automaticRetry: automaticRetry, dueTo: PubNubError(.unknown)) let startDate = Date() effect.performTask { returnedEvents in @@ -65,7 +66,7 @@ class DelayedHeartbeatEffectTests: XCTestCase { let delayRange = 2.0...3.0 let automaticRetry = AutomaticRetry(retryLimit: 3, policy: .linear(delay: delayRange.lowerBound), excluded: []) let error = PubNubError(.unknown) - let effect = configureEffect(attempt: 0, automaticRetry: automaticRetry, error: error) + let effect = configureEffectToTest(retryAttempt: 0, automaticRetry: automaticRetry, dueTo: error) effect.performTask { returnedEvents in let expectedError = PubNubError(.internalServiceError) @@ -84,7 +85,7 @@ class DelayedHeartbeatEffectTests: XCTestCase { let automaticRetry = AutomaticRetry(retryLimit: 3, policy: .linear(delay: 2.0), excluded: []) let error = PubNubError(.unknown) - let effect = configureEffect(attempt: 3, automaticRetry: automaticRetry, error: error) + let effect = configureEffectToTest(retryAttempt: 3, automaticRetry: automaticRetry, dueTo: error) mockResponse(GenericServicePayloadResponse(status: 200)) @@ -98,9 +99,9 @@ class DelayedHeartbeatEffectTests: XCTestCase { } } -fileprivate extension DelayedHeartbeatEffectTests { +private extension DelayedHeartbeatEffectTests { func mockResponse(_ response: GenericServicePayloadResponse) { - mockUrlSession.responseForDataTask = { task, id in + mockUrlSession.responseForDataTask = { task, _ in task.mockError = nil task.mockData = try? Constant.jsonEncoder.encode(response) task.mockResponse = HTTPURLResponse(statusCode: response.status) @@ -108,9 +109,10 @@ fileprivate extension DelayedHeartbeatEffectTests { } } - func configureEffect( - attempt: Int, automaticRetry: AutomaticRetry?, - error: PubNubError + func configureEffectToTest( + retryAttempt attempt: Int, + automaticRetry: AutomaticRetry?, + dueTo error: PubNubError ) -> any EffectHandler { factory.effect( for: .delayedHeartbeat( diff --git a/Tests/PubNubTests/EventEngine/Subscribe/SubscribeEffectsTests.swift b/Tests/PubNubTests/EventEngine/Subscribe/SubscribeEffectsTests.swift index b66624a2..461cf21a 100644 --- a/Tests/PubNubTests/EventEngine/Subscribe/SubscribeEffectsTests.swift +++ b/Tests/PubNubTests/EventEngine/Subscribe/SubscribeEffectsTests.swift @@ -23,10 +23,7 @@ class SubscribeEffectsTests: XCTestCase { publishKey: "pubKey", subscribeKey: "subKey", userId: "userId", - automaticRetry: AutomaticRetry( - retryLimit: 3, - policy: .linear(delay: 2.0) - ) + automaticRetry: AutomaticRetry(retryLimit: 3, policy: .linear(delay: 2.0)) ) private func configWithLinearPolicy(_ delay: Double = 2.0) -> PubNubConfiguration { @@ -47,9 +44,10 @@ class SubscribeEffectsTests: XCTestCase { } override func tearDown() { - mockUrlSession = nil delegate = nil + mockUrlSession = nil httpSession = nil + factory = nil super.tearDown() } } @@ -62,21 +60,28 @@ extension SubscribeEffectsTests { cursor: SubscribeCursor(timetoken: 12345, region: 1), messages: [] )) - runEffect( - configuration: config, - invocation: .handshakeRequest( - channels: ["channel1", "channel1-pnpres", "channel2"], - groups: ["g1", "g2", "g2-pnpres"] - ), - expectedOutput: [ - .handshakeSuccess( - cursor: SubscribeCursor( - timetoken: 12345, - region: 1 - ) - ) - ] + + let expectation = XCTestExpectation(description: "Effect Completion") + expectation.expectedFulfillmentCount = 1 + expectation.assertForOverFulfill = true + + let testedInvocation: Subscribe.Invocation = .handshakeRequest( + channels: ["channel1", "channel1-pnpres", "channel2"], + groups: ["g1", "g2", "g2-pnpres"] + ) + let effect = factory.effect( + for: testedInvocation, + with: EventEngineDependencies(value: Subscribe.Dependencies(configuration: config)) + ) + let expectedOutput: Subscribe.Event = .handshakeSuccess( + cursor: SubscribeCursor(timetoken: 12345, region: 1) ) + + effect.performTask { + XCTAssertEqual([expectedOutput], $0) + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) } func test_HandshakingEffectWithFailedResponse() { @@ -84,17 +89,28 @@ extension SubscribeEffectsTests { errorIfAny: URLError(.cannotFindHost), httpResponse: HTTPURLResponse(statusCode: 404)! ) - runEffect( - configuration: config, - invocation: .handshakeRequest( - channels: ["channel1", "channel1-pnpres", "channel2"], - groups: ["g1", "g2", "g2-pnpres"] - ), expectedOutput: [ - .handshakeFailure( - error: PubNubError(.nameResolutionFailure, underlying: URLError(.cannotFindHost)) - ) - ] + + let expectation = XCTestExpectation(description: "Effect Completion") + expectation.expectedFulfillmentCount = 1 + expectation.assertForOverFulfill = true + + let testedInvocation: Subscribe.Invocation = .handshakeRequest( + channels: ["channel1", "channel1-pnpres", "channel2"], + groups: ["g1", "g2", "g2-pnpres"] + ) + let expectedOutput: Subscribe.Event = .handshakeFailure( + error: PubNubError(.nameResolutionFailure, underlying: URLError(.cannotFindHost)) + ) + let effect = factory.effect( + for: testedInvocation, + with: EventEngineDependencies(value: Subscribe.Dependencies(configuration: config)) ) + + effect.performTask { + XCTAssertEqual([expectedOutput], $0) + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) } } @@ -106,19 +122,30 @@ extension SubscribeEffectsTests { cursor: SubscribeCursor(timetoken: 12345, region: 1), messages: [firstMessage, secondMessage] )) - runEffect( - configuration: config, - invocation: .receiveMessages( - channels: ["channel1", "channel1-pnpres", "channel2"], - groups: ["g1", "g2", "g2-pnpres"], - cursor: SubscribeCursor(timetoken: 111, region: 1) - ), expectedOutput: [ - .receiveSuccess( - cursor: SubscribeCursor(timetoken: 12345, region: 1), - messages: [firstMessage, secondMessage] - ) - ] + + let expectation = XCTestExpectation(description: "Effect Completion") + expectation.expectedFulfillmentCount = 1 + expectation.assertForOverFulfill = true + + let testedInvocation: Subscribe.Invocation = .receiveMessages( + channels: ["channel1", "channel1-pnpres", "channel2"], + groups: ["g1", "g2", "g2-pnpres"], + cursor: SubscribeCursor(timetoken: 111, region: 1) + ) + let expectedOutput: Subscribe.Event = .receiveSuccess( + cursor: SubscribeCursor(timetoken: 12345, region: 1), + messages: [firstMessage, secondMessage] ) + let effect = factory.effect( + for: testedInvocation, + with: EventEngineDependencies(value: Subscribe.Dependencies(configuration: config)) + ) + + effect.performTask { + XCTAssertEqual([expectedOutput], $0) + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) } func test_ReceivingEffectWithFailedResponse() { @@ -126,17 +153,29 @@ extension SubscribeEffectsTests { errorIfAny: URLError(.cannotFindHost), httpResponse: HTTPURLResponse(statusCode: 404)! ) - runEffect( - configuration: config, - invocation: .receiveMessages( - channels: ["channel1", "channel1-pnpres", "channel2"], - groups: ["g1", "g2", "g2-pnpres"], - cursor: SubscribeCursor(timetoken: 111, region: 1) - ), expectedOutput: [ - .receiveFailure( - error: PubNubError(.nameResolutionFailure, underlying: URLError(.cannotFindHost)) - ) - ]) + + let expectation = XCTestExpectation(description: "Effect Completion") + expectation.expectedFulfillmentCount = 1 + expectation.assertForOverFulfill = true + + let testedInvocation: Subscribe.Invocation = .receiveMessages( + channels: ["channel1", "channel1-pnpres", "channel2"], + groups: ["g1", "g2", "g2-pnpres"], + cursor: SubscribeCursor(timetoken: 111, region: 1) + ) + let expectedOutput: Subscribe.Event = .receiveFailure( + error: PubNubError(.nameResolutionFailure, underlying: URLError(.cannotFindHost)) + ) + let effect = factory.effect( + for: testedInvocation, + with: EventEngineDependencies(value: Subscribe.Dependencies(configuration: config)) + ) + + effect.performTask { + XCTAssertEqual([expectedOutput], $0) + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) } } @@ -144,115 +183,141 @@ extension SubscribeEffectsTests { extension SubscribeEffectsTests { func test_HandshakeReconnectingSuccess() { - let delayRange = 2.0...3.0 - let urlError = URLError(.badServerResponse) - let httpUrlResponse = HTTPURLResponse(statusCode: 500)! - let pubNubError = PubNubError(urlError.pubnubReason!, underlying: urlError, affected: [.response(httpUrlResponse)]) - mockResponse(subscribeResponse: SubscribeResponse( cursor: SubscribeCursor(timetoken: 12345, region: 1), messages: [] )) - runEffect( - configuration: configWithLinearPolicy(delayRange.lowerBound), - invocation: .handshakeReconnect( - channels: ["channel1", "channel1-pnpres", "channel2"], - groups: ["g1", "g2", "g2-pnpres"], - retryAttempt: 1, - reason: pubNubError - ), - timeout: 2 * delayRange.upperBound, - expectedOutput: [ - .handshakeReconnectSuccess(cursor: SubscribeCursor( - timetoken: 12345, - region: 1 - )) - ] + + let delayRange = 2.0...3.0 + let expectation = XCTestExpectation(description: "Effect Completion") + expectation.expectedFulfillmentCount = 1 + expectation.assertForOverFulfill = true + + let testedInvocation: Subscribe.Invocation = .handshakeReconnect( + channels: ["channel1", "channel1-pnpres", "channel2"], + groups: ["g1", "g2", "g2-pnpres"], + retryAttempt: 1, + reason: PubNubError( + URLError(.badServerResponse).pubnubReason!, + underlying: URLError(.badServerResponse), + affected: [.response(HTTPURLResponse(statusCode: 500)!)] + ) ) + let expectedOutput: Subscribe.Event = .handshakeReconnectSuccess( + cursor: SubscribeCursor(timetoken: 12345, region: 1) + ) + let effect = factory.effect( + for: testedInvocation, + with: EventEngineDependencies(value: Subscribe.Dependencies( + configuration: configWithLinearPolicy(delayRange.lowerBound) + )) + ) + + effect.performTask { + XCTAssertEqual([expectedOutput], $0) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2 * delayRange.upperBound) } func test_HandshakeReconnectingFailed() { - let delayRange = 2.0...3.0 - let urlError = URLError(.badServerResponse) - let httpUrlResponse = HTTPURLResponse(statusCode: 500)! - let pubNubError = PubNubError(urlError.pubnubReason!, underlying: urlError, affected: [.response(httpUrlResponse)]) - mockResponse( errorIfAny: URLError(.cannotFindHost), httpResponse: HTTPURLResponse(statusCode: 404)! ) - runEffect( - configuration: configWithLinearPolicy(delayRange.lowerBound), - invocation: .handshakeReconnect( - channels: ["channel1", "channel1-pnpres", "channel2"], - groups: ["g1", "g2", "g2-pnpres"], - retryAttempt: 1, - reason: pubNubError - ), - timeout: 2 * delayRange.upperBound, - expectedOutput: [ - .handshakeReconnectFailure( - error: PubNubError( - .nameResolutionFailure, - underlying: URLError(.cannotFindHost) - ) - ) - ] + + let delayRange = 2.0...3.0 + let expectation = XCTestExpectation(description: "Effect Completion") + expectation.expectedFulfillmentCount = 1 + expectation.assertForOverFulfill = true + + let testedInvocation: Subscribe.Invocation = .handshakeReconnect( + channels: ["channel1", "channel1-pnpres", "channel2"], + groups: ["g1", "g2", "g2-pnpres"], + retryAttempt: 1, + reason: PubNubError( + URLError(.badServerResponse).pubnubReason!, + underlying: URLError(.badServerResponse), + affected: [.response(HTTPURLResponse(statusCode: 500)!)] + ) + ) + let expectedOutput: Subscribe.Event = .handshakeReconnectFailure( + error: PubNubError(.nameResolutionFailure, underlying: URLError(.cannotFindHost)) + ) + let effect = factory.effect( + for: testedInvocation, + with: EventEngineDependencies(value: Subscribe.Dependencies( + configuration: configWithLinearPolicy(delayRange.lowerBound) + )) ) + + effect.performTask { + XCTAssertEqual([expectedOutput], $0) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2 * delayRange.upperBound) } func test_HandshakeReconnectGiveUp() { let delayRange = 2.0...3.0 - let urlError = URLError(.badServerResponse) - - runEffect( - configuration: configWithLinearPolicy(delayRange.lowerBound), - invocation: .handshakeReconnect( - channels: ["channel1", "channel1-pnpres", "channel2"], - groups: ["g1", "g2", "g2-pnpres"], - retryAttempt: 3, - reason: PubNubError(urlError.pubnubReason!, underlying: urlError) - ), - expectedOutput: [ - .handshakeReconnectGiveUp( - error: PubNubError(.badServerResponse) - ) - ] + let expectation = XCTestExpectation(description: "Effect Completion") + expectation.expectedFulfillmentCount = 1 + expectation.assertForOverFulfill = true + + let testedInvocation: Subscribe.Invocation = .handshakeReconnect( + channels: ["channel1", "channel1-pnpres", "channel2"], + groups: ["g1", "g2", "g2-pnpres"], + retryAttempt: 3, + reason: PubNubError(URLError(.badServerResponse).pubnubReason!, underlying: URLError(.badServerResponse)) + ) + let expectedOutput: Subscribe.Event = .handshakeReconnectGiveUp( + error: PubNubError(.badServerResponse) + ) + let effect = factory.effect( + for: testedInvocation, + with: EventEngineDependencies(value: Subscribe.Dependencies( + configuration: configWithLinearPolicy(delayRange.lowerBound) + )) ) + + effect.performTask { + XCTAssertEqual([expectedOutput], $0) + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) } func test_HandshakeReconnectIsDelayed() { - let urlError = URLError(.badServerResponse) - let httpUrlResponse = HTTPURLResponse(statusCode: 500)! - let pubNubError = PubNubError(urlError.pubnubReason!, underlying: urlError, affected: [.response(httpUrlResponse)]) - - let delayRange = 2.0...3.0 - let startDate = Date() - mockResponse(subscribeResponse: SubscribeResponse( cursor: SubscribeCursor(timetoken: 12345, region: 1), messages: [] )) - runEffect( - configuration: configWithLinearPolicy(delayRange.lowerBound), - invocation: .handshakeReconnect( - channels: ["channel1", "channel1-pnpres", "channel2"], - groups: ["g1", "g2", "g2-pnpres"], - retryAttempt: 1, - reason: pubNubError - ), - timeout: 2.5 * delayRange.upperBound, - expectedOutput: [ - .handshakeReconnectSuccess( - cursor: SubscribeCursor(timetoken: 12345, region: 1) - ) - ], - additionalValidations: { - XCTAssertTrue( - Int(Date().timeIntervalSince(startDate)) <= Int(delayRange.upperBound) - ) - } + + let expectation = XCTestExpectation(description: "Effect Completion") + expectation.expectedFulfillmentCount = 1 + expectation.assertForOverFulfill = true + + let testedInvocation: Subscribe.Invocation = .handshakeReconnect( + channels: ["channel1", "channel1-pnpres", "channel2"], + groups: ["g1", "g2", "g2-pnpres"], + retryAttempt: 3, + reason: PubNubError( + URLError(.badServerResponse).pubnubReason!, + underlying: URLError(.badServerResponse), + affected: [.response(HTTPURLResponse(statusCode: 500)!)] + ) ) + + let delayRange = 2.0...3.0 + let startDate = Date() + let depValue = Subscribe.Dependencies(configuration: configWithLinearPolicy(delayRange.lowerBound)) + let effect = factory.effect(for: testedInvocation, with: EventEngineDependencies(value: depValue)) + + effect.performTask { _ in + XCTAssertTrue(Int(Date().timeIntervalSince(startDate)) <= Int(delayRange.upperBound)) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2 * delayRange.upperBound) } } @@ -260,165 +325,163 @@ extension SubscribeEffectsTests { extension SubscribeEffectsTests { func test_ReceiveReconnectingSuccess() { - let urlError = URLError(.badServerResponse) - let httpUrlResponse = HTTPURLResponse(statusCode: 500)! - let pubNubError = PubNubError(urlError.pubnubReason!, underlying: urlError, affected: [.response(httpUrlResponse)]) - let delayRange = 2.0...3.0 - mockResponse(subscribeResponse: SubscribeResponse( cursor: SubscribeCursor(timetoken: 12345, region: 1), messages: [firstMessage, secondMessage] )) - runEffect( - configuration: configWithLinearPolicy(delayRange.lowerBound), - invocation: .receiveReconnect( - channels: ["channel1", "channel1-pnpres", "channel2"], - groups: ["g1", "g2", "g2-pnpres"], - cursor: SubscribeCursor(timetoken: 1111, region: 1), - retryAttempt: 1, - reason: pubNubError - ), - timeout: 2 * delayRange.upperBound, - expectedOutput: [ - .receiveReconnectSuccess( - cursor: SubscribeCursor(timetoken: 12345, region: 1), - messages: [firstMessage, secondMessage] - ) - ] + + let expectation = XCTestExpectation(description: "Effect Completion") + expectation.expectedFulfillmentCount = 1 + expectation.assertForOverFulfill = true + + let testedInvocation: Subscribe.Invocation = .receiveReconnect( + channels: ["channel1", "channel1-pnpres", "channel2"], + groups: ["g1", "g2", "g2-pnpres"], + cursor: SubscribeCursor(timetoken: 1111, region: 1), + retryAttempt: 1, + reason: PubNubError( + URLError(.badServerResponse).pubnubReason!, + underlying: URLError(.badServerResponse), + affected: [.response(HTTPURLResponse(statusCode: 500)!)] + ) + ) + let expectedOutput: Subscribe.Event = .receiveReconnectSuccess( + cursor: SubscribeCursor(timetoken: 12345, region: 1), + messages: [firstMessage, secondMessage] ) + + let delayRange = 2.0...3.0 + let depValue = Subscribe.Dependencies(configuration: configWithLinearPolicy(delayRange.lowerBound)) + let effect = factory.effect(for: testedInvocation, with: EventEngineDependencies(value: depValue)) + + effect.performTask { + XCTAssertEqual([expectedOutput], $0) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2 * delayRange.upperBound) } func test_ReceiveReconnectingFailure() { - let delayRange = 2.0...3.0 - let urlError = URLError(.badServerResponse) - let httpUrlResponse = HTTPURLResponse(statusCode: 500)! - let pubNubError = PubNubError(urlError.pubnubReason!, underlying: urlError, affected: [.response(httpUrlResponse)]) + let expectation = XCTestExpectation(description: "Effect Completion") + expectation.expectedFulfillmentCount = 1 + expectation.assertForOverFulfill = true mockResponse( errorIfAny: URLError(.cannotFindHost), httpResponse: HTTPURLResponse(statusCode: 404)! ) - runEffect( - configuration: configWithLinearPolicy(delayRange.lowerBound), - invocation: .receiveReconnect( - channels: ["channel1", "channel1-pnpres", "channel2"], - groups: ["g1", "g2", "g2-pnpres"], - cursor: SubscribeCursor(timetoken: 1111, region: 1), - retryAttempt: 1, - reason: pubNubError - ), - timeout: 2 * delayRange.upperBound, - expectedOutput: [ - .receiveReconnectFailure( - error: PubNubError(.nameResolutionFailure) - ) - ] + let testedInvocation: Subscribe.Invocation = .receiveReconnect( + channels: ["channel1", "channel1-pnpres", "channel2"], + groups: ["g1", "g2", "g2-pnpres"], + cursor: SubscribeCursor(timetoken: 1111, region: 1), + retryAttempt: 1, + reason: PubNubError( + URLError(.badServerResponse).pubnubReason!, + underlying: URLError(.badServerResponse), + affected: [.response(HTTPURLResponse(statusCode: 500)!)] + ) ) + + let expectedOutput: Subscribe.Event = .receiveReconnectFailure(error: PubNubError(.nameResolutionFailure)) + let delayRange = 2.0...3.0 + let depValue = Subscribe.Dependencies(configuration: configWithLinearPolicy(delayRange.lowerBound)) + let effect = factory.effect(for: testedInvocation, with: EventEngineDependencies(value: depValue)) + + effect.performTask { + XCTAssertEqual([expectedOutput], $0) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2 * delayRange.upperBound) } func test_ReceiveReconnectGiveUp() { - let urlError = URLError(.badServerResponse) - let delayRange = 2.0...3.0 + let expectation = XCTestExpectation(description: "Effect Completion") + expectation.expectedFulfillmentCount = 1 + expectation.assertForOverFulfill = true mockResponse( errorIfAny: URLError(.cannotFindHost), httpResponse: HTTPURLResponse(statusCode: 404)! ) - runEffect( - configuration: configWithLinearPolicy(delayRange.lowerBound), - invocation: .receiveReconnect( - channels: ["channel1", "channel1-pnpres", "channel2"], - groups: ["g1", "g2", "g2-pnpres"], - cursor: SubscribeCursor(timetoken: 1111, region: 1), - retryAttempt: 3, - reason: PubNubError(urlError.pubnubReason!, underlying: urlError) - ), - expectedOutput: [ - .receiveReconnectGiveUp( - error: PubNubError(.badServerResponse) - ) - ] + + let testedInvocation: Subscribe.Invocation = .receiveReconnect( + channels: ["channel1", "channel1-pnpres", "channel2"], + groups: ["g1", "g2", "g2-pnpres"], + cursor: SubscribeCursor(timetoken: 1111, region: 1), + retryAttempt: 3, + reason: PubNubError( + URLError(.badServerResponse).pubnubReason!, + underlying: URLError(.badServerResponse) + ) ) + let expectedOutput: Subscribe.Event = .receiveReconnectGiveUp( + error: PubNubError(.badServerResponse) + ) + + let delayRange = 2.0...3.0 + let depValue = Subscribe.Dependencies(configuration: configWithLinearPolicy(delayRange.lowerBound)) + let effect = factory.effect(for: testedInvocation, with: EventEngineDependencies(value: depValue)) + + effect.performTask { + XCTAssertEqual([expectedOutput], $0) + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) } func test_ReceiveReconnectingIsDelayed() { - let urlError = URLError(.badServerResponse) - let httpUrlResponse = HTTPURLResponse(statusCode: 500)! - let pubNubError = PubNubError(urlError.pubnubReason!, underlying: urlError, affected: [.response(httpUrlResponse)]) - - let delayRange = 2.0...3.0 - let startDate = Date() + let expectation = XCTestExpectation(description: "Effect Completion") + expectation.expectedFulfillmentCount = 1 + expectation.assertForOverFulfill = true mockResponse(subscribeResponse: SubscribeResponse( cursor: SubscribeCursor(timetoken: 12345, region: 1), messages: [firstMessage, secondMessage] )) - runEffect( - configuration: configWithLinearPolicy(delayRange.lowerBound), - invocation: .receiveReconnect( - channels: ["channel1", "channel1-pnpres", "channel2"], - groups: ["g1", "g2", "g2-pnpres"], - cursor: SubscribeCursor(timetoken: 1111, region: 1), - retryAttempt: 1, - reason: pubNubError - ), - timeout: 2 * delayRange.upperBound, - expectedOutput: [ - .receiveReconnectSuccess( - cursor: SubscribeCursor(timetoken: 12345, region: 1), - messages: [firstMessage, secondMessage] - ) - ], - additionalValidations: { - XCTAssertTrue( - Int(Date().timeIntervalSince(startDate)) <= Int(delayRange.upperBound) - ) - } + + let testedInvocation: Subscribe.Invocation = .receiveReconnect( + channels: ["channel1", "channel1-pnpres", "channel2"], + groups: ["g1", "g2", "g2-pnpres"], + cursor: SubscribeCursor(timetoken: 1111, region: 1), + retryAttempt: 1, + reason: PubNubError( + URLError(.badServerResponse).pubnubReason!, + underlying: URLError(.badServerResponse), + affected: [.response(HTTPURLResponse(statusCode: 500)!)] + ) ) + + let delayRange = 2.0...3.0 + let startDate = Date() + let depValue = Subscribe.Dependencies(configuration: configWithLinearPolicy(delayRange.lowerBound)) + let effect = factory.effect(for: testedInvocation, with: EventEngineDependencies(value: depValue)) + + effect.performTask { _ in + XCTAssertTrue(Int(Date().timeIntervalSince(startDate)) <= Int(delayRange.upperBound)) + expectation.fulfill() + } + wait(for: [expectation], timeout: 2 * delayRange.upperBound) } } // MARK: - Helpers -fileprivate extension SubscribeEffectsTests { +private extension SubscribeEffectsTests { func mockResponse( subscribeResponse: SubscribeResponse? = nil, errorIfAny: Error? = nil, httpResponse: HTTPURLResponse = HTTPURLResponse(statusCode: 200)! ) { - mockUrlSession.responseForDataTask = { task, id in + mockUrlSession.responseForDataTask = { task, _ in task.mockError = errorIfAny task.mockData = try? Constant.jsonEncoder.encode(subscribeResponse) task.mockResponse = httpResponse return task } } - - private func runEffect( - configuration: PubNubConfiguration, - invocation: Subscribe.Invocation, - timeout: TimeInterval = 0.5, - expectedOutput results: [Subscribe.Event] = [], - additionalValidations validations: @escaping () -> Void = {} - ) { - let expectation = XCTestExpectation(description: "Effect Completion") - expectation.expectedFulfillmentCount = 1 - expectation.assertForOverFulfill = true - - let effect = factory.effect( - for: invocation, - with: EventEngineDependencies(value: Subscribe.Dependencies(configuration: configuration)) - ) - effect.performTask { - XCTAssertEqual(results, $0) - validations() - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } } -fileprivate let firstMessage = SubscribeMessagePayload( +private let firstMessage = SubscribeMessagePayload( shard: "", subscription: nil, channel: "test-channel", @@ -433,7 +496,7 @@ fileprivate let firstMessage = SubscribeMessagePayload( error: nil ) -fileprivate let secondMessage = SubscribeMessagePayload( +private let secondMessage = SubscribeMessagePayload( shard: "", subscription: nil, channel: "test-channel", diff --git a/Tests/PubNubTests/Mocking/Responses/Subscribe/subscription_invalid_json.json b/Tests/PubNubTests/Mocking/Responses/Subscribe/subscription_invalid_json.json new file mode 100644 index 00000000..8228f327 --- /dev/null +++ b/Tests/PubNubTests/Mocking/Responses/Subscribe/subscription_invalid_json.json @@ -0,0 +1,23 @@ +{ + "code": 200, + "body": { + "t": { + "t": "15912183441526350", + "r": 1 + }, + "m": [ + { + "a": "3", + "f": 512, + "p": { + "t": "15912183441554200", + "r": 1 + }, + "k": "demo-36", + "c": "swiftInvalidJSON.", + "d": "hello", + "b": "swiftInvalidJSON.*" + } + ] + } +} diff --git a/Tests/PubNubTests/Networking/Operators/InstanceIdOperatorTests.swift b/Tests/PubNubTests/Networking/Operators/InstanceIdOperatorTests.swift deleted file mode 100644 index fed9e972..00000000 --- a/Tests/PubNubTests/Networking/Operators/InstanceIdOperatorTests.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// InstanceIdOperatorTests.swift -// -// Copyright (c) PubNub Inc. -// All rights reserved. -// -// This source code is licensed under the license found in the -// LICENSE file in the root directory of this source tree. -// - -@testable import PubNub -import XCTest - -class InstanceIdOperatorTests: XCTestCase { - var pubnub: PubNub! - var config = PubNubConfiguration(publishKey: "FakeTestString", subscribeKey: "FakeTestString", userId: UUID().uuidString) - - func testUseInstanceID_Success() { - var expectations = [XCTestExpectation]() - - let sessionListener = SessionListener(queue: DispatchQueue(label: "Session Listener", - qos: .userInitiated, - attributes: .concurrent)) - - guard let sessions = try? MockURLSession.mockSession(for: ["time_success"], - with: sessionListener) - else { - return XCTFail("Could not create mock url session") - } - - let sessionExpector = SessionExpector(session: sessionListener) - sessionExpector.expectDidMutateRequest { _, initialURLRequest, mutatedURLRequest in - guard let mutatedURL = mutatedURLRequest.url, let initialURL = initialURLRequest.url else { - return XCTFail("Could not create URL during request mutation") - } - - XCTAssertFalse(initialURL.absoluteString.contains(InstanceIdOperator.instanceIDKey)) - XCTAssertTrue(mutatedURL.absoluteString.contains(InstanceIdOperator.instanceIDKey)) - } - - let totalExpectation = expectation(description: "Time Response Received") - config.useInstanceId = true - pubnub = PubNub(configuration: config, session: sessions.session) - - XCTAssertTrue(pubnub.configuration.useInstanceId) - - pubnub.time { _ in - totalExpectation.fulfill() - } - expectations.append(totalExpectation) - - XCTAssertEqual(sessionExpector.expectations.count, 1) - expectations.append(contentsOf: sessionExpector.expectations) - - wait(for: expectations, timeout: 1.0) - } -} diff --git a/Tests/PubNubTests/Networking/Routers/SubscribeRouterTests.swift b/Tests/PubNubTests/Networking/Routers/SubscribeRouterTests.swift index 8c53a1e9..b045285b 100644 --- a/Tests/PubNubTests/Networking/Routers/SubscribeRouterTests.swift +++ b/Tests/PubNubTests/Networking/Routers/SubscribeRouterTests.swift @@ -32,27 +32,29 @@ final class SubscribeRouterTests: XCTestCase { ) let testChannel = "TestChannel" - // MARK: - Endpoint Tests - + func testSubscribe_Router() { let router = SubscribeRouter(.subscribe( channels: ["TestChannel"], groups: [], channelStates: [:], timetoken: 0, region: nil, heartbeat: nil, filter: nil ), configuration: config) - + XCTAssertEqual(router.endpoint.description, "Subscribe") XCTAssertEqual(router.category, "Subscribe") XCTAssertEqual(router.service, .subscribe) } - + func testSubscribe_Router_ValidationError() { let router = SubscribeRouter(.subscribe( channels: [], groups: [], channelStates: [:], timetoken: 0, region: nil, heartbeat: nil, filter: nil ), configuration: config) - - XCTAssertNotEqual(router.validationError?.pubNubError, PubNubError(.invalidEndpointType, router: router)) + + XCTAssertNotEqual( + router.validationError?.pubNubError, + PubNubError(.invalidEndpointType, router: router) + ) } } @@ -79,16 +81,15 @@ extension SubscribeRouterTests { endpoint, configuration: config ) - + // There's no guaranteed order of returned states. // Therefore, these are two possible and valid combinations: let expStateValues = [ "{\"c1\":{\"x\":1},\"c2\":{\"a\":\"someText\"}}", "{\"c2\":{\"a\":\"someText\"},\"c1\":{\"x\":1}}" ] - let queryItems = (try? router.queryItems.get()) ?? [] - + XCTAssertTrue(queryItems.count == 8) XCTAssertTrue(queryItems.contains { $0.name == "pnsdk" }) XCTAssertTrue(queryItems.contains { $0.name == "uuid" && $0.value == "someId" }) @@ -99,7 +100,7 @@ extension SubscribeRouterTests { XCTAssertTrue(queryItems.contains { $0.name == "ee" && $0.value == nil }) XCTAssertTrue(queryItems.contains { $0.name == "state" && expStateValues.contains($0.value!) }) } - + func testSubscribeRouter_QueryParamsWithEventEngineDisabled() { let config = PubNubConfiguration( publishKey: "FakeTestString", @@ -116,13 +117,10 @@ extension SubscribeRouterTests { channels: ["c1"], groups: ["group-1", "group-2"], channelStates: channelStates, timetoken: 123456, region: "42", heartbeat: 30, filter: nil ) - let router = SubscribeRouter( - endpoint, - configuration: config - ) - + + let router = SubscribeRouter(endpoint, configuration: config) let queryItems = (try? router.queryItems.get()) ?? [] - + XCTAssertTrue(queryItems.count == 6) XCTAssertTrue(queryItems.contains { $0.name == "pnsdk" }) XCTAssertTrue(queryItems.contains { $0.name == "uuid" && $0.value == "someId" }) @@ -131,7 +129,7 @@ extension SubscribeRouterTests { XCTAssertTrue(queryItems.contains { $0.name == "tt" && $0.value == "123456" }) XCTAssertTrue(queryItems.contains { $0.name == "tr" && $0.value == "42" }) } - + func testSubscribeRouter_QueryParamsWithMaintainPresenceStateDisabled() { let config = PubNubConfiguration( publishKey: "FakeTestString", @@ -148,13 +146,10 @@ extension SubscribeRouterTests { channels: ["c1"], groups: ["group-1", "group-2"], channelStates: channelStates, timetoken: 123456, region: "42", heartbeat: 30, filter: nil ) - let router = SubscribeRouter( - endpoint, - configuration: config - ) - + + let router = SubscribeRouter(endpoint, configuration: config) let queryItems = (try? router.queryItems.get()) ?? [] - + XCTAssertTrue(queryItems.count == 7) XCTAssertTrue(queryItems.contains { $0.name == "pnsdk" }) XCTAssertTrue(queryItems.contains { $0.name == "uuid" && $0.value == "someId" }) @@ -164,7 +159,7 @@ extension SubscribeRouterTests { XCTAssertTrue(queryItems.contains { $0.name == "tr" && $0.value == "42" }) XCTAssertTrue(queryItems.contains { $0.name == "ee" && $0.value == nil }) } - + func testSubscribeRouter_QueryParamsWithEmptyPresenceStates() { let config = PubNubConfiguration( publishKey: "FakeTestString", @@ -177,13 +172,9 @@ extension SubscribeRouterTests { channels: ["c1"], groups: ["group-1", "group-2"], channelStates: [:], timetoken: 123456, region: "42", heartbeat: 30, filter: nil ) - let router = SubscribeRouter( - endpoint, - configuration: config - ) - + let router = SubscribeRouter(endpoint, configuration: config) let queryItems = (try? router.queryItems.get()) ?? [] - + XCTAssertTrue(queryItems.count == 7) XCTAssertTrue(queryItems.contains { $0.name == "pnsdk" }) XCTAssertTrue(queryItems.contains { $0.name == "uuid" && $0.value == "someId" }) @@ -195,6 +186,40 @@ extension SubscribeRouterTests { } } +// MARK: - Mock HTTP session + +fileprivate extension SubscribeRouterTests { + typealias MockResult = ( + subscriptionSession: SubscriptionSession, + listener: SubscriptionListener + ) + + func mockSubscriptionSession( + with responses: [String], + raw dataResource: [Data] = [], + and configuration: PubNubConfiguration + ) -> MockResult { + // Creates a container to resolve SubscriptionSession + let container = DependencyContainer(configuration: configuration) + let listener = SubscriptionListener() + + // Registers mock URL session before retrieving SubscriptionSession + container.register( + value: try! MockURLSession.mockSession(for: responses, raw: dataResource).session!, + forKey: HTTPSubscribeSessionDependencyKey.self + ) + + // Adds a single listener and returns the output to perform further tests + let resolvedSession = container.subscriptionSession + resolvedSession.add(listener) + + return MockResult( + subscriptionSession: resolvedSession, + listener: listener + ) + } +} + // MARK: - Message Response extension SubscribeRouterTests { @@ -203,35 +228,24 @@ extension SubscribeRouterTests { XCTContext.runActivity(named: "Testing with enableEventEngine=\(configuration.enableEventEngine)") { _ in let messageExpect = XCTestExpectation(description: "Message Event") let statusExpect = XCTestExpectation(description: "Status Event") + let mockResponses = ["subscription_handshake_success", "subscription_message_success", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, and: configuration) - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_message_success", "cancelled"] - ).session else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: configuration, with: session) - let listener = SubscriptionListener() - - listener.didReceiveMessage = { [weak self] message in + mockResult.listener.didReceiveMessage = { [weak self, mockResult] message in XCTAssertEqual(message.channel, self?.testChannel) XCTAssertEqual(message.payload.stringOptional, "Test Message") - subscription.unsubscribeAll() + mockResult.subscriptionSession.unsubscribeAll() messageExpect.fulfill() } - - listener.didReceiveStatus = { status in + mockResult.listener.didReceiveStatus = { status in if let status = try? status.get(), status == .disconnected { statusExpect.fulfill() } } + mockResult.subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) - - defer { listener.cancel() } + defer { mockResult.listener.cancel() } wait(for: [messageExpect, statusExpect], timeout: 1.0) } } @@ -244,40 +258,29 @@ extension SubscribeRouterTests { func testSubscribe_Presence() { for configuration in [config, eeEnabledConfig] { XCTContext.runActivity(named: "Testing with enableEventEngine=\(configuration.enableEventEngine)") { _ in + let mockResponses = ["subscription_handshake_success", "subscription_presence_success", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, and: configuration) let presenceExpect = XCTestExpectation(description: "Presence Event") let statusExpect = XCTestExpectation(description: "Status Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_presence_success", "cancelled"] - ).session - else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: configuration, with: session) - let listener = SubscriptionListener() - listener.didReceivePresence = { [weak self] presence in + mockResult.listener.didReceivePresence = { [weak self, mockResult] presence in XCTAssertEqual(presence.channel, self?.testChannel) XCTAssertEqual(presence.actions, [ .join(uuids: ["db9c5e39-7c95-40f5-8d71-125765b6f561", "vqwqvae39-7c95-40f5-8d71-25234165142"]), .leave(uuids: ["234vq2343-7c95-40f5-8d71-125765b6f561", "42vvsge39-7c95-40f5-8d71-25234165142"]) ]) - subscription.unsubscribeAll() + mockResult.subscriptionSession.unsubscribeAll() presenceExpect.fulfill() } - - listener.didReceiveStatus = { status in + mockResult.listener.didReceiveStatus = { status in if let status = try? status.get(), status == .disconnected { statusExpect.fulfill() } } - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) - - defer { listener.cancel() } + mockResult.subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) + + defer { mockResult.listener.cancel() } wait(for: [presenceExpect, statusExpect], timeout: 1.0) } } @@ -290,39 +293,27 @@ extension SubscribeRouterTests { func testSubscribe_Signal() { for configuration in [config, eeEnabledConfig] { XCTContext.runActivity(named: "Testing with enableEventEngine=\(configuration.enableEventEngine)") { _ in + let mockResponses = ["subscription_handshake_success", "subscription_signal_success", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, and: configuration) let signalExpect = XCTestExpectation(description: "Signal Event") let statusExpect = XCTestExpectation(description: "Status Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_signal_success", "cancelled"] - ).session else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: configuration, with: session) - let listener = SubscriptionListener() - listener.didReceiveSignal = { [weak self] signal in + mockResult.listener.didReceiveSignal = { [weak self, mockResult] signal in XCTAssertEqual(signal.channel, self?.testChannel) XCTAssertEqual(signal.publisher, "TestUser") XCTAssertEqual(signal.payload.stringOptional, "Test Signal") - - subscription.unsubscribeAll() + mockResult.subscriptionSession.unsubscribeAll() signalExpect.fulfill() } - - listener.didReceiveStatus = { status in + mockResult.listener.didReceiveStatus = { status in if let status = try? status.get(), status == .disconnected { statusExpect.fulfill() } } + mockResult.subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) - - defer { listener.cancel() } + defer { mockResult.listener.cancel() } wait(for: [signalExpect, statusExpect], timeout: 1.0) } } @@ -336,37 +327,31 @@ extension SubscribeRouterTests { func testSubscribe_UUIDMetadata_Set() { for configuration in [config, eeEnabledConfig] { XCTContext.runActivity(named: "Testing with enableEventEngine=\(configuration.enableEventEngine)") { _ in + let mockResponses = ["subscription_handshake_success", "subscription_uuidSet_success", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, and: configuration) let objectExpect = XCTestExpectation(description: "Object Event") let statusExpect = XCTestExpectation(description: "Status Event") let objectListenerExpect = XCTestExpectation(description: "Object Listener Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_uuidSet_success", "cancelled"] - ).session else { - return XCTFail("Could not create mock url session") - } - - let baseUser = PubNubUUIDMetadataBase(metadataId: "TestUserID", name: "Not Real Name") + + let baseUser = PubNubUUIDMetadataBase( + metadataId: "TestUserID", + name: "Not Real Name" + ) let patchedObjectUser = PubNubUUIDMetadataBase( metadataId: "TestUserID", name: "Test Name", type: "Test Type", status: "Test Status", updated: DateFormatter.iso8601.date(from: "2019-10-06T01:55:50.645685Z"), eTag: "UserUpdateEtag" ) - - let subscription = SubscribeSessionFactory.shared.getSession(from: configuration, with: session) - let listener = SubscriptionListener() - listener.didReceiveSubscription = { event in + mockResult.listener.didReceiveSubscription = { event in switch event { case let .connectionStatusChanged(status): if status == .disconnected { statusExpect.fulfill() } case let .uuidMetadataSet(changeset): - XCTAssertEqual( - try? changeset.apply(to: baseUser).transcode(), patchedObjectUser - ) + XCTAssertEqual(try? changeset.apply(to: baseUser).transcode(), patchedObjectUser) objectExpect.fulfill() case let .subscriptionChanged(change): switch change { @@ -381,29 +366,25 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received \(event)") } } - - listener.didReceiveObjectMetadataEvent = { event in + mockResult.listener.didReceiveObjectMetadataEvent = { [mockResult] event in switch event { case let .setUUID(changeset): XCTAssertEqual(changeset.metadataId, "TestUserID") - subscription.unsubscribeAll() + mockResult.subscriptionSession.unsubscribeAll() objectListenerExpect.fulfill() default: XCTFail("Incorrect Event Received") } } + mockResult.subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) - - defer { listener.cancel() } + defer { mockResult.listener.cancel() } wait(for: [objectExpect, statusExpect, objectListenerExpect], timeout: 1.0) } } } - + // swiftlint:disable:next cyclomatic_complexity func testSubscribe_UUIDMetadata_Removed() { for configuration in [config, eeEnabledConfig] { @@ -411,17 +392,10 @@ extension SubscribeRouterTests { let objectExpect = XCTestExpectation(description: "Object Event") let statusExpect = XCTestExpectation(description: "Status Event") let objectListenerExpect = XCTestExpectation(description: "Object Listener Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_uuidRemove_success", "cancelled"] - ).session else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: configuration, with: session) - let listener = SubscriptionListener() + let mockResponses = ["subscription_handshake_success", "subscription_uuidRemove_success", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, and: configuration) - listener.didReceiveSubscription = { event in + mockResult.listener.didReceiveSubscription = { event in switch event { case let .connectionStatusChanged(status): if status == .disconnected { @@ -443,45 +417,40 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received") } } - - listener.didReceiveObjectMetadataEvent = { event in + mockResult.listener.didReceiveObjectMetadataEvent = { [mockResult] event in switch event { case let .removedUUID(metadataId): XCTAssertEqual(metadataId, "TestUserID") - subscription.unsubscribeAll() + mockResult.subscriptionSession.unsubscribeAll() objectListenerExpect.fulfill() default: XCTFail("Incorrect Event Received") } } + mockResult.subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) - - defer { listener.cancel() } + defer { mockResult.listener.cancel() } wait(for: [objectExpect, statusExpect, objectListenerExpect], timeout: 1.0) } } } - + // swiftlint:disable:next function_body_length func testSubscribe_ChannelMetadata_Set() { for configuration in [config, eeEnabledConfig] { + let mockResponses = ["subscription_handshake_success", "subscription_channelSet_success", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, and: configuration) + XCTContext.runActivity(named: "Testing with enableEventEngine=\(configuration.enableEventEngine)") { _ in let objectExpect = XCTestExpectation(description: "Object Event") let statusExpect = XCTestExpectation(description: "Status Event") let objectListenerExpect = XCTestExpectation(description: "Object Listener Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_channelSet_success", "cancelled"] - ).session else { - return XCTFail("Could not create mock url session") - } - + let baseChannel = PubNubChannelMetadataBase( - metadataId: "TestSpaceID", name: "Not Real Name", type: "someType" + metadataId: "TestSpaceID", + name: "Not Real Name", + type: "someType" ) let patchedChannel = PubNubChannelMetadataBase( metadataId: "TestSpaceID", @@ -490,71 +459,52 @@ extension SubscribeRouterTests { updated: DateFormatter.iso8601.date(from: "2019-10-06T01:55:50.645685Z"), eTag: "SpaceUpdateEtag" ) - - let subscription = SubscribeSessionFactory.shared.getSession(from: config, with: session) - let listener = SubscriptionListener() - listener.didReceiveSubscription = { event in + mockResult.listener.didReceiveSubscription = { event in switch event { case let .connectionStatusChanged(status): if status == .disconnected { statusExpect.fulfill() } case let .channelMetadataSet(changeset): - XCTAssertEqual( - try? changeset.apply(to: baseChannel).transcode(), patchedChannel - ) + XCTAssertEqual(try? changeset.apply(to: baseChannel).transcode(), patchedChannel) objectExpect.fulfill() case let .subscriptionChanged(change): - switch change { - default: - break - } + break default: XCTFail("Incorrect Event Received") } } - - listener.didReceiveObjectMetadataEvent = { [unowned subscription] event in + mockResult.listener.didReceiveObjectMetadataEvent = { [mockResult] event in switch event { case let .setChannel(changeset): XCTAssertEqual(changeset.metadataId, "TestSpaceID") - subscription.unsubscribeAll() + mockResult.subscriptionSession.unsubscribeAll() objectListenerExpect.fulfill() default: XCTFail("Incorrect Event Received") } } + mockResult.subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) - - defer { listener.cancel() } + defer { mockResult.listener.cancel() } wait(for: [objectExpect, statusExpect, objectListenerExpect], timeout: 1.0) } } } - + // swiftlint:disable:next cyclomatic_complexity func testSubscribe_ChannelMetadata_Removed() { for configuration in [config, eeEnabledConfig] { XCTContext.runActivity(named: "Testing with enableEventEngine=\(configuration.enableEventEngine)") { _ in + let mockResponses = ["subscription_handshake_success", "subscription_channelRemove_success", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, and: configuration) let objectExpect = XCTestExpectation(description: "Object Event") let statusExpect = XCTestExpectation(description: "Status Event") let objectListenerExpect = XCTestExpectation(description: "Object Listener Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_channelRemove_success", "cancelled"] - ).session else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: configuration, with: session) - let listener = SubscriptionListener() - listener.didReceiveSubscription = { event in + mockResult.listener.didReceiveSubscription = { event in switch event { case let .connectionStatusChanged(status): if status == .disconnected { @@ -576,53 +526,47 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received") } } - - listener.didReceiveObjectMetadataEvent = { event in + mockResult.listener.didReceiveObjectMetadataEvent = { [mockResult] event in switch event { case let .removedChannel(metadataId: metadataId): XCTAssertEqual(metadataId, "TestSpaceID") - subscription.unsubscribeAll() + mockResult.subscriptionSession.unsubscribeAll() objectListenerExpect.fulfill() default: XCTFail("Incorrect Event Received") } } + mockResult.subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) - - defer { listener.cancel() } + defer { mockResult.listener.cancel() } wait(for: [objectExpect, statusExpect, objectListenerExpect], timeout: 1.0) } } } - + // swiftlint:disable:next function_body_length cyclomatic_complexity func testSubscribe_Membership_Set() { for configuration in [config, eeEnabledConfig] { XCTContext.runActivity(named: "Testing with enableEventEngine=\(configuration.enableEventEngine)") { _ in + let mockResponses = ["subscription_handshake_success", "subscription_membershipSet_success", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, and: configuration) let objectExpect = XCTestExpectation(description: "Object Event") let statusExpect = XCTestExpectation(description: "Status Event") let objectListenerExpect = XCTestExpectation(description: "Object Listener Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_membershipSet_success", "cancelled"] - ).session else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: configuration, with: session) + let channel = PubNubChannelMetadataBase(metadataId: "TestSpaceID") let uuid = PubNubUUIDMetadataBase(metadataId: "TestUserID") + let testMembership = PubNubMembershipMetadataBase( - uuidMetadataId: "TestUserID", channelMetadataId: "TestSpaceID", uuid: uuid, channel: channel, custom: ["something": true], + uuidMetadataId: "TestUserID", + channelMetadataId: "TestSpaceID", + uuid: uuid, channel: channel, + custom: ["something": true], updated: DateFormatter.iso8601.date(from: "2019-10-05T23:35:38.457823306Z"), eTag: "TestETag" ) - let listener = SubscriptionListener() - listener.didReceiveSubscription = { [unowned self] event in + mockResult.listener.didReceiveSubscription = { [unowned self] event in switch event { case let .connectionStatusChanged(status): if status == .disconnected { @@ -644,53 +588,44 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received \(event)") } } - - listener.didReceiveObjectMetadataEvent = { event in + mockResult.listener.didReceiveObjectMetadataEvent = { [mockResult] event in switch event { case let .setMembership(membership): XCTAssertEqual(try? membership.transcode(), testMembership) - subscription.unsubscribeAll() + mockResult.subscriptionSession.unsubscribeAll() objectListenerExpect.fulfill() default: XCTFail("Incorrect Event Received \(event)") } } + mockResult.subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) - - defer { listener.cancel() } + defer { mockResult.listener.cancel() } wait(for: [objectExpect, statusExpect, objectListenerExpect], timeout: 1.0) } } } - + // swiftlint:disable:next function_body_length cyclomatic_complexity func testSubscribe_Membership_Removed() { for configuration in [config, eeEnabledConfig] { XCTContext.runActivity(named: "Testing with enableEventEngine=\(configuration.enableEventEngine)") { _ in + let mockResponses = ["subscription_handshake_success", "subscription_membershipRemove_success", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, and: configuration) let objectExpect = XCTestExpectation(description: "Object Event") let statusExpect = XCTestExpectation(description: "Status Event") let objectListenerExpect = XCTestExpectation(description: "Object Listener Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_membershipRemove_success", "leave_success"] - ).session else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: config, with: session) let channel = PubNubChannelMetadataBase(metadataId: "TestSpaceID") let uuid = PubNubUUIDMetadataBase(metadataId: "TestUserID") + let testMembership = PubNubMembershipMetadataBase( - uuidMetadataId: "TestUserID", channelMetadataId: "TestSpaceID", uuid: uuid, channel: channel, + uuidMetadataId: "TestUserID", channelMetadataId: "TestSpaceID", + uuid: uuid, channel: channel, updated: DateFormatter.iso8601.date(from: "2019-10-05T23:35:38.457823306Z"), eTag: "TestETag" ) - - let listener = SubscriptionListener() - listener.didReceiveSubscription = { [weak self] event in + + mockResult.listener.didReceiveSubscription = { [weak self] event in switch event { case let .connectionStatusChanged(status): if status == .disconnected { @@ -712,24 +647,20 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received \(event)") } } - - listener.didReceiveObjectMetadataEvent = { [unowned subscription] event in + mockResult.listener.didReceiveObjectMetadataEvent = { [mockResult] event in switch event { case let .removedMembership(membership): XCTAssertEqual(try? membership.transcode(), testMembership) - subscription.unsubscribeAll() + mockResult.subscriptionSession.unsubscribeAll() objectListenerExpect.fulfill() default: XCTFail("Incorrect Event Received \(event)") } } + mockResult.subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) - - defer { listener.cancel() } + defer { mockResult.listener.cancel() } wait(for: [objectExpect, statusExpect, objectListenerExpect], timeout: 1.0) } } @@ -746,17 +677,10 @@ extension SubscribeRouterTests { let actionExpect = XCTestExpectation(description: "Message Action Event") let statusExpect = XCTestExpectation(description: "Status Event") let actionListenerExpect = XCTestExpectation(description: "Action Listener Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_addMessageAction_success", "leave_success"] - ).session else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: configuration, with: session) - let listener = SubscriptionListener() + let mockResponses = ["subscription_handshake_success", "subscription_addMessageAction_success", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, and: configuration) - listener.didReceiveSubscription = { [weak self] event in + mockResult.listener.didReceiveSubscription = { [weak self] event in switch event { case let .connectionStatusChanged(status): if status == .disconnected { @@ -778,47 +702,36 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received \(event)") } } - - listener.didReceiveMessageAction = { [weak self] event in + mockResult.listener.didReceiveMessageAction = { [weak self, mockResult] event in switch event { case let .added(action): XCTAssertEqual(try? action.transcode(), self?.testAction) - subscription.unsubscribeAll() + mockResult.subscriptionSession.unsubscribeAll() actionListenerExpect.fulfill() default: XCTFail("Incorrect Event Received") } } + mockResult.subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) - - defer { listener.cancel() } + defer { mockResult.listener.cancel() } wait(for: [actionExpect, statusExpect, actionListenerExpect], timeout: 1.0) } } } - + // swiftlint:disable:next cyclomatic_complexity function_body_length func testSubscribe_MessageAction_Removed() { for configuration in [config, eeEnabledConfig] { XCTContext.runActivity(named: "Testing with enableEventEngine=\(configuration.enableEventEngine)") { _ in + let mockResponses = ["subscription_handshake_success", "subscription_removeMessageAction_success", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, and: configuration) let actionExpect = XCTestExpectation(description: "Message Action Event") let statusExpect = XCTestExpectation(description: "Status Event") let actionListenerExpect = XCTestExpectation(description: "Action Listener Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_removeMessageAction_success", "leave_success"] - ).session else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: configuration, with: session) - let listener = SubscriptionListener() - listener.didReceiveSubscription = { [weak self] event in + mockResult.listener.didReceiveSubscription = { [weak self] event in switch event { case let .connectionStatusChanged(status): if status == .disconnected { @@ -840,24 +753,20 @@ extension SubscribeRouterTests { XCTFail("Incorrect Event Received") } } - - listener.didReceiveMessageAction = { [weak self] event in + mockResult.listener.didReceiveMessageAction = { [weak self, mockResult] event in switch event { case let .removed(action): XCTAssertEqual(try? action.transcode(), self?.testAction) - subscription.unsubscribeAll() + mockResult.subscriptionSession.unsubscribeAll() actionListenerExpect.fulfill() default: XCTFail("Incorrect Event Received") } } + mockResult.subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) - - defer { listener.cancel() } + defer { mockResult.listener.cancel() } wait(for: [actionExpect, statusExpect, actionListenerExpect], timeout: 1.0) } } @@ -870,54 +779,44 @@ extension SubscribeRouterTests { func testSubscribe_Mixed() { for configuration in [config, eeEnabledConfig] { XCTContext.runActivity(named: "Testing with enableEventEngine=\(configuration.enableEventEngine)") { _ in + let mockResponses = ["subscription_handshake_success", "subscription_mixed_success", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, and: configuration) let messageExpect = XCTestExpectation(description: "Message Event") let presenceExpect = XCTestExpectation(description: "Presence Event") let signalExpect = XCTestExpectation(description: "Signal Event") let statusExpect = XCTestExpectation(description: "Status Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_mixed_success", "leave_success"] - ).session else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: configuration, with: session) - - let listener = SubscriptionListener() var payloadCount = 0 - listener.didReceiveSubscription = { _ in + + mockResult.listener.didReceiveSubscription = { [mockResult] _ in payloadCount += 1 if payloadCount == 7 { - subscription.unsubscribeAll() + mockResult.subscriptionSession.unsubscribeAll() } } - - listener.didReceiveMessage = { [weak self] message in + mockResult.listener.didReceiveMessage = { [weak self] message in XCTAssertEqual(message.channel, self?.testChannel) XCTAssertEqual(message.payload.stringOptional, "Test Message") messageExpect.fulfill() } - listener.didReceivePresence = { [weak self] presence in + mockResult.listener.didReceivePresence = { [weak self] presence in XCTAssertEqual(presence.channel, self?.testChannel) XCTAssertEqual(presence.actions, [.join(uuids: ["db9c5e39-7c95-40f5-8d71-125765b6f561"])]) presenceExpect.fulfill() } - listener.didReceiveSignal = { [weak self] signal in + mockResult.listener.didReceiveSignal = { [weak self] signal in XCTAssertEqual(signal.channel, self?.testChannel) XCTAssertEqual(signal.payload.stringOptional, "Test Signal") signalExpect.fulfill() } - listener.didReceiveStatus = { status in + mockResult.listener.didReceiveStatus = { status in if let status = try? status.get(), status == .disconnected { statusExpect.fulfill() } } - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) - - defer { listener.cancel() } + mockResult.subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) + + defer { mockResult.listener.cancel() } wait(for: [signalExpect, statusExpect], timeout: 1.0) } } @@ -932,25 +831,17 @@ extension SubscribeRouterTests { XCTContext.runActivity(named: "Testing with enableEventEngine=\(configuration.enableEventEngine)") { _ in // swiftlint:disable:next line_length let corruptBase64Response = "eyJ0Ijp7InQiOiIxNTkxMjE4MzQ0MTUyNjM1MCIsInIiOjF9LCJtIjpbeyJhIjoiMyIsImYiOjUxMiwicCI6eyJ0IjoiMTU5MTIxODM0NDE1NTQyMDAiLCJyIjoxfSwiayI6ImRlbW8tMzYiLCJjIjoic3dpZnRJbnZhbGlkSlNPTi7/IiwiZCI6ImhlbGxvIiwiYiI6InN3aWZ0SW52YWxpZEpTT04uKiJ9XX0=" - + guard let corruptedData = Data(base64Encoded: corruptBase64Response) else { return XCTFail("Could not create Data from String") } - + + let mockResponses = ["subscription_handshake_success", "subscription_invalid_json", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, raw: [corruptedData], and: configuration) let errorExpect = XCTestExpectation(description: "Error Event") let statusExpect = XCTestExpectation(description: "Status Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "cancelled"], - raw: [corruptedData] - ).session else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: configuration, with: session) - let listener = SubscriptionListener() - listener.didReceiveSubscription = { event in + mockResult.listener.didReceiveSubscription = { [mockResult] event in switch event { case .subscriptionChanged: break @@ -960,19 +851,16 @@ extension SubscribeRouterTests { } case let .subscribeError(error): XCTAssertEqual(error.reason, .jsonDataDecodingFailure) - subscription.unsubscribeAll() + mockResult.subscriptionSession.unsubscribeAll() errorExpect.fulfill() default: XCTFail("Unexpected event received \(event)") } } + mockResult.subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) - - defer { listener.cancel() } + defer { mockResult.listener.cancel() } wait(for: [errorExpect, statusExpect], timeout: 1.0, enforceOrder: true) } } @@ -985,19 +873,13 @@ extension SubscribeRouterTests { func testUnsubscribe() { for configuration in [config, eeEnabledConfig] { XCTContext.runActivity(named: "Testing with enableEventEngine=\(configuration.enableEventEngine)") { _ in + let mockResponses = ["subscription_handshake_success", "subscription_mixed_success", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, and: configuration) let statusExpect = XCTestExpectation(description: "Status Event") statusExpect.expectedFulfillmentCount = 2 - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_mixed_success", "cancelled"] - ).session else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: configuration, with: session) - let listener = SubscriptionListener() + statusExpect.assertForOverFulfill = true - listener.didReceiveSubscription = { [unowned self] event in + mockResult.listener.didReceiveSubscription = { [unowned self, mockResult] event in switch event { case let .subscriptionChanged(change): switch change { @@ -1011,8 +893,8 @@ extension SubscribeRouterTests { case let .connectionStatusChanged(status): switch status { case .connected: - subscription.unsubscribe(from: [self.testChannel]) - XCTAssertEqual(subscription.subscribedChannels, []) + mockResult.subscriptionSession.unsubscribe(from: [self.testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, []) statusExpect.fulfill() case .disconnected: statusExpect.fulfill() @@ -1023,34 +905,24 @@ extension SubscribeRouterTests { break } } + mockResult.subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, [testChannel]) - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) - - defer { listener.cancel() } + defer { mockResult.listener.cancel() } wait(for: [statusExpect], timeout: 1.0) } } } - + func testUnsubscribeAll() { for configuration in [config, eeEnabledConfig] { XCTContext.runActivity(named: "Testing with enableEventEngine=\(configuration.enableEventEngine)") { _ in + let mockResponses = ["subscription_handshake_success", "subscription_mixed_success", "cancelled"] + let mockResult = mockSubscriptionSession(with: mockResponses, and: configuration) let statusExpect = XCTestExpectation(description: "Status Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_mixed_success", "cancelled"] - ).session else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: configuration, with: session) let otherChannel = "OtherChannel" - let listener = SubscriptionListener() - listener.didReceiveSubscription = { [weak self] event in + mockResult.listener.didReceiveSubscription = { [weak self, mockResult] event in switch event { case let .subscriptionChanged(change): switch change { @@ -1066,8 +938,8 @@ extension SubscribeRouterTests { case let .connectionStatusChanged(status): switch status { case .connected: - subscription.unsubscribeAll() - XCTAssertEqual(subscription.subscribedChannels, []) + mockResult.subscriptionSession.unsubscribeAll() + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, []) statusExpect.fulfill() case .disconnected: statusExpect.fulfill() @@ -1079,17 +951,13 @@ extension SubscribeRouterTests { } } - subscription.add(listener) - subscription.subscribe(to: [testChannel, otherChannel]) - - XCTAssertTrue(subscription.subscribedChannels.contains(testChannel)) - XCTAssertTrue(subscription.subscribedChannels.contains(otherChannel)) - - subscription.unsubscribeAll() + mockResult.subscriptionSession.subscribe(to: [testChannel, otherChannel]) + XCTAssertTrue(mockResult.subscriptionSession.subscribedChannels.contains(testChannel)) + XCTAssertTrue(mockResult.subscriptionSession.subscribedChannels.contains(otherChannel)) + mockResult.subscriptionSession.unsubscribeAll() + XCTAssertEqual(mockResult.subscriptionSession.subscribedChannels, []) - XCTAssertEqual(subscription.subscribedChannels, []) - - defer { listener.cancel() } + defer { mockResult.listener.cancel() } wait(for: [statusExpect], timeout: 1.0) } } @@ -1101,12 +969,8 @@ extension SubscribeRouterTests { extension SubscribeRouterTests { func testSubscribe_DecryptNonEncryptedMessage() { let messageExpect = XCTestExpectation(description: "Message Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_message_success", "cancelled"] - ).session else { - return XCTFail("Could not create mock url session") - } + messageExpect.assertForOverFulfill = true + messageExpect.expectedFulfillmentCount = 1 let config = PubNubConfiguration( publishKey: "pubKey", @@ -1114,22 +978,29 @@ extension SubscribeRouterTests { userId: "userId", cryptoModule: CryptoModule.aesCbcCryptoModule(with: "pubnubenigma") ) - let pubNubWithMockedSession = PubNub( - configuration: config, - subscribeSession: session + let mockResponses = [ + "subscription_handshake_success", + "subscription_message_success", + "cancelled" + ] + let container = DependencyContainer(configuration: config).register( + value: try! MockURLSession.mockSession(for: mockResponses).session, + forKey: HTTPSubscribeSessionDependencyKey.self ) + + let pubnub = PubNub(container: container) let listener = SubscriptionListener() - listener.didReceiveMessage = { [weak self, unowned pubNubWithMockedSession] message in + listener.didReceiveMessage = { [weak self, unowned pubnub] message in XCTAssertEqual(message.channel, self?.testChannel) XCTAssertEqual(message.payload.stringOptional, "Test Message") XCTAssertTrue(message.error?.reason == .decryptionFailure) - pubNubWithMockedSession.unsubscribeAll() + pubnub.unsubscribeAll() messageExpect.fulfill() } - pubNubWithMockedSession.add(listener) - pubNubWithMockedSession.subscribe(to: [testChannel]) + pubnub.add(listener) + pubnub.subscribe(to: [testChannel]) defer { listener.cancel() } wait(for: [messageExpect], timeout: 1.0) @@ -1137,12 +1008,8 @@ extension SubscribeRouterTests { func testSubscribe_DecryptEncryptedMessage() { let messageExpect = XCTestExpectation(description: "Message Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_encrypted_message_success", "cancelled"] - ).session else { - return XCTFail("Could not create mock url session") - } + messageExpect.assertForOverFulfill = true + messageExpect.expectedFulfillmentCount = 1 let config = PubNubConfiguration( publishKey: "pubKey", @@ -1150,22 +1017,29 @@ extension SubscribeRouterTests { userId: "userId", cryptoModule: CryptoModule.aesCbcCryptoModule(with: "pubnubenigma") ) - let pubNubWithMockedSession = PubNub( - configuration: config, - subscribeSession: session + let mockResponses = [ + "subscription_handshake_success", + "subscription_encrypted_message_success", + "cancelled" + ] + let container = DependencyContainer(configuration: config).register( + value: try! MockURLSession.mockSession(for: mockResponses).session, + forKey: HTTPSubscribeSessionDependencyKey.self ) + + let pubnub = PubNub(container: container) let listener = SubscriptionListener() - listener.didReceiveMessage = { [weak self, unowned pubNubWithMockedSession] message in + listener.didReceiveMessage = { [weak self, unowned pubnub] message in XCTAssertEqual(message.channel, self?.testChannel) XCTAssertEqual(message.payload.stringOptional, "Test Message") XCTAssertNil(message.error) - pubNubWithMockedSession.unsubscribeAll() + pubnub.unsubscribeAll() messageExpect.fulfill() } - pubNubWithMockedSession.add(listener) - pubNubWithMockedSession.subscribe(to: [testChannel]) + pubnub.add(listener) + pubnub.subscribe(to: [testChannel]) defer { listener.cancel() } wait(for: [messageExpect], timeout: 1.0) @@ -1173,12 +1047,8 @@ extension SubscribeRouterTests { func testSubscribe_DecryptEncryptedMessageWithMismatchedKey() { let messageExpect = XCTestExpectation(description: "Message Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_encrypted_message_success", "cancelled"] - ).session else { - return XCTFail("Could not create mock url session") - } + messageExpect.assertForOverFulfill = true + messageExpect.expectedFulfillmentCount = 1 let config = PubNubConfiguration( publishKey: "pubKey", @@ -1186,23 +1056,29 @@ extension SubscribeRouterTests { userId: "userId", cryptoModule: CryptoModule.aesCbcCryptoModule(with: "lorem-ipsum-dolor-sit-amet") ) - let pubNubWithMockedSession = PubNub( - configuration: config, - subscribeSession: session + let mockResponses = [ + "subscription_handshake_success", + "subscription_encrypted_message_success", + "cancelled" + ] + let container = DependencyContainer(configuration: config).register( + value: try! MockURLSession.mockSession(for: mockResponses).session, + forKey: HTTPSubscribeSessionDependencyKey.self ) + let pubnub = PubNub(container: container) let listener = SubscriptionListener() - listener.didReceiveMessage = { [weak self, unowned pubNubWithMockedSession] message in + listener.didReceiveMessage = { [weak self, unowned pubnub] message in XCTAssertEqual(message.channel, self?.testChannel) XCTAssertEqual(message.payload.stringOptional, "UE5FRAFBQ1JIEGOmGQMIMXD+91V+5hTxm7p7uEUhEEYohYLQz5fEGITC") XCTAssertTrue(message.error?.reason == .decryptionFailure) - pubNubWithMockedSession.unsubscribeAll() + pubnub.unsubscribeAll() messageExpect.fulfill() } - pubNubWithMockedSession.add(listener) - pubNubWithMockedSession.subscribe(to: [testChannel]) + pubnub.add(listener) + pubnub.subscribe(to: [testChannel]) defer { listener.cancel() } wait(for: [messageExpect], timeout: 1.0) diff --git a/Tests/PubNubTests/Subscription/SubscribeSessionFactoryTests.swift b/Tests/PubNubTests/Subscription/SubscribeSessionFactoryTests.swift index be09221a..6d9cb92d 100644 --- a/Tests/PubNubTests/Subscription/SubscribeSessionFactoryTests.swift +++ b/Tests/PubNubTests/Subscription/SubscribeSessionFactoryTests.swift @@ -14,19 +14,30 @@ import XCTest class SubscribeSessionFactoryTests: XCTestCase { func testLoggingSameInstance() { let config = PubNubConfiguration(publishKey: nil, subscribeKey: "FakeKey", userId: UUID().uuidString) - let first = SubscribeSessionFactory.shared.getSession(from: config) - let second = SubscribeSessionFactory.shared.getSession(from: config) + let dependencyContainer = DependencyContainer(configuration: config) + let first = dependencyContainer.subscriptionSession + let second = dependencyContainer.subscriptionSession XCTAssertEqual(first.uuid, second.uuid) } func testMutlipleInstances() { - let config = PubNubConfiguration(publishKey: nil, subscribeKey: "FakeKey", userId: UUID().uuidString) - var newConfig = PubNubConfiguration(publishKey: nil, subscribeKey: "OtherKey", userId: UUID().uuidString) - newConfig.authKey = "SomeNewKey" - - let first = SubscribeSessionFactory.shared.getSession(from: config) - let third = SubscribeSessionFactory.shared.getSession(from: newConfig) + let config = PubNubConfiguration( + publishKey: nil, + subscribeKey: "FakeKey", + userId: UUID().uuidString + ) + let newConfig = PubNubConfiguration( + publishKey: nil, + subscribeKey: "OtherKey", + userId: UUID().uuidString, + authKey: "SomeNewKey" + ) + + let dependencyContainer = DependencyContainer(configuration: config) + let nextDependencyContainer = DependencyContainer(configuration: config) + let first = dependencyContainer.subscriptionSession + let third = nextDependencyContainer.subscriptionSession XCTAssertNotEqual(first.uuid, third.uuid) } diff --git a/Tests/PubNubTests/Subscription/SubscriptionSessionTests.swift b/Tests/PubNubTests/Subscription/SubscriptionSessionTests.swift index 6ff2a66f..70c00a63 100644 --- a/Tests/PubNubTests/Subscription/SubscriptionSessionTests.swift +++ b/Tests/PubNubTests/Subscription/SubscriptionSessionTests.swift @@ -48,42 +48,33 @@ class SubscriptionSessionTests: XCTestCase { XCTContext.runActivity(named: "Testing with enableEventEngine=\(configuration.enableEventEngine)") { _ in let messageExpect = XCTestExpectation(description: "Message Event") let statusExpect = XCTestExpectation(description: "Status Event") - - guard let session = try? MockURLSession.mockSession( - for: ["subscription_handshake_success", "subscription_message_success", "cancelled"] - ).session else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: configuration, with: session) + let mockResponses = ["subscription_handshake_success", "subscription_message_success", "cancelled"] + let subscriptionSession = mockSubscriptionSession(with: mockResponses, and: configuration) let listener = SubscriptionListener() listener.didReceiveMessage = { message in XCTAssertEqual( - subscription.previousTokenResponse, + subscriptionSession.previousTokenResponse, SubscribeCursor(timetoken: 15614817397807903, region: 2) ) - subscription.unsubscribeAll() + subscriptionSession.unsubscribeAll() messageExpect.fulfill() } listener.didReceiveStatus = { status in if let status = try? status.get(), status == .connected { XCTAssertEqual( - subscription.previousTokenResponse, + subscriptionSession.previousTokenResponse, SubscribeCursor(timetoken: 16873352451141050, region: 42) ) statusExpect.fulfill() } } - - subscription.add(listener) - subscription.subscribe(to: [testChannel]) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) + subscriptionSession.add(listener) + subscriptionSession.subscribe(to: [testChannel]) + XCTAssertEqual(subscriptionSession.subscribedChannels, [testChannel]) defer { listener.cancel() } wait(for: [messageExpect, statusExpect], timeout: 1.0) - } } } @@ -95,32 +86,25 @@ class SubscriptionSessionTests: XCTestCase { statusExpect.assertForOverFulfill = true statusExpect.expectedFulfillmentCount = configuration.enableEventEngine ? 2 : 1 - guard let session = try? MockURLSession.mockSession( - for: ["badURL", "cancelled"] - ).session else { - return XCTFail("Could not create mock url session") - } - - let subscription = SubscribeSessionFactory.shared.getSession(from: config, with: session) + let mockResponses = ["badURL", "cancelled"] + let subscriptionSession = mockSubscriptionSession(with: mockResponses, and: configuration) let listener = SubscriptionListener() - listener.didReceiveStatus = { [unowned subscription] status in + listener.didReceiveStatus = { [unowned subscriptionSession] status in if case .failure(_) = status { - XCTAssertNil(subscription.previousTokenResponse) + XCTAssertNil(subscriptionSession.previousTokenResponse) statusExpect.fulfill() } if case .success(let newStatus) = status { if newStatus == .connectionError(PubNubError(.invalidURL)) { - XCTAssertNil(subscription.previousTokenResponse) + XCTAssertNil(subscriptionSession.previousTokenResponse) statusExpect.fulfill() } } } - - subscription.add(listener) - subscription.subscribe(to: [testChannel], at: SubscribeCursor(timetoken: 123456, region: 1)) - - XCTAssertEqual(subscription.subscribedChannels, [testChannel]) + subscriptionSession.add(listener) + subscriptionSession.subscribe(to: [testChannel], at: SubscribeCursor(timetoken: 123456, region: 1)) + XCTAssertEqual(subscriptionSession.subscribedChannels, [testChannel]) defer { listener.cancel() } wait(for: [statusExpect], timeout: 1.0) @@ -128,3 +112,18 @@ class SubscriptionSessionTests: XCTestCase { } } } + +fileprivate extension SubscriptionSessionTests { + func mockSubscriptionSession( + with responses: [String], + and configuration: PubNubConfiguration + ) -> SubscriptionSession { + let dependencyContainer = DependencyContainer(configuration: configuration) + let mockURLSession = try! MockURLSession.mockSession(for: responses).session + + return dependencyContainer.register( + value: mockURLSession, + forKey: HTTPSubscribeSessionDependencyKey.self + ).subscriptionSession + } +}