From c58d58e3761010289d395ab2530cb19f38255108 Mon Sep 17 00:00:00 2001 From: jguz-pubnub <102806147+jguz-pubnub@users.noreply.github.com> Date: Mon, 18 Mar 2024 11:09:49 +0100 Subject: [PATCH] Fixes for PubNub client (#160) fix(subscribe): preventing disconnection when subscribing via PubNub to a channel that was already subscribed to fix(crypto): fixes for computing a hash value in CryptoModule --- .pubnub.yml | 11 +- PubNub.xcodeproj/project.pbxproj | 16 +-- PubNubSwift.podspec | 2 +- README.md | 4 +- Sources/PubNub/Helpers/Constants.swift | 2 +- .../PubNub/Helpers/Crypto/CryptoModule.swift | 67 ++++++----- .../Crypto/Cryptors/AESCBCCryptor.swift | 7 ++ .../Helpers/Crypto/Cryptors/Cryptor.swift | 2 +- .../Crypto/Cryptors/LegacyCryptor.swift | 8 ++ Sources/PubNub/PubNub.swift | 8 +- .../Subscription/SubscriptionSession.swift | 112 ++++++++---------- .../PubNubCryptoModuleContractTestSteps.swift | 2 +- .../Events/New/SubscriptionTests.swift | 28 ++--- .../PublishEndpointIntegrationTests.swift | 86 +++++++++++--- .../SubscriptionIntegrationTests.swift | 99 +++++++++++++--- .../PubNubConfigurationTests.swift | 25 +++- 16 files changed, 318 insertions(+), 161 deletions(-) diff --git a/.pubnub.yml b/.pubnub.yml index b4bcada2..0a4db9aa 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,9 +1,16 @@ --- name: swift scm: github.com/pubnub/swift -version: "7.0.0" +version: "7.1.0" schema: 1 changelog: + - date: 2024-03-18 + version: 7.1.0 + changes: + - type: bug + text: "Preventing disconnection when subscribing via `PubNub` to a channel that was already subscribed to." + - type: bug + text: "Fixes for computing a hash value in `CryptoModule`." - date: 2024-02-21 version: 7.0.0 changes: @@ -524,7 +531,7 @@ sdks: - distribution-type: source distribution-repository: GitHub release package-name: PubNub - location: https://github.com/pubnub/swift/archive/refs/tags/7.0.0.zip + location: https://github.com/pubnub/swift/archive/refs/tags/7.1.0.zip supported-platforms: supported-operating-systems: macOS: diff --git a/PubNub.xcodeproj/project.pbxproj b/PubNub.xcodeproj/project.pbxproj index 6453c2ff..431548aa 100644 --- a/PubNub.xcodeproj/project.pbxproj +++ b/PubNub.xcodeproj/project.pbxproj @@ -3843,7 +3843,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 7.0.0; + MARKETING_VERSION = 7.1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.pubnub.swift.PubNubUser; @@ -3892,7 +3892,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 7.0.0; + MARKETING_VERSION = 7.1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.pubnub.swift.PubNubUser; @@ -3998,7 +3998,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 7.0.0; + MARKETING_VERSION = 7.1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.pubnub.swift.PubNubSpace; @@ -4049,7 +4049,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 7.0.0; + MARKETING_VERSION = 7.1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.pubnub.swift.PubNubSpace; @@ -4168,7 +4168,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 7.0.0; + MARKETING_VERSION = 7.1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.pubnub.swift.PubNubMembership; @@ -4218,7 +4218,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 7.0.0; + MARKETING_VERSION = 7.1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.pubnub.swift.PubNubMembership; @@ -4696,7 +4696,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 7.0.0; + MARKETING_VERSION = 7.1.0; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; @@ -4737,7 +4737,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 7.0.0; + MARKETING_VERSION = 7.1.0; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; diff --git a/PubNubSwift.podspec b/PubNubSwift.podspec index c4688940..9d9f4a35 100644 --- a/PubNubSwift.podspec +++ b/PubNubSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'PubNubSwift' - s.version = '7.0.0' + s.version = '7.1.0' s.homepage = 'https://github.com/pubnub/swift' s.documentation_url = 'https://www.pubnub.com/docs/swift-native/pubnub-swift-sdk' s.authors = { 'PubNub, Inc.' => 'support@pubnub.com' } diff --git a/README.md b/README.md index 61eedd27..86bc682d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Build Status](https://travis-ci.org/pubnub/swift.svg?branch=master)](https://travis-ci.org/pubnub/swift) -[![Codacy Coverage Grade Badge](https://api.codacy.com/project/badge/Grade/d6dbd8cad97d42bbb72c47137e94d6f5)](https://www.codacy.com?utm_source=github.com&utm_medium=referral&utm_content=pubnub/swift&utm_campaign=Badge_Grade) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/ea96a32a311944eaa09b4c452db4d397)](https://app.codacy.com?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) This is the official PubNub Swift SDK repository. @@ -59,7 +59,7 @@ end > Note: Replace `YOUR_TARGET_NAME` with your target's name. -In the directory containing your `Podfile`. execute the following: +In the directory containing your `Podfile` execute the following: ```bash pod install diff --git a/Sources/PubNub/Helpers/Constants.swift b/Sources/PubNub/Helpers/Constants.swift index e93273f7..8c287679 100644 --- a/Sources/PubNub/Helpers/Constants.swift +++ b/Sources/PubNub/Helpers/Constants.swift @@ -57,7 +57,7 @@ public enum Constant { static let pubnubSwiftSDKName: String = "PubNubSwift" - static let pubnubSwiftSDKVersion: String = "7.0.0" + static let pubnubSwiftSDKVersion: String = "7.1.0" static let appBundleId: String = { if let info = Bundle.main.infoDictionary, diff --git a/Sources/PubNub/Helpers/Crypto/CryptoModule.swift b/Sources/PubNub/Helpers/Crypto/CryptoModule.swift index f75db9b3..fdfe29fa 100644 --- a/Sources/PubNub/Helpers/Crypto/CryptoModule.swift +++ b/Sources/PubNub/Helpers/Crypto/CryptoModule.swift @@ -15,6 +15,7 @@ public class CryptorModule { public static func aesCbcCryptoModule(with key: String, withRandomIV: Bool = true) -> CryptoModule { preconditionFailure("This method is no longer available") } + public static func legacyCryptoModule(with key: String, withRandomIV: Bool = true) -> CryptoModule { preconditionFailure("This method is no longer available") } @@ -22,22 +23,23 @@ public class CryptorModule { /// Object capable of encryption/decryption public struct CryptoModule { - private let defaultCryptor: Cryptor - private let cryptors: [Cryptor] + private let defaultCryptor: any Cryptor + private let cryptors: [any Cryptor] private let legacyCryptorId: CryptorId = [] typealias Base64EncodedString = String /// Initializes `CryptoModule` with custom ``Cryptor`` objects capable of encryption and decryption /// - /// Use this constructor if you would like to provide **custom** objects for decryption and encryption and don't want to use PubNub's built-in `Cryptors`. - /// Otherwise, refer to convenience static factory methods such as ``aesCbcCryptoModule(with:withRandomIV:)`` - /// and ``legacyCryptoModule(with:withRandomIV:)`` that return `CryptoModule` configured for you. + /// Use this constructor if you would like to provide **custom** objects for decryption and encryption + /// and don't want to use PubNub's built-in `Cryptors`. Otherwise, refer to convenience static factory methods + /// such as ``aesCbcCryptoModule(with:withRandomIV:)``and ``legacyCryptoModule(with:withRandomIV:)`` + /// that return `CryptoModule` configured for you. /// /// - Parameters: /// - default: Primary ``Cryptor`` instance used for encryption and decryption /// - cryptors: An optional list of ``Cryptor`` instances which older messages/files were encoded - public init(default cryptor: Cryptor, cryptors: [Cryptor] = []) { + public init(default cryptor: any Cryptor, cryptors: [any Cryptor] = []) { self.defaultCryptor = cryptor self.cryptors = cryptors } @@ -46,13 +48,15 @@ public struct CryptoModule { /// /// - Parameters: /// - data: Data to encrypt - /// - Returns: A success, storing encrypted `Data` if operation succeeds. Otherwise, a failure storing `PubNubError` is returned + /// - Returns: + /// - **Success**: An encrypted `Data` object + /// - **Failure**: `PubNubError` describing the reason of failure public func encrypt(data: Data) -> Result { guard !data.isEmpty else { return .failure(PubNubError( .encryptionFailure, - additional: ["Cannot encrypt empty Data"]) - ) + additional: ["Cannot encrypt empty Data"] + )) } return defaultCryptor.encrypt(data: data).map { if defaultCryptor.id == LegacyCryptor.ID { @@ -71,13 +75,15 @@ public struct CryptoModule { /// /// - Parameters: /// - data: Data to decrypt - /// - Returns: A success, storing decrypted `Data` if operation succeeds. Otherwise, a failure storing `PubNubError` is returned + /// - Returns: + /// - **Success**: A decrypted `Data` object + /// - **Failure**: `PubNubError` describing the reason of failure public func decrypt(data: Data) -> Result { guard !data.isEmpty else { return .failure(PubNubError( .decryptionFailure, - additional: ["Cannot decrypt empty Data in \(String(describing: self))"]) - ) + additional: ["Cannot decrypt empty Data in \(String(describing: self))"] + )) } do { let header = try CryptorHeader.from(data: data) @@ -87,7 +93,7 @@ public struct CryptoModule { .unknownCryptorFailure, additional: [ "Could not find matching Cryptor for \(header.cryptorId()) while decrypting Data. " + - "Ensure the corresponding instance is registered in \(String(describing: Self.self))" + "Ensure the corresponding instance is registered in \(String(describing: Self.self))" ] )) } @@ -115,8 +121,8 @@ public struct CryptoModule { if $0.isEmpty { return .failure(PubNubError( .decryptionFailure, - additional: ["Decrypting resulted with empty Data"]) - ) + additional: ["Decrypting resulted with empty Data"] + )) } return .success($0) } @@ -129,8 +135,8 @@ public struct CryptoModule { return .failure(PubNubError( .decryptionFailure, underlying: error, - additional: ["Cannot decrypt InputStream"]) - ) + additional: ["Cannot decrypt InputStream"] + )) } } @@ -139,7 +145,9 @@ public struct CryptoModule { /// - Parameters: /// - stream: Stream to encrypt /// - contentLength: Content length of encoded stream - /// - Returns: A success, storing an `InputStream` value if operation succeeds. Otherwise, a failure storing `PubNubError` is returned + /// - Returns: + /// - **Success**: An `InputStream` value + /// - **Failure**: `PubNubError` describing the reason of failure public func encrypt(stream: InputStream, contentLength: Int) -> Result { guard contentLength > 0 else { return .failure(PubNubError( @@ -179,7 +187,9 @@ public struct CryptoModule { /// - stream: Stream to decrypt /// - contentLength: Content length of encrypted stream /// - to: URL where the stream should be decrypted to - /// - Returns: A success, storing a decrypted `InputStream` value if operation succeeds. Otherwise, a failure storing `PubNubError` is returned + /// - Returns: + /// - **Success**: A decrypted `InputStream` object + /// - **Failure**: `PubNubError` describing the reason of failure @discardableResult public func decrypt( stream: InputStream, @@ -203,7 +213,7 @@ public struct CryptoModule { .unknownCryptorFailure, additional: [ "Could not find matching Cryptor for \(readHeaderResp.header.cryptorId()) while decrypting InputStream. " + - "Ensure the corresponding instance is registered in \(String(describing: Self.self))" + "Ensure the corresponding instance is registered in \(String(describing: Self.self))" ] )) } @@ -218,8 +228,8 @@ public struct CryptoModule { if outputPath.sizeOf == 0 { return .failure(PubNubError( .decryptionFailure, - additional: ["Decrypting resulted with an empty File"]) - ) + additional: ["Decrypting resulted with an empty File"] + )) } return .success($0) } @@ -237,7 +247,7 @@ public struct CryptoModule { } } - private func cryptor(matching header: CryptorHeader) -> Cryptor? { + private func cryptor(matching header: CryptorHeader) -> (any Cryptor)? { header.cryptorId() == defaultCryptor.id ? defaultCryptor : cryptors.first(where: { $0.id == header.cryptorId() }) @@ -246,7 +256,6 @@ public struct CryptoModule { /// Convenience methods for creating `CryptoModule` public extension CryptoModule { - /// Returns **recommended** `CryptoModule` for encryption/decryption /// /// - Parameters: @@ -279,7 +288,9 @@ extension CryptoModule: Equatable { extension CryptoModule: Hashable { public func hash(into hasher: inout Hasher) { - hasher.combine(cryptors.map { $0.id }) + for cryptor in cryptors { + hasher.combine(cryptor) + } } } @@ -289,7 +300,7 @@ extension CryptoModule: CustomStringConvertible { } } -internal extension CryptoModule { +extension CryptoModule { func encrypt(string: String) -> Result { guard let data = string.data(using: .utf8) else { return .failure(PubNubError( @@ -309,8 +320,8 @@ internal extension CryptoModule { } else { return .failure(PubNubError( .decryptionFailure, - additional: ["Cannot create String from provided Data"]) - ) + additional: ["Cannot create String from provided Data"] + )) } } } diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift index 5b935c32..a5b067ac 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/AESCBCCryptor.swift @@ -147,3 +147,10 @@ public struct AESCBCCryptor: Cryptor { } } } + +extension AESCBCCryptor: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(key) + hasher.combine(id) + } +} diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift index c3a0d533..e8c1d983 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/Cryptor.swift @@ -33,7 +33,7 @@ public struct EncryptedStreamData { public typealias CryptorId = [UInt8] /// Protocol for all types that encapsulate concrete encryption/decryption operations -public protocol Cryptor { +public protocol Cryptor: Hashable { /// Unique 4-byte identifier across all `Cryptor` /// /// - Important: `[0x41, 0x43, 0x52, 0x48]` and `[0x00, 0x00, 0x00, 0x00]` values are reserved diff --git a/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift b/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift index 28f324ce..af0fa871 100644 --- a/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift +++ b/Sources/PubNub/Helpers/Crypto/Cryptors/LegacyCryptor.swift @@ -171,3 +171,11 @@ public struct LegacyCryptor: Cryptor { } } } + +extension LegacyCryptor: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(key) + hasher.combine(withRandomIV) + hasher.combine(LegacyCryptor.ID) + } +} diff --git a/Sources/PubNub/PubNub.swift b/Sources/PubNub/PubNub.swift index 32f2e541..98a9350b 100644 --- a/Sources/PubNub/PubNub.swift +++ b/Sources/PubNub/PubNub.swift @@ -1381,9 +1381,9 @@ public extension PubNub { // MARK: - Crypto extension PubNub { - /// Encrypt some `Data` using the configuration `CryptoModule` value + /// 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 Crypto Error + /// - Returns: A `Result` containing either the encryped `Data` (mapped to Base64-encoded data) or the `CryptoError` public func encrypt(message: String) -> Result { guard let cryptoModule = configuration.cryptoModule else { PubNub.log.error(ErrorDescription.missingCryptoKey) @@ -1400,9 +1400,9 @@ extension PubNub { } } - /// Decrypt some `Data` using the configuration CryptoModule value + /// 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 Crypto Error + /// - Returns: A `Result` containing either the decrypted plain text message or the `CryptoError` public func decrypt(data: Data) -> Result { guard let cryptoModule = configuration.cryptoModule else { PubNub.log.error(ErrorDescription.missingCryptoKey) diff --git a/Sources/PubNub/Subscription/SubscriptionSession.swift b/Sources/PubNub/Subscription/SubscriptionSession.swift index 05fc10d2..cc356fd8 100644 --- a/Sources/PubNub/Subscription/SubscriptionSession.swift +++ b/Sources/PubNub/Subscription/SubscriptionSession.swift @@ -55,13 +55,11 @@ public class SubscriptionSession: EventEmitter, StatusEmitter { } } - private lazy var globalEventsListener: BaseSubscriptionListenerAdapter = { - BaseSubscriptionListenerAdapter( - receiver: self, - uuid: uuid, - queue: queue - ) - }() + private lazy var globalEventsListener: BaseSubscriptionListenerAdapter = .init( + receiver: self, + uuid: uuid, + queue: queue + ) private lazy var globalStatusListener: BaseSubscriptionListener = { // Creates legacy listener under the hood to capture status changes @@ -80,14 +78,14 @@ public class SubscriptionSession: EventEmitter, StatusEmitter { private var globalGroupSubscriptions: [String: Subscription] = [:] private let strategy: any SubscriptionSessionStrategy - internal init( + init( strategy: any SubscriptionSessionStrategy, eventsQueue queue: DispatchQueue = .main ) { self.strategy = strategy self.queue = queue - self.add(globalEventsListener) - self.add(globalStatusListener) + add(globalEventsListener) + add(globalStatusListener) } /// Names of all subscribed channels @@ -121,20 +119,19 @@ public class SubscriptionSession: EventEmitter, StatusEmitter { /// - 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. - /// - setting: The object containing the state for the channel(s). public func subscribe( to channels: [String], and groups: [String] = [], at cursor: SubscribeCursor? = nil, withPresence: Bool = false ) { - let channelSubscriptions = channels.map { + let channelSubscriptions = channels.compactMap { channel($0).subscription( queue: queue, options: withPresence ? ReceivePresenceEvents() : SubscriptionOptions.empty() ) } - let channelGroupSubscriptions = groups.map { + let channelGroupSubscriptions = groups.compactMap { channelGroup($0).subscription( queue: queue, options: withPresence ? ReceivePresenceEvents() : SubscriptionOptions.empty() @@ -145,12 +142,12 @@ public class SubscriptionSession: EventEmitter, StatusEmitter { and: channelGroupSubscriptions, at: cursor?.timetoken ) - channelSubscriptions.forEach { subscription in + for subscription in channelSubscriptions { subscription.subscriptionNames.flatMap { $0 }.forEach { globalChannelSubscriptions[$0] = subscription } } - channelGroupSubscriptions.forEach { subscription in + for subscription in channelGroupSubscriptions { subscription.subscriptionNames.flatMap { $0 }.forEach { globalGroupSubscriptions[$0] = subscription } @@ -246,10 +243,10 @@ extension SubscriptionSession: SubscribeReceiver { let extractingChannelsRes = retrieveItemsToSubscribe(from: channels) let extractingGroupsRes = retrieveItemsToSubscribe(from: groups) - channels.forEach { channelSubscription in + for channelSubscription in channels { registerAdapter(channelSubscription.adapter) } - groups.forEach { groupSubscription in + for groupSubscription in groups { registerAdapter(groupSubscription.adapter) } strategy.subscribe( @@ -297,18 +294,16 @@ extension SubscriptionSession: SubscribeReceiver { ) { let extractingChannelsRes = extractItemsToUnsubscribe( from: channels, - type: .channel, presenceItemsOnly: presenceOnly ) let extractingGroupsRes = extractItemsToUnsubscribe( from: channelGroups, - type: .channelGroup, presenceItemsOnly: presenceOnly ) - channels.forEach { channelSubscription in + for channelSubscription in channels { remove(channelSubscription.adapter) } - channelGroups.forEach { channelGroupSubscription in + for channelGroupSubscription in channelGroups { remove(channelGroupSubscription.adapter) } strategy.unsubscribeFrom( @@ -319,48 +314,63 @@ extension SubscriptionSession: SubscribeReceiver { ) } - private func subscriptionCount(for name: String, type: SubscribableType) -> Int { - subscriptionTopology[type]?.filter { $0 == name }.count ?? 0 + // Returns an array of subscriptions that subscribe to at least one name in common with the given Subscription + func matchingSubscriptions(for subscription: Subscription, presenceOnly: Bool) -> [SubscribeMessagesReceiver] { + let allSubscriptions = strategy.listeners.compactMap { + $0 as? BaseSubscriptionListenerAdapter + } + let namesToFind = subscription.subscriptionNames.filter { + presenceOnly ? $0.isPresenceChannelName : true + } + + return allSubscriptions.filter { + $0.uuid != subscription.uuid && $0.uuid != globalEventsListener.uuid + }.compactMap { + $0.receiver + }.filter { + ($0.subscriptionTopology[subscription.subscriptionType] ?? [String]()).contains { + namesToFind.contains($0) + } + } } // Creates the final list of Presence channels/channel groups and main channels/channel groups // the user should unsubscribe from according to the following rules: // - // 1. Unsubscribes from the main channel happen if: + // 1. Unsubscribing from the main channel happens if: // * There are no references to its Presence equivalent from other subscriptions // * There are no references to the main channel from other subscriptions // 2. Unsubscribing from the Presence channel happens if: // * There are no references to it from other subscriptions private func extractItemsToUnsubscribe( from subscriptions: [Subscription], - type: SubscribableType, presenceItemsOnly: Bool ) -> UnsubscribeRetrievalRes { - let presenceItems = Set(subscriptions.flatMap { + let presenceItems = Set(subscriptions.filter { + matchingSubscriptions(for: $0, presenceOnly: true).isEmpty + }.flatMap { $0.subscriptionNames }).filter { $0.isPresenceChannelName }.map { PubNubChannel(channel: $0) - }.filter { - subscriptionCount(for: $0.presenceId, type: type) <= 1 } - let channels = presenceItemsOnly ? [] : Set(subscriptions.flatMap { + let channels = presenceItemsOnly ? [] : Set(subscriptions.filter { + matchingSubscriptions( + for: $0, + presenceOnly: false + ).isEmpty && + matchingSubscriptions( + for: $0, + presenceOnly: true + ).isEmpty + }.flatMap { $0.subscriptionNames }).symmetricDifference(presenceItems.map { $0.presenceId }).map { PubNubChannel(id: $0, withPresence: false) - }.filter { - subscriptionCount( - for: $0.presenceId, - type: type - ) <= 1 && - subscriptionCount( - for: $0.id, - type: type - ) <= 1 } return UnsubscribeRetrievalRes( @@ -370,18 +380,6 @@ extension SubscriptionSession: SubscribeReceiver { } } -fileprivate extension WeakSet where Element == BaseSubscriptionListener { - func subscriptions(excluding uuid: UUID? = nil) -> [BaseSubscriptionListenerAdapter] { - compactMap { - if let listener = $0 as? BaseSubscriptionListenerAdapter { - return listener.uuid != uuid ? listener : nil - } else { - return nil - } - } - } -} - // MARK: - EntityCreator extension SubscriptionSession: EntityCreator { @@ -447,18 +445,8 @@ extension SubscriptionSession: Hashable, CustomStringConvertible { // MARK: - SubscribeMessagePayloadReceiver extension SubscriptionSession: SubscribeMessagesReceiver { - var subscriptionTopology: [SubscribableType : [String]] { - var result: [SubscribableType: [String]] = [:] - result[.channel] = [] - result[.channelGroup] = [] - - return strategy.listeners.subscriptions( - excluding: globalEventsListener.uuid - ).reduce(into: result) { res, current in - let currentRes = current.receiver?.subscriptionTopology ?? [:] - res[.channel]?.append(contentsOf: currentRes[.channel] ?? []) - res[.channelGroup]?.append(contentsOf: currentRes[.channelGroup] ?? []) - } + var subscriptionTopology: [SubscribableType: [String]] { + [.channel: subscribedChannels, .channelGroup: subscribedChannelGroups] } func onPayloadsReceived(payloads: [SubscribeMessagePayload]) -> [PubNubEvent] { diff --git a/Tests/PubNubContractTest/Steps/CryptorModule/PubNubCryptoModuleContractTestSteps.swift b/Tests/PubNubContractTest/Steps/CryptorModule/PubNubCryptoModuleContractTestSteps.swift index 626dc24f..7d45ad30 100644 --- a/Tests/PubNubContractTest/Steps/CryptorModule/PubNubCryptoModuleContractTestSteps.swift +++ b/Tests/PubNubContractTest/Steps/CryptorModule/PubNubCryptoModuleContractTestSteps.swift @@ -186,7 +186,7 @@ fileprivate extension PubNubCryptoModuleContractTestSteps { ) } - func createCryptor(for stringIdentifier: String) -> Cryptor { + func createCryptor(for stringIdentifier: String) -> any Cryptor { if stringIdentifier == "acrh" { return AESCBCCryptor(key: self.cipherKey) } else { diff --git a/Tests/PubNubTests/Events/New/SubscriptionTests.swift b/Tests/PubNubTests/Events/New/SubscriptionTests.swift index 8e2a4deb..895d198f 100644 --- a/Tests/PubNubTests/Events/New/SubscriptionTests.swift +++ b/Tests/PubNubTests/Events/New/SubscriptionTests.swift @@ -8,8 +8,8 @@ // LICENSE file in the root directory of this source tree. // -import XCTest @testable import PubNub +import XCTest class SubscriptionTests: XCTestCase { private let pubnub = PubNub( @@ -24,27 +24,27 @@ class SubscriptionTests: XCTestCase { let messagesExpectation = XCTestExpectation(description: "Message") messagesExpectation.assertForOverFulfill = true messagesExpectation.expectedFulfillmentCount = 1 - + let signalExpectation = XCTestExpectation(description: "Signal") signalExpectation.assertForOverFulfill = true signalExpectation.expectedFulfillmentCount = 1 - + let messageAction = XCTestExpectation(description: "Message Action") messageAction.assertForOverFulfill = true messageAction.expectedFulfillmentCount = 1 - + let presenceChangeExpectation = XCTestExpectation(description: "Presence") presenceChangeExpectation.assertForOverFulfill = true presenceChangeExpectation.expectedFulfillmentCount = 1 - + let appContextExpectation = XCTestExpectation(description: "App Context") appContextExpectation.assertForOverFulfill = true appContextExpectation.expectedFulfillmentCount = 1 - + let fileExpectation = XCTestExpectation(description: "File") fileExpectation.assertForOverFulfill = true fileExpectation.expectedFulfillmentCount = 1 - + let allEventsExpectation = XCTestExpectation(description: "All Events") allEventsExpectation.assertForOverFulfill = true allEventsExpectation.expectedFulfillmentCount = 1 @@ -102,28 +102,28 @@ class SubscriptionTests: XCTestCase { let messagesExpectation = XCTestExpectation(description: "Message") messagesExpectation.isInverted = true messagesExpectation.assertForOverFulfill = true - + let signalExpectation = XCTestExpectation(description: "Signal") signalExpectation.isInverted = true signalExpectation.assertForOverFulfill = true - + let messageAction = XCTestExpectation(description: "Message Action") messageAction.isInverted = true messageAction.assertForOverFulfill = true - + let presenceChangeExpectation = XCTestExpectation(description: "Presence") presenceChangeExpectation.isInverted = true presenceChangeExpectation.assertForOverFulfill = true presenceChangeExpectation.expectedFulfillmentCount = 1 - + let appContextExpectation = XCTestExpectation(description: "App Context") appContextExpectation.isInverted = true appContextExpectation.assertForOverFulfill = true - + let fileExpectation = XCTestExpectation(description: "File") fileExpectation.isInverted = true fileExpectation.assertForOverFulfill = true - + let allEventsExpectation = XCTestExpectation(description: "All Events") allEventsExpectation.isInverted = true allEventsExpectation.assertForOverFulfill = true @@ -185,7 +185,7 @@ class SubscriptionTests: XCTestCase { let channel = pubnub.channel("channel.item.*") let subscription = channel.subscription() - subscription.onMessage = { message in + subscription.onMessage = { _ in expectation.fulfill() } subscription.onPayloadsReceived( diff --git a/Tests/PubNubTests/Integration/PublishEndpointIntegrationTests.swift b/Tests/PubNubTests/Integration/PublishEndpointIntegrationTests.swift index 130372bc..5fc0c138 100644 --- a/Tests/PubNubTests/Integration/PublishEndpointIntegrationTests.swift +++ b/Tests/PubNubTests/Integration/PublishEndpointIntegrationTests.swift @@ -16,7 +16,6 @@ class PublishEndpointIntegrationTests: XCTestCase { func testPublishEndpoint() { let publishExpect = expectation(description: "Publish Response") - // Instantiate PubNub let configuration = PubNubConfiguration(from: testsBundle) let client = PubNub(configuration: configuration) @@ -37,19 +36,19 @@ class PublishEndpointIntegrationTests: XCTestCase { func testSignalTooLong() { let publishExpect = expectation(description: "Publish Response") - // Instantiate PubNub let configuration = PubNubConfiguration(from: testsBundle) let client = PubNub(configuration: configuration) - client.signal(channel: "SwiftITest", - message: ["$": "35.75", "HI": "b62", "t": "BO"]) { result in + client.signal( + channel: "SwiftITest", + message: ["$": "35.75", "HI": "b62", "t": "BO"] + ) { result in switch result { case .success: XCTFail("Publish should fail") case let .failure(error): - XCTAssertEqual(error.pubNubError?.reason, - PubNubError.Reason.messageTooLong) + XCTAssertEqual(error.pubNubError?.reason, PubNubError.Reason.messageTooLong) } publishExpect.fulfill() } @@ -59,15 +58,16 @@ class PublishEndpointIntegrationTests: XCTestCase { func testCompressedPublishEndpoint() { let compressedPublishExpect = expectation(description: "Compressed Publish Response") - // Instantiate PubNub let configuration = PubNubConfiguration(from: testsBundle) let client = PubNub(configuration: configuration) // Publish a simple message to the demo_tutorial channel - client.publish(channel: "SwiftITest", - message: "TestCompressedPublish", - shouldCompress: true) { result in + client.publish( + channel: "SwiftITest", + message: "TestCompressedPublish", + shouldCompress: true + ) { result in switch result { case .success: break @@ -82,7 +82,6 @@ class PublishEndpointIntegrationTests: XCTestCase { func testFireEndpoint() { let fireExpect = expectation(description: "Fire Response") - // Instantiate PubNub let configuration = PubNubConfiguration(from: testsBundle) let client = PubNub(configuration: configuration) @@ -103,7 +102,6 @@ class PublishEndpointIntegrationTests: XCTestCase { func testSignalEndpoint() { let signalExpect = expectation(description: "Signal Response") - // Instantiate PubNub let configuration = PubNubConfiguration(from: testsBundle) let client = PubNub(configuration: configuration) @@ -124,7 +122,6 @@ class PublishEndpointIntegrationTests: XCTestCase { func testPushblishEscapedString() { let message = "{\"text\": \"bob\", \"duckName\": \"swiftduck\"}" - let publishExpect = expectation(description: "Publish Response") // Instantiate PubNub @@ -137,8 +134,7 @@ class PublishEndpointIntegrationTests: XCTestCase { case .success: XCTFail("Publish should fail") case let .failure(error): - XCTAssertEqual(error.pubNubError?.reason, - PubNubError.Reason.requestContainedInvalidJSON) + XCTAssertEqual(error.pubNubError?.reason, PubNubError.Reason.requestContainedInvalidJSON) } publishExpect.fulfill() } @@ -181,4 +177,64 @@ class PublishEndpointIntegrationTests: XCTestCase { wait(for: [publishExpect], timeout: 10.0) } + + func testPublish_WithCryptoModulesFromDifferentClients() { + let firstClient = PubNub(configuration: PubNubConfiguration( + publishKey: PubNubConfiguration(from: testsBundle).publishKey, + subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, + userId: PubNubConfiguration(from: testsBundle).userId, + cryptoModule: CryptoModule.aesCbcCryptoModule(with: "someKey") + )) + let secondClient = PubNub(configuration: PubNubConfiguration( + publishKey: PubNubConfiguration(from: testsBundle).publishKey, + subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, + userId: PubNubConfiguration(from: testsBundle).userId, + cryptoModule: CryptoModule.aesCbcCryptoModule(with: "anotherKey") + )) + + let channelForFistClient = "ChannelA" + let channelForSecondClient = "ChannelB" + + let publishExpect = expectation(description: "Publish Response") + publishExpect.assertForOverFulfill = true + publishExpect.expectedFulfillmentCount = 2 + + let subscribeExpect = expectation(description: "Subscribe Response") + subscribeExpect.assertForOverFulfill = true + subscribeExpect.assertForOverFulfill = true + + for client in [firstClient, secondClient] { + client.onConnectionStateChange = { [unowned client] newStatus in + if newStatus == .connected { + client.publish( + channel: client === firstClient ? channelForFistClient : channelForSecondClient, + message: "This is a message" + ) { result in + switch result { + case .success: + publishExpect.fulfill() + case let .failure(error): + XCTFail("Unexpected failure: \(error)") + } + } + } + } + } + + let subscription = firstClient.channel(channelForFistClient).subscription() + let subscriptionFromSecondClient = secondClient.channel(channelForSecondClient).subscription() + + subscription.onMessage = { message in + XCTAssertEqual(message.payload.stringOptional, "This is a message") + subscribeExpect.fulfill() + } + subscriptionFromSecondClient.onMessage = { message in + XCTAssertEqual(message.payload.stringOptional, "This is a message") + subscribeExpect.fulfill() + } + subscription.subscribe() + subscriptionFromSecondClient.subscribe() + + wait(for: [publishExpect, subscribeExpect], timeout: 10.0) + } } diff --git a/Tests/PubNubTests/Integration/SubscriptionIntegrationTests.swift b/Tests/PubNubTests/Integration/SubscriptionIntegrationTests.swift index b0ab938c..0f456c08 100644 --- a/Tests/PubNubTests/Integration/SubscriptionIntegrationTests.swift +++ b/Tests/PubNubTests/Integration/SubscriptionIntegrationTests.swift @@ -50,7 +50,7 @@ class SubscriptionIntegrationTests: XCTestCase { connectingExpect.fulfill() case .disconnectedUnexpectedly: disconnectedExpect.fulfill() - case .connectionError(_): + case .connectionError: disconnectedExpect.fulfill() default: XCTFail("Only should emit these two states") @@ -74,13 +74,15 @@ class SubscriptionIntegrationTests: XCTestCase { // swiftlint:disable:next function_body_length cyclomatic_complexity func testUnsubscribeResubscribe() { let configurationFromBundle = PubNubConfiguration( - from: testsBundle + publishKey: PubNubConfiguration(from: testsBundle).publishKey, + subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, + userId: PubNubConfiguration(from: testsBundle).userId, + enableEventEngine: false ) let configWithEventEngineEnabled = PubNubConfiguration( publishKey: configurationFromBundle.publishKey, subscribeKey: configurationFromBundle.subscribeKey, - userId: configurationFromBundle.userId, - enableEventEngine: true + userId: configurationFromBundle.userId ) for config in [configurationFromBundle, configWithEventEngineEnabled] { @@ -153,13 +155,15 @@ class SubscriptionIntegrationTests: XCTestCase { func test_MixedSubscriptions() { let configurationFromBundle = PubNubConfiguration( - from: testsBundle + publishKey: PubNubConfiguration(from: testsBundle).publishKey, + subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, + userId: PubNubConfiguration(from: testsBundle).userId, + enableEventEngine: false ) let configWithEventEngineEnabled = PubNubConfiguration( publishKey: configurationFromBundle.publishKey, subscribeKey: configurationFromBundle.subscribeKey, - userId: configurationFromBundle.userId, - enableEventEngine: true + userId: configurationFromBundle.userId ) for config in [configurationFromBundle, configWithEventEngineEnabled] { @@ -230,13 +234,15 @@ class SubscriptionIntegrationTests: XCTestCase { func test_GlobalSubscription() { let configurationFromBundle = PubNubConfiguration( - from: testsBundle + publishKey: PubNubConfiguration(from: testsBundle).publishKey, + subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, + userId: PubNubConfiguration(from: testsBundle).userId, + enableEventEngine: false ) let configWithEventEngineEnabled = PubNubConfiguration( publishKey: configurationFromBundle.publishKey, subscribeKey: configurationFromBundle.subscribeKey, - userId: configurationFromBundle.userId, - enableEventEngine: true + userId: configurationFromBundle.userId ) for config in [configurationFromBundle, configWithEventEngineEnabled] { @@ -279,7 +285,7 @@ class SubscriptionIntegrationTests: XCTestCase { } } - func test_SimultaneousSubscriptions() { + func test_SimultaneousSubscriptionToDifferentChannels() { let expectation = XCTestExpectation(description: "Expectation") expectation.assertForOverFulfill = true expectation.expectedFulfillmentCount = 3 @@ -287,15 +293,12 @@ class SubscriptionIntegrationTests: XCTestCase { let publishExpectation = XCTestExpectation(description: "Publish") publishExpectation.assertForOverFulfill = true publishExpectation.expectedFulfillmentCount = 1 - - let configWithEventEngineEnabled = PubNubConfiguration( + + let pubnub = PubNub(configuration: PubNubConfiguration( publishKey: PubNubConfiguration(from: testsBundle).publishKey, subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, - userId: PubNubConfiguration(from: testsBundle).userId, - enableEventEngine: true - ) - - let pubnub = PubNub(configuration: configWithEventEngineEnabled) + userId: PubNubConfiguration(from: testsBundle).userId + )) let timetoken = Timetoken(Int(Date().timeIntervalSince1970 * 10000000)) pubnub.publish(channel: testChannel, message: "Message", completion: { [unowned pubnub, unowned self] _ in @@ -303,12 +306,13 @@ class SubscriptionIntegrationTests: XCTestCase { publishExpectation.fulfill() }) }) + wait(for: [publishExpectation], timeout: 1.5) let anotherChannel = testChannel.appending("2") let listener = SubscriptionListener() - listener.didReceiveMessage = { message in + listener.didReceiveMessage = { _ in expectation.fulfill() } @@ -320,4 +324,61 @@ class SubscriptionIntegrationTests: XCTestCase { defer { pubnub.disconnect() } wait(for: [expectation], timeout: 10) } + + func test_SimultaneousSubscriptionsToTheSameChannel() { + let expectation = XCTestExpectation(description: "Test Simultaneous Subscriptions") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 2 + + let pubnub = PubNub(configuration: PubNubConfiguration( + publishKey: PubNubConfiguration(from: testsBundle).publishKey, + subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, + userId: PubNubConfiguration(from: testsBundle).userId + )) + + pubnub.onConnectionStateChange = { newStatus in + switch newStatus { + case .connecting: + expectation.fulfill() + case .connected: + expectation.fulfill() + default: + XCTFail("Unexpected connection status") + } + } + + pubnub.subscribe(to: ["channel"]) + pubnub.subscribe(to: ["channel"]) + + XCTAssertEqual(pubnub.subscribedChannels, ["channel"]) + wait(for: [expectation], timeout: 5.0) + } + + func test_SimultaneousSubscriptionsToTheSameChannelWithTimetoken() { + let expectation = XCTestExpectation(description: "Test Simultaneous Subscriptions With Timetoken") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 3 + + let pubnub = PubNub(configuration: PubNubConfiguration( + publishKey: PubNubConfiguration(from: testsBundle).publishKey, + subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, + userId: PubNubConfiguration(from: testsBundle).userId + )) + + pubnub.onConnectionStateChange = { newStatus in + switch newStatus { + case .connecting: + expectation.fulfill() + case .connected: + expectation.fulfill() + default: + XCTFail("Unexpected connection status") + } + } + + pubnub.subscribe(to: ["channel"]) + pubnub.subscribe(to: ["channel"], at: Timetoken(Int(Date().timeIntervalSince1970 * 10000000))) + + wait(for: [expectation], timeout: 5.0) + } } diff --git a/Tests/PubNubTests/PubNubConfigurationTests.swift b/Tests/PubNubTests/PubNubConfigurationTests.swift index fe7de13a..2424ade5 100644 --- a/Tests/PubNubTests/PubNubConfigurationTests.swift +++ b/Tests/PubNubTests/PubNubConfigurationTests.swift @@ -55,11 +55,30 @@ class PubNubConfigurationTests: XCTestCase { } func testInit_RawValues() { - let config = PubNubConfiguration(publishKey: publishKeyValue, - subscribeKey: subscribeKeyValue, - userId: UUID().uuidString) + let config = PubNubConfiguration( + publishKey: publishKeyValue, + subscribeKey: subscribeKeyValue, + userId: UUID().uuidString + ) XCTAssertEqual(config.publishKey, publishKeyValue) XCTAssertEqual(config.subscribeKey, subscribeKeyValue) } + + func testConfigurations_DifferentCryptoModules() { + let firstConfig = PubNubConfiguration( + publishKey: PubNubConfiguration(from: testsBundle).publishKey, + subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, + userId: PubNubConfiguration(from: testsBundle).userId, + cryptoModule: CryptoModule.aesCbcCryptoModule(with: "someKey") + ) + let secondConfig = PubNubConfiguration( + publishKey: PubNubConfiguration(from: testsBundle).publishKey, + subscribeKey: PubNubConfiguration(from: testsBundle).subscribeKey, + userId: PubNubConfiguration(from: testsBundle).userId, + cryptoModule: CryptoModule.aesCbcCryptoModule(with: "anotherKey") + ) + + XCTAssertNotEqual(firstConfig.hashValue, secondConfig.hashValue) + } }