diff --git a/.pubnub.yml b/.pubnub.yml index a8fd2231..5c6f1a18 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,9 +1,14 @@ --- name: swift scm: github.com/pubnub/swift -version: "8.0.1" +version: "8.1.0" schema: 1 changelog: + - date: 2024-11-18 + version: 8.1.0 + changes: + - type: feature + text: "Add custom message type support for the following APIs: publish, signal, share file, subscribe, and history." - date: 2024-10-17 version: 8.0.1 changes: @@ -591,16 +596,16 @@ sdks: - distribution-type: source distribution-repository: GitHub release package-name: PubNub - location: https://github.com/pubnub/swift/archive/refs/tags/8.0.1.zip + location: https://github.com/pubnub/swift/archive/refs/tags/8.1.0.zip supported-platforms: supported-operating-systems: macOS: runtime-version: - Swift 5.x minimum-os-version: - - OS X 10.11 + - macOS 10.15 maximum-os-version: - - macOS 11.4 + - macOS 15.0.1 target-architecture: - arm64 - x86_64 @@ -608,12 +613,11 @@ sdks: runtime-version: - Swift 5.x minimum-os-version: - - iOS 9.0 + - iOS 12.0 maximum-os-version: - - iOS 14.6 + - iOS 18.0.1 target-architecture: - arm64 - - armv7 target-devices: - iPhone - iPad @@ -621,9 +625,9 @@ sdks: runtime-version: - Swift 5.x minimum-os-version: - - tvOS 10.0 + - tvOS 12.0 maximum-os-version: - - tvOS 14.6 + - tvOS 18.0 target-architecture: - arm64 target-devices: @@ -632,10 +636,11 @@ sdks: runtime-version: - Swift 5.x minimum-os-version: - - watchOS 2.0 + - watchOS 4.0 maximum-os-version: - - watchOS 7.5 + - watchOS 12.0 target-architecture: + - arm64 - armv7k - arm64_32 target-devices: @@ -659,9 +664,9 @@ sdks: runtime-version: - Swift 5.x minimum-os-version: - - OS X 10.11 + - macOS 10.15 maximum-os-version: - - macOS 11.4 + - macOS 15.0.1 target-architecture: - arm64 - x86_64 @@ -669,12 +674,11 @@ sdks: runtime-version: - Swift 5.x minimum-os-version: - - iOS 9.0 + - iOS 12.0 maximum-os-version: - - iOS 14.6 + - iOS 18.0.1 target-architecture: - arm64 - - armv7 target-devices: - iPhone - iPad @@ -682,9 +686,9 @@ sdks: runtime-version: - Swift 5.x minimum-os-version: - - tvOS 10.0 + - tvOS 12.0 maximum-os-version: - - tvOS 14.6 + - tvOS 18.0 target-architecture: - arm64 target-devices: @@ -693,12 +697,13 @@ sdks: runtime-version: - Swift 5.x minimum-os-version: - - watchOS 2.0 + - watchOS 4.0 maximum-os-version: - - watchOS 7.5 + - watchOS 12.0 target-architecture: - armv7k - arm64_32 + - arm64 target-devices: - Apple Watch - distribution-type: package @@ -711,9 +716,9 @@ sdks: runtime-version: - Swift 5.x minimum-os-version: - - OS X 10.11 + - macOS 10.15 maximum-os-version: - - macOS 11.4 + - macOS 15.0.1 target-architecture: - arm64 - x86_64 @@ -721,9 +726,9 @@ sdks: runtime-version: - Swift 5.x minimum-os-version: - - iOS 9.0 + - iOS 14.0 maximum-os-version: - - iOS 14.6 + - iOS 18.0.1 target-architecture: - arm64 - armv7 @@ -734,9 +739,9 @@ sdks: runtime-version: - Swift 5.x minimum-os-version: - - tvOS 10.0 + - tvOS 12.0 maximum-os-version: - - tvOS 14.6 + - tvOS 18.0 target-architecture: - arm64 target-devices: @@ -745,18 +750,19 @@ sdks: runtime-version: - Swift 5.x minimum-os-version: - - watchOS 2.0 + - watchOS 4.0 maximum-os-version: - - watchOS 7.5 + - watchOS 12.0 target-architecture: - armv7k - arm64_32 + - arm64 target-devices: - Apple Watch supported-platforms: - version: PubNub Swift SDK platforms: - - iOS 9.0 or higher - - macOS 10.11 or higher - - tvOS 9.0 or higher - - watchOS 2.0 or higher + - iOS 14.0 or higher + - macOS 10.15 or higher + - tvOS 12.0 or higher + - watchOS 4.0 or higher diff --git a/PubNub.xcodeproj/project.pbxproj b/PubNub.xcodeproj/project.pbxproj index 5a1b7a0a..bee3ba8c 100644 --- a/PubNub.xcodeproj/project.pbxproj +++ b/PubNub.xcodeproj/project.pbxproj @@ -3995,7 +3995,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 8.0.1; + MARKETING_VERSION = 8.1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -4046,7 +4046,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 8.0.1; + MARKETING_VERSION = 8.1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -4154,7 +4154,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 8.0.1; + MARKETING_VERSION = 8.1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -4207,7 +4207,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 8.0.1; + MARKETING_VERSION = 8.1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -4328,7 +4328,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 8.0.1; + MARKETING_VERSION = 8.1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -4380,7 +4380,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 8.0.1; + MARKETING_VERSION = 8.1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++17"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -4860,7 +4860,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 8.0.1; + MARKETING_VERSION = 8.1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; @@ -4903,7 +4903,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 8.0.1; + MARKETING_VERSION = 8.1.0; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++14"; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; diff --git a/PubNubSwift.podspec b/PubNubSwift.podspec index b96bf615..d32f5f39 100644 --- a/PubNubSwift.podspec +++ b/PubNubSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'PubNubSwift' - s.version = '8.0.1' + s.version = '8.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/Sources/PubNub/APIs/File+PubNub.swift b/Sources/PubNub/APIs/File+PubNub.swift index 17ce8ef1..d0c86733 100644 --- a/Sources/PubNub/APIs/File+PubNub.swift +++ b/Sources/PubNub/APIs/File+PubNub.swift @@ -80,6 +80,8 @@ public extension PubNub { struct PublishFileRequest { /// The optional message that will be include alongside the File information public var additionalMessage: JSONCodable? + /// A user-provided custom message type + public var customMessageType: String? /// If true the published message is stored in history. public var store: Bool? /// Set a per message time to live in storage. @@ -92,18 +94,21 @@ public extension PubNub { /// Default init /// - Parameters: /// - additionalMessage: The optional message that will be include alongside the File information - /// - store: If true the published message is stored in history. - /// - ttl: Set a per message time to live in storage. + /// - customMessageType: A user-provided custom message type + /// - store: If true the published message is stored in history + /// - ttl: Set a per message time to live in storage /// - meta: Additional metadata to publish alongside the file /// - customRequestConfig: Custom configuration overrides for this request public init( additionalMessage: JSONCodable? = nil, + customMessageType: String? = nil, store: Bool? = nil, ttl: Int? = nil, meta: JSONCodable? = nil, customRequestConfig: RequestConfiguration = RequestConfiguration() ) { self.additionalMessage = additionalMessage + self.customMessageType = customMessageType self.store = store self.ttl = ttl self.meta = meta @@ -222,7 +227,7 @@ public extension PubNub { let router = PublishRouter( .file( - message: fileMessage, + message: fileMessage, customMessageType: request.customMessageType, shouldStore: request.store, ttl: request.ttl, meta: request.meta?.codableValue ), configuration: configuration diff --git a/Sources/PubNub/Helpers/Constants.swift b/Sources/PubNub/Helpers/Constants.swift index a1830812..1f60a8f6 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 = "8.0.1" + static let pubnubSwiftSDKVersion: String = "8.1.0" static let appBundleId: String = { if let info = Bundle.main.infoDictionary, diff --git a/Sources/PubNub/Models/PubNubMessage.swift b/Sources/PubNub/Models/PubNubMessage.swift index dcf6c92c..9f1a9752 100644 --- a/Sources/PubNub/Models/PubNubMessage.swift +++ b/Sources/PubNub/Models/PubNubMessage.swift @@ -16,7 +16,6 @@ public enum PubNubMessageType: Int, Codable, Hashable { case object = 2 case messageAction = 3 case file = 4 - case unknown = 999 } @@ -38,6 +37,8 @@ public protocol PubNubMessage { var metadata: JSONCodable? { get set } /// The type of message that was received var messageType: PubNubMessageType { get set } + /// A user-provided custom message type + var customMessageType: String? { get set } /// An error (if any) occured while getting this message var error: PubNubError? { get set } @@ -76,6 +77,7 @@ public struct PubNubMessageBase: PubNubMessage, Codable, Hashable { public var subscription: String? public var published: Timetoken public var messageType: PubNubMessageType + public var customMessageType: String? public var error: PubNubError? var concretePayload: AnyJSON @@ -112,6 +114,7 @@ public struct PubNubMessageBase: PubNubMessage, Codable, Hashable { subscription: other.subscription, published: other.published, metadata: other.metadata?.codableValue, + customMessageType: other.customMessageType, messageType: other.messageType, error: other.error ) @@ -126,6 +129,7 @@ public struct PubNubMessageBase: PubNubMessage, Codable, Hashable { subscription: subscribe.subscription, published: subscribe.publishTimetoken.timetoken, metadata: subscribe.metadata, + customMessageType: subscribe.customMessageType, messageType: subscribe.messageType.asPubNubMessageType, error: subscribe.error ) @@ -146,7 +150,8 @@ public struct PubNubMessageBase: PubNubMessage, Codable, Hashable { subscription: nil, published: history.timetoken, metadata: history.meta, - messageType: history.messageType ?? .unknown, + customMessageType: history.customMessageType, + messageType: history.messageType?.asPubNubMessageType ?? .unknown, error: history.error ) } @@ -159,6 +164,7 @@ public struct PubNubMessageBase: PubNubMessage, Codable, Hashable { subscription: String?, published: Timetoken, metadata: AnyJSON?, + customMessageType: String? = nil, messageType: PubNubMessageType = .unknown, error: PubNubError? = nil ) { @@ -170,6 +176,7 @@ public struct PubNubMessageBase: PubNubMessage, Codable, Hashable { self.published = published self.concreteMetadata = metadata self.messageType = messageType + self.customMessageType = customMessageType self.error = error } @@ -184,6 +191,7 @@ public struct PubNubMessageBase: PubNubMessage, Codable, Hashable { try container.encode(self.published, forKey: .published) try container.encodeIfPresent(self.concreteMetadata, forKey: .concreteMetadata) try container.encode(self.messageType, forKey: .messageType) + try container.encode(self.customMessageType, forKey: .customMessageType) } enum CodingKeys: CodingKey { @@ -195,6 +203,7 @@ public struct PubNubMessageBase: PubNubMessage, Codable, Hashable { case published case concreteMetadata case messageType + case customMessageType } public init(from decoder: Decoder) throws { @@ -208,6 +217,7 @@ public struct PubNubMessageBase: PubNubMessage, Codable, Hashable { self.published = try container.decode(Timetoken.self, forKey: .published) self.concreteMetadata = try container.decodeIfPresent(AnyJSON.self, forKey: .concreteMetadata) self.messageType = try container.decode(PubNubMessageType.self, forKey: .messageType) + self.customMessageType = try container.decodeIfPresent(String.self, forKey: .customMessageType) } } diff --git a/Sources/PubNub/Networking/HTTPRouter.swift b/Sources/PubNub/Networking/HTTPRouter.swift index 7b70d043..c03c8b6d 100644 --- a/Sources/PubNub/Networking/HTTPRouter.swift +++ b/Sources/PubNub/Networking/HTTPRouter.swift @@ -104,6 +104,7 @@ enum QueryKey: String { case remove case add case type + case customMessageType = "custom_message_type" case start case end case channel @@ -111,6 +112,7 @@ enum QueryKey: String { case max case includeMeta = "include_meta" case includeMessageType = "include_message_type" + case includeCustomMessageType = "include_custom_message_type" case includeUUID = "include_uuid" case timetoken case channelsTimetoken diff --git a/Sources/PubNub/Networking/Routers/HistoryRouter.swift b/Sources/PubNub/Networking/Routers/HistoryRouter.swift index cabafc7c..4a5b7efb 100644 --- a/Sources/PubNub/Networking/Routers/HistoryRouter.swift +++ b/Sources/PubNub/Networking/Routers/HistoryRouter.swift @@ -17,11 +17,11 @@ struct HistoryRouter: HTTPRouter { enum Endpoint: CustomStringConvertible { case fetch( channels: [String], max: Int?, start: Timetoken?, end: Timetoken?, - includeMeta: Bool, includeMessageType: Bool, includeUUID: Bool + includeMeta: Bool, includeMessageType: Bool, includeCustomMessageType: Bool, includeUUID: Bool ) case fetchWithActions( channel: String, max: Int?, start: Timetoken?, end: Timetoken?, - includeMeta: Bool, includeMessageType: Bool, includeUUID: Bool + includeMeta: Bool, includeMessageType: Bool, includeCustomMessageType: Bool, includeUUID: Bool ) case delete(channel: String, start: Timetoken?, end: Timetoken?) case messageCounts(channels: [String], timetoken: Timetoken?, channelsTimetoken: [Timetoken]?) @@ -41,9 +41,9 @@ struct HistoryRouter: HTTPRouter { var firstChannel: String? { switch self { - case let .fetchWithActions(channel, _, _, _, _, _, _): + case let .fetchWithActions(channel, _, _, _, _, _, _, _): return channel - case let .fetch(channels, _, _, _, _, _, _): + case let .fetch(channels, _, _, _, _, _, _, _): return channels.first case let .delete(channel, _, _): return channel @@ -75,9 +75,9 @@ struct HistoryRouter: HTTPRouter { let path: String switch endpoint { - case let .fetchWithActions(channel, _, _, _, _, _, _): + case let .fetchWithActions(channel, _, _, _, _, _, _, _): path = "/v3/history-with-actions/sub-key/\(subscribeKey)/channel/\(channel)" - case let .fetch(channels, _, _, _, _, _, _): + case let .fetch(channels, _, _, _, _, _, _, _): path = "/v3/history/sub-key/\(subscribeKey)/channel/\(channels.csvString.urlEncodeSlash)" case let .delete(channel, _, _): path = "/v3/history/sub-key/\(subscribeKey)/channel/\(channel.urlEncodeSlash)" @@ -91,27 +91,28 @@ struct HistoryRouter: HTTPRouter { var query = defaultQueryItems switch endpoint { - case let .fetchWithActions(_, max, start, end, includeMeta, includeMessageType, includeUUID): + case let .fetchWithActions(_, max, start, end, includeMeta, includeMessageType, includeCustomMessageType, includeUUID): query.appendIfPresent(key: .max, value: max?.description) query.appendIfPresent(key: .start, value: start?.description) query.appendIfPresent(key: .end, value: end?.description) query.appendIfPresent(key: .includeMeta, value: includeMeta.description) query.appendIfPresent(key: .includeMessageType, value: includeMessageType.description) + query.appendIfPresent(key: .includeCustomMessageType, value: includeCustomMessageType.description) query.appendIfPresent(key: .includeUUID, value: includeUUID.description) - case let .fetch(_, max, start, end, includeMeta, includeMessageType, includeUUID): + case let .fetch(_, max, start, end, includeMeta, includeMessageType, includeCustomMessageType, includeUUID): query.appendIfPresent(key: .max, value: max?.description) query.appendIfPresent(key: .start, value: start?.description) query.appendIfPresent(key: .end, value: end?.description) query.appendIfPresent(key: .includeMeta, value: includeMeta.description) query.appendIfPresent(key: .includeMessageType, value: includeMessageType.description) + query.appendIfPresent(key: .includeCustomMessageType, value: includeCustomMessageType.description) query.appendIfPresent(key: .includeUUID, value: includeUUID.description) case let .delete(_, startTimetoken, endTimetoken): query.appendIfPresent(key: .start, value: startTimetoken?.description) query.appendIfPresent(key: .end, value: endTimetoken?.description) case let .messageCounts(_, timetoken, channelsTimetoken): query.appendIfPresent(key: .timetoken, value: timetoken?.description) - query.appendIfPresent(key: .channelsTimetoken, - value: channelsTimetoken?.map { $0.description }.csvString) + query.appendIfPresent(key: .channelsTimetoken, value: channelsTimetoken?.map { $0.description }.csvString) } return .success(query) @@ -129,9 +130,9 @@ struct HistoryRouter: HTTPRouter { // Validated var validationErrorDetail: String? { switch endpoint { - case let .fetchWithActions(channel, _, _, _, _, _, _): + case let .fetchWithActions(channel, _, _, _, _, _, _, _): return isInvalidForReason((channel.isEmpty, ErrorDescription.emptyChannelString)) - case let .fetch(channels, _, _, _, _, _, _): + case let .fetch(channels, _, _, _, _, _, _, _): return isInvalidForReason((channels.isEmpty, ErrorDescription.emptyChannelArray)) case let .delete(channel, _, _): return isInvalidForReason((channel.isEmpty, ErrorDescription.emptyChannelString)) @@ -152,16 +153,15 @@ struct MessageHistoryResponseDecoder: ResponseDecoder { func decode(response: EndpointResponse) -> Result, Error> { do { // Version3 - let payload = try Constant.jsonDecoder.decode(MessageHistoryResponse.self, from: response.payload) - let decodedResponse = EndpointResponse(router: response.router, - request: response.request, - response: response.response, - data: response.data, - payload: payload) - - // Attempt to decode message response - - return .success(decodedResponse) + return .success( + EndpointResponse( + router: response.router, + request: response.request, + response: response.response, + data: response.data, + payload: try Constant.jsonDecoder.decode(MessageHistoryResponse.self, from: response.payload) + ) + ) } catch { return .failure(PubNubError(.jsonDataDecodingFailure, response: response, error: error)) } @@ -191,6 +191,7 @@ struct MessageHistoryResponseDecoder: ResponseDecoder { meta: message.meta, uuid: message.uuid, messageType: message.messageType, + customMessageType: message.customMessageType, error: nil ) case .failure(let error): @@ -200,6 +201,7 @@ struct MessageHistoryResponseDecoder: ResponseDecoder { meta: message.meta, uuid: message.uuid, messageType: message.messageType, + customMessageType: message.customMessageType, error: error ) PubNub.log.warn("History message failed to decrypt due to \(error)") @@ -211,6 +213,7 @@ struct MessageHistoryResponseDecoder: ResponseDecoder { meta: message.meta, uuid: message.uuid, messageType: message.messageType, + customMessageType: message.customMessageType, error: PubNubError( .decryptionFailure, additional: ["Cannot decrypt message due to invalid Base-64 input"] @@ -223,15 +226,18 @@ struct MessageHistoryResponseDecoder: ResponseDecoder { } // Replace previous payload with decrypted one - let decryptedPayload = MessageHistoryResponse(status: response.payload.status, - error: response.payload.error, - errorMessage: response.payload.errorMessage, - channels: channels) - let decryptedResponse = EndpointResponse(router: response.router, - request: response.request, - response: response.response, - data: response.data, - payload: decryptedPayload) + let decryptedResponse = EndpointResponse( + router: response.router, + request: response.request, + response: response.response, + data: response.data, + payload: MessageHistoryResponse( + status: response.payload.status, + error: response.payload.error, + errorMessage: response.payload.errorMessage, + channels: channels + ) + ) return .success(decryptedResponse) } } @@ -299,12 +305,14 @@ struct MessageHistoryMessagePayload: Codable { typealias ActionType = String typealias ActionValue = String typealias RawMessageAction = [ActionType: [ActionValue: [MessageHistoryMessageAction]]] + typealias LegacyPubNubMessageType = SubscribeMessagePayload.Action let message: AnyJSON let timetoken: Timetoken let meta: AnyJSON? let uuid: String? - let messageType: PubNubMessageType? + let messageType: LegacyPubNubMessageType? + let customMessageType: String? let actions: RawMessageAction let error: PubNubError? @@ -313,7 +321,8 @@ struct MessageHistoryMessagePayload: Codable { timetoken: Timetoken = 0, meta: JSONCodable? = nil, uuid: String?, - messageType: PubNubMessageType?, + messageType: LegacyPubNubMessageType?, + customMessageType: String? = nil, actions: RawMessageAction = [:], error: PubNubError? ) { @@ -321,6 +330,7 @@ struct MessageHistoryMessagePayload: Codable { self.timetoken = timetoken self.uuid = uuid self.messageType = messageType + self.customMessageType = customMessageType self.meta = meta?.codableValue self.actions = actions self.error = error @@ -332,6 +342,7 @@ struct MessageHistoryMessagePayload: Codable { case meta case uuid case messageType = "message_type" + case customMessageType = "custom_message_type" case actions } @@ -341,9 +352,10 @@ struct MessageHistoryMessagePayload: Codable { message = try container.decode(AnyJSON.self, forKey: .message) meta = try container.decodeIfPresent(AnyJSON.self, forKey: .meta) uuid = try container.decodeIfPresent(String.self, forKey: .uuid) - messageType = try container.decodeIfPresent(PubNubMessageType.self, forKey: .messageType) timetoken = Timetoken(try container.decode(String.self, forKey: .timetoken)) ?? 0 actions = try container.decodeIfPresent(RawMessageAction.self, forKey: .actions) ?? [:] + messageType = try container.decodeIfPresent(LegacyPubNubMessageType.self, forKey: .messageType) ?? .message + customMessageType = try container.decodeIfPresent(String.self, forKey: .customMessageType) error = nil } @@ -354,8 +366,9 @@ struct MessageHistoryMessagePayload: Codable { try container.encode(timetoken.description, forKey: .timetoken) try container.encodeIfPresent(meta, forKey: .meta) try container.encodeIfPresent(uuid, forKey: .uuid) - try container.encodeIfPresent(messageType, forKey: .messageType) try container.encode(actions, forKey: .actions) + try container.encodeIfPresent(messageType, forKey: .messageType) + try container.encodeIfPresent(customMessageType, forKey: .customMessageType) } } diff --git a/Sources/PubNub/Networking/Routers/PublishRouter.swift b/Sources/PubNub/Networking/Routers/PublishRouter.swift index b7246b75..f2d7a5be 100644 --- a/Sources/PubNub/Networking/Routers/PublishRouter.swift +++ b/Sources/PubNub/Networking/Routers/PublishRouter.swift @@ -15,11 +15,33 @@ import Foundation struct PublishRouter: HTTPRouter { // Nested Endpoint enum Endpoint: CustomStringConvertible { - case publish(message: AnyJSON, channel: String, shouldStore: Bool?, ttl: Int?, meta: AnyJSON?) - case compressedPublish(message: AnyJSON, channel: String, shouldStore: Bool?, ttl: Int?, meta: AnyJSON?) - case fire(message: AnyJSON, channel: String, meta: AnyJSON?) - case signal(message: AnyJSON, channel: String) - case file(message: FilePublishPayload, shouldStore: Bool?, ttl: Int?, meta: AnyJSON?) + case publish( + message: AnyJSON, channel: String, + customMessageType: String?, shouldStore: Bool?, + ttl: Int?, meta: AnyJSON? + ) + case compressedPublish( + message: AnyJSON, channel: String, + customMessageType: String?, shouldStore: Bool?, + ttl: Int?, meta: AnyJSON? + ) + case fire( + message: AnyJSON, + channel: String, + meta: AnyJSON? + ) + case signal( + message: AnyJSON, + channel: String, + customMessageType: String? + ) + case file( + message: FilePublishPayload, + customMessageType: String?, + shouldStore: Bool?, + ttl: Int?, + meta: AnyJSON? + ) var description: String { switch self { @@ -57,20 +79,19 @@ struct PublishRouter: HTTPRouter { var path: Result { switch endpoint { - case let .publish(message, channel, _, _, _): - return append(message: message, - to: "/publish/\(publishKey)/\(subscribeKey)/0/\(channel.urlEncodeSlash)/0/") + case let .publish(message, channel, _, _, _, _): + return append(message: message, to: "/publish/\(publishKey)/\(subscribeKey)/0/\(channel.urlEncodeSlash)/0/") case let .fire(message, channel, _): - return append(message: message, - to: "/publish/\(publishKey)/\(subscribeKey)/0/\(channel.urlEncodeSlash)/0/") - case let .compressedPublish(_, channel, _, _, _): + return append(message: message, to: "/publish/\(publishKey)/\(subscribeKey)/0/\(channel.urlEncodeSlash)/0/") + case let .compressedPublish(_, channel, _, _, _, _): return .success("/publish/\(publishKey)/\(subscribeKey)/0/\(channel.urlEncodeSlash)/0") - case let .signal(message, channel): - return append(message: message, - to: "/signal/\(publishKey)/\(subscribeKey)/0/\(channel.urlEncodeSlash)/0/") - case let .file(message, _, _, _): - return append(message: message, - to: "/v1/files/publish-file/\(publishKey)/\(subscribeKey)/0/\(message.channel.urlEncodeSlash)/0/") + case let .signal(message, channel, _): + return append(message: message, to: "/signal/\(publishKey)/\(subscribeKey)/0/\(channel.urlEncodeSlash)/0/") + case let .file(message, _, _, _, _): + return append( + message: message, + to: "/v1/files/publish-file/\(publishKey)/\(subscribeKey)/0/\(message.channel.urlEncodeSlash)/0/" + ) } } @@ -88,24 +109,29 @@ struct PublishRouter: HTTPRouter { var query = defaultQueryItems switch endpoint { - case let .publish(_, _, shouldStore, ttl, meta): - return parsePublish(query: &query, store: shouldStore, ttl: ttl, meta: meta) - case let .compressedPublish(_, _, shouldStore, ttl, meta): - return parsePublish(query: &query, store: shouldStore, ttl: ttl, meta: meta) + case let .publish(_, _, customMessageType, shouldStore, ttl, meta): + return parsePublish(query: &query, customMessageType: customMessageType, store: shouldStore, ttl: ttl, meta: meta) + case let .compressedPublish(_, _, customMessageType, shouldStore, ttl, meta): + return parsePublish(query: &query, customMessageType: customMessageType, store: shouldStore, ttl: ttl, meta: meta) case let .fire(_, _, meta): - return parsePublish(query: &query, store: false, ttl: 0, meta: meta) - case .signal: - break - case let .file(_, shouldStore, ttl, meta): - return parsePublish(query: &query, store: shouldStore, ttl: ttl, meta: meta) + return parsePublish(query: &query, customMessageType: nil, store: false, ttl: 0, meta: meta) + case let .signal(_, _, customMessageType): + return parsePublish(query: &query, customMessageType: customMessageType, store: nil, ttl: nil, meta: nil) + case let .file(_, customMessageType, shouldStore, ttl, meta): + return parsePublish(query: &query, customMessageType: customMessageType, store: shouldStore, ttl: ttl, meta: meta) } - - return .success(query) } - func parsePublish(query: inout [URLQueryItem], store: Bool?, ttl: Int?, meta: AnyJSON?) -> QueryResult { + func parsePublish( + query: inout [URLQueryItem], + customMessageType: String?, + store: Bool?, + ttl: Int?, + meta: AnyJSON? + ) -> QueryResult { query.appendIfPresent(key: .store, value: store?.stringNumber) query.appendIfPresent(key: .ttl, value: ttl?.description) + query.appendIfPresent(key: .customMessageType, value: customMessageType) if let meta = meta, !meta.isEmpty { return meta.jsonStringifyResult.map { json -> [URLQueryItem] in @@ -127,7 +153,7 @@ struct PublishRouter: HTTPRouter { var body: Result { switch endpoint { - case let .compressedPublish(message, _, _, _, _): + case let .compressedPublish(message, _, _, _, _, _): if let cryptoModule = configuration.cryptoModule { return message.jsonStringifyResult.flatMap { cryptoModule.encrypt(string: $0) @@ -152,12 +178,12 @@ struct PublishRouter: HTTPRouter { var validationErrorDetail: String? { switch endpoint { - case let .publish(message, channel, _, _, _): + case let .publish(message, channel, _, _, _, _): return isInvalidForReason( (message.isEmpty, ErrorDescription.emptyMessagePayload), (channel.isEmpty, ErrorDescription.emptyChannelString) ) - case let .compressedPublish(message, channel, _, _, _): + case let .compressedPublish(message, channel, _, _, _, _): return isInvalidForReason( (message.isEmpty, ErrorDescription.emptyMessagePayload), (channel.isEmpty, ErrorDescription.emptyChannelString) @@ -167,12 +193,12 @@ struct PublishRouter: HTTPRouter { (message.isEmpty, ErrorDescription.emptyMessagePayload), (channel.isEmpty, ErrorDescription.emptyChannelString) ) - case let .signal(message, channel): + case let .signal(message, channel, _): return isInvalidForReason( (message.isEmpty, ErrorDescription.emptyMessagePayload), (channel.isEmpty, ErrorDescription.emptyChannelString) ) - case let .file(message, _, _, _): + case let .file(message, _, _, _, _): return message.validationErrorDetail } } diff --git a/Sources/PubNub/Networking/Routers/SubscribeRouter.swift b/Sources/PubNub/Networking/Routers/SubscribeRouter.swift index 0f863c14..125d7a72 100644 --- a/Sources/PubNub/Networking/Routers/SubscribeRouter.swift +++ b/Sources/PubNub/Networking/Routers/SubscribeRouter.swift @@ -139,8 +139,14 @@ struct SubscribeDecoder: ResponseDecoder { if let timetokenResponse = try? Constant.jsonDecoder.decode( Payload.self, from: truncatedData ).cursor { - return .failure(PubNubError(.jsonDataDecodingFailure, response: response, error: error, - affected: [.subscribe(timetokenResponse)])) + return .failure( + PubNubError( + .jsonDataDecodingFailure, + response: response, + error: error, + affected: [.subscribe(timetokenResponse)] + ) + ) } } @@ -276,6 +282,7 @@ public struct SubscribeMessagePayload: Codable, Hashable { public let subscription: String? public let channel: String public let messageType: Action + public var customMessageType: String? public var payload: AnyJSON public let flags: Int public let publisher: String? @@ -291,6 +298,7 @@ public struct SubscribeMessagePayload: Codable, Hashable { case channel = "c" case payload = "d" case messageType = "e" + case customMessageType = "cmt" case flags = "f" case publisher = "i" case subscribeKey = "k" @@ -332,6 +340,7 @@ public struct SubscribeMessagePayload: Codable, Hashable { subscription: String?, channel: String, messageType: Action, + customMessageType: String? = nil, payload: AnyJSON, flags: Int, publisher: String?, @@ -345,6 +354,7 @@ public struct SubscribeMessagePayload: Codable, Hashable { self.subscription = subscription self.channel = channel self.messageType = messageType + self.customMessageType = customMessageType self.payload = payload self.flags = flags self.publisher = publisher @@ -359,9 +369,7 @@ public struct SubscribeMessagePayload: Codable, Hashable { let container = try decoder.container(keyedBy: CodingKeys.self) shard = try container.decode(String.self, forKey: .shard) - subscription = try container - .decodeIfPresent(String.self, forKey: .subscription)? - .trimmingPresenceChannelSuffix + subscription = try container.decodeIfPresent(String.self, forKey: .subscription)?.trimmingPresenceChannelSuffix payload = try container.decode(AnyJSON.self, forKey: .payload) flags = try container.decode(Int.self, forKey: .flags) publisher = try container.decodeIfPresent(String.self, forKey: .publisher) @@ -369,11 +377,12 @@ public struct SubscribeMessagePayload: Codable, Hashable { originTimetoken = try container.decodeIfPresent(SubscribeCursor.self, forKey: .originTimetoken) publishTimetoken = try container.decode(SubscribeCursor.self, forKey: .publishTimetoken) metadata = try container.decodeIfPresent(AnyJSON.self, forKey: .meta) + customMessageType = try container.decodeIfPresent(String.self, forKey: .customMessageType) - let messageType = try container.decodeIfPresent(Int.self, forKey: .messageType) + let pubNubMessageType = try container.decodeIfPresent(Int.self, forKey: .messageType) let fullChannel = try container.decode(String.self, forKey: .channel) - if let messageType = messageType, let action = Action(rawValue: messageType) { + if let pubNubMessageType = pubNubMessageType, let action = Action(rawValue: pubNubMessageType) { self.messageType = action } else { // If channel endswith -pnpres we assume it's a presence event @@ -406,6 +415,8 @@ public struct SubscribeMessagePayload: Codable, Hashable { if messageType != .presence { try container.encode(messageType, forKey: .messageType) } + + try container.encode(customMessageType, forKey: .customMessageType) } // swiftlint:disable:next file_length } diff --git a/Sources/PubNub/PubNub.swift b/Sources/PubNub/PubNub.swift index 300f07f4..ef0d77dd 100644 --- a/Sources/PubNub/PubNub.swift +++ b/Sources/PubNub/PubNub.swift @@ -205,8 +205,9 @@ public extension PubNub { /// 4. If `storeTTL` is not specified, then expiration of the message defaults back to the expiry value for the key. /// /// - Parameters: - /// - channel: The destination of the message - /// - message: The message to publish + /// - channel: The destination of the message. + /// - message: The message to publish. + /// - customMessageType: Custom message type. /// - shouldStore: If true the published message is stored in history. /// - storeTTL: Set a per message time to live in storage. /// - meta: Publish extra metadata with the request. @@ -218,6 +219,7 @@ public extension PubNub { func publish( channel: String, message: JSONCodable, + customMessageType: String? = nil, shouldStore: Bool? = nil, storeTTL: Int? = nil, meta: JSONCodable? = nil, @@ -231,6 +233,7 @@ public extension PubNub { .compressedPublish( message: message.codableValue, channel: channel, + customMessageType: customMessageType, shouldStore: shouldStore, ttl: storeTTL, meta: meta?.codableValue @@ -242,6 +245,7 @@ public extension PubNub { .publish( message: message.codableValue, channel: channel, + customMessageType: customMessageType, shouldStore: shouldStore, ttl: storeTTL, meta: meta?.codableValue @@ -305,6 +309,7 @@ public extension PubNub { /// - Parameters: /// - channel: The destination of the message /// - message: The message to publish + /// - customMessageType: Custom signal type. /// - custom: Custom configuration overrides for this request /// - completion: The async `Result` of the method call /// - **Success**: The `Timetoken` of the published Message @@ -312,12 +317,13 @@ public extension PubNub { func signal( channel: String, message: JSONCodable, + customMessageType: String? = nil, custom requestConfig: RequestConfiguration = RequestConfiguration(), completion: ((Result) -> Void)? ) { route( PublishRouter( - .signal(message: message.codableValue, channel: channel), + .signal(message: message.codableValue, channel: channel, customMessageType: customMessageType), configuration: requestConfig.customConfiguration ?? configuration ), requestOperator: configuration.automaticRetry?.retryOperator(for: .messageSend), @@ -1059,6 +1065,7 @@ public extension PubNub { /// - includeMeta: If `true` the meta properties of messages will be included in the response /// - includeUUID: If `true` the UUID of the message publisher will be included with each message in the response /// - includeMessageType: If `true` the message type will be included with each message + /// - includeCustomMessageType: If `true` the user-provided custom message type will be included with each message /// - page: The paging object used for pagination /// - custom: Custom configuration overrides for this request /// - completion: The async `Result` of the method call @@ -1066,8 +1073,11 @@ public extension PubNub { /// - **Failure**: An `Error` describing the failure func fetchMessageHistory( for channels: [String], - includeActions: Bool = false, includeMeta: Bool = false, - includeUUID: Bool = true, includeMessageType: Bool = true, + includeActions: Bool = false, + includeMeta: Bool = false, + includeUUID: Bool = true, + includeMessageType: Bool = true, + includeCustomMessageType: Bool = false, page: PubNubBoundedPage? = PubNubBoundedPageBase(), custom requestConfig: RequestConfiguration = RequestConfiguration(), completion: ((Result<(messagesByChannel: [String: [PubNubMessage]], next: PubNubBoundedPage?), Error>) -> Void)? @@ -1080,7 +1090,7 @@ public extension PubNub { .fetchWithActions( channel: channels.first ?? "", max: page?.limit ?? 25, start: page?.start, end: page?.end, - includeMeta: includeMeta, includeMessageType: includeMessageType, + includeMeta: includeMeta, includeMessageType: includeMessageType, includeCustomMessageType: includeCustomMessageType, includeUUID: includeUUID ), configuration: requestConfig.customConfiguration ?? configuration @@ -1090,7 +1100,7 @@ public extension PubNub { .fetch( channels: channels, max: page?.limit ?? 100, start: page?.start, end: page?.end, - includeMeta: includeMeta, includeMessageType: includeMessageType, + includeMeta: includeMeta, includeMessageType: includeMessageType, includeCustomMessageType: includeCustomMessageType, includeUUID: includeUUID ), configuration: requestConfig.customConfiguration ?? configuration @@ -1100,7 +1110,7 @@ public extension PubNub { .fetch( channels: channels, max: page?.limit ?? 25, start: page?.start, end: page?.end, - includeMeta: includeMeta, includeMessageType: includeMessageType, + includeMeta: includeMeta, includeMessageType: includeMessageType, includeCustomMessageType: includeCustomMessageType, includeUUID: includeUUID ), configuration: requestConfig.customConfiguration ?? configuration diff --git a/Tests/PubNubContractTest/PubNubContractTestCase.swift b/Tests/PubNubContractTest/PubNubContractTestCase.swift index bec9fba2..89bc86f8 100644 --- a/Tests/PubNubContractTest/PubNubContractTestCase.swift +++ b/Tests/PubNubContractTest/PubNubContractTestCase.swift @@ -18,10 +18,10 @@ let defaultSubscribeKey = "demo-36" let defaultPublishKey = "demo-36" @objc public class PubNubContractTestCase: XCTestCase { - fileprivate var listener: SubscriptionListener! public var messageReceivedHandler: ((PubNubMessage, [PubNubMessage]) -> Void)? + public var fileReceivedHandler: ((PubNubFileEvent, [PubNubFileEvent]) -> Void)? public var statusReceivedHandler: ((SubscriptionListener.StatusEvent, [SubscriptionListener.StatusEvent]) -> Void)? public var presenceChangeReceivedHandler: ((PubNubPresenceChange, [PubNubPresenceChange]) -> Void)? @@ -29,11 +29,12 @@ let defaultPublishKey = "demo-36" fileprivate static var _receivedStatuses: [SubscriptionListener.StatusEvent] = [] fileprivate static var _receivedMessages: [PubNubMessage] = [] fileprivate static var _receivedPresenceChanges: [PubNubPresenceChange] = [] - + fileprivate static var _receivedFiles: [PubNubFileEvent] = [] fileprivate static var _currentScenario: CCIScenarioDefinition? fileprivate static var _apiCallResults: [Any] = [] - fileprivate static var _currentConfiguration = PubNubContractTestCase._defaultConfiguration + fileprivate static var currentClient: PubNub? + fileprivate static var _defaultConfiguration: PubNubConfiguration { PubNubConfiguration( publishKey: defaultPublishKey, @@ -44,8 +45,6 @@ let defaultPublishKey = "demo-36" supressLeaveEvents: true ) } - - fileprivate static var currentClient: PubNub? public var configuration: PubNubConfiguration { PubNubContractTestCase._currentConfiguration } @@ -74,6 +73,11 @@ let defaultPublishKey = "demo-36" set { PubNubContractTestCase._receivedMessages = newValue } } + public var receivedFiles: [PubNubFileEvent] { + get { PubNubContractTestCase._receivedFiles } + set { PubNubContractTestCase._receivedFiles = newValue } + } + public var receivedPresenceChanges: [PubNubPresenceChange] { get { PubNubContractTestCase._receivedPresenceChanges } set { PubNubContractTestCase._receivedPresenceChanges = newValue } @@ -97,9 +101,14 @@ let defaultPublishKey = "demo-36" } PubNubContractTestCase._currentConfiguration = configuration } - + func createPubNubClient() -> PubNub { - PubNub(configuration: configuration) + // In the unit test target only, URLSession with a background configuration fails to create a URLSessionUploadTask. + // Therefore, it is replaced with a standard configuration: https://developer.apple.com/forums/thread/725625 + PubNub( + configuration: configuration, + fileSession: URLSession(configuration: .default, delegate: FileSessionManager(), delegateQueue: .main) + ) } public func startCucumberHookEventsListening() { @@ -125,6 +134,8 @@ let defaultPublishKey = "demo-36" receivedMessages.removeAll() receivedPresenceChanges.removeAll() apiCallResults.removeAll() + receivedFiles.removeAll() + listener = nil } @objc public func setup() { @@ -178,7 +189,7 @@ let defaultPublishKey = "demo-36" XCTAssertFalse(result is Error, "Last API call shouldn't fail.") } - Then("I receive error response") { _, _ in + Then("I receive (an )?error response") { _, _ in let lastResult = self.lastResult() XCTAssertNotNil(lastResult, "There is no API calls results.") @@ -298,16 +309,34 @@ let defaultPublishKey = "demo-36" } } } - + listener.didReceiveMessage = { [weak self] message in guard let strongSelf = self else { return } strongSelf.receivedMessages.append(message) + + if let handler = strongSelf.messageReceivedHandler { + handler(message, strongSelf.receivedMessages) + } + } + + listener.didReceiveSignal = { [weak self] message in + guard let strongSelf = self else { return } + strongSelf.receivedMessages.append(message) if let handler = strongSelf.messageReceivedHandler { handler(message, strongSelf.receivedMessages) } } + listener.didReceiveFileUpload = { [weak self] file in + guard let strongSelf = self else { return } + strongSelf.receivedFiles.append(file) + + if let handler = strongSelf.fileReceivedHandler { + handler(file, strongSelf.receivedFiles) + } + } + listener.didReceivePresence = { [weak self] presenceChange in guard let strongSelf = self else { return } strongSelf.receivedPresenceChanges.append(presenceChange) @@ -319,7 +348,7 @@ let defaultPublishKey = "demo-36" client.add(listener) client.subscribe(to: channels, and: groups, at: timetoken, withPresence: presence) - + wait(for: [subscribeStatusExpect], timeout: 10.0) } @@ -343,8 +372,26 @@ let defaultPublishKey = "demo-36" } } - // MARK: - Presence - + public func waitForFiles(_: PubNub, count: Int) -> [PubNubFileEvent]? { + if receivedFiles.count < count { + let subscribeFileExpect = expectation(description: "Subscribe files") + subscribeFileExpect.assertForOverFulfill = false + fileReceivedHandler = { _, files in + if files.count >= count { + subscribeFileExpect.fulfill() + } + } + + wait(for: [subscribeFileExpect], timeout: 30.0) + } + + if receivedFiles.count > count { + return Array(receivedFiles[.. 0 ? receivedFiles : nil + } + } + @discardableResult public func waitForPresenceChanges(_: PubNub, count: Int) -> [PubNubPresenceChange]? { if receivedPresenceChanges.count < count { diff --git a/Tests/PubNubContractTest/Steps/EventEngine/PubNubPresenceEngineContractTestSteps.swift b/Tests/PubNubContractTest/Steps/EventEngine/PubNubPresenceEngineContractTestSteps.swift index a28e73a8..6e990183 100644 --- a/Tests/PubNubContractTest/Steps/EventEngine/PubNubPresenceEngineContractTestSteps.swift +++ b/Tests/PubNubContractTest/Steps/EventEngine/PubNubPresenceEngineContractTestSteps.swift @@ -118,7 +118,7 @@ class PubNubPresenceEngineContractTestsSteps: PubNubEventEngineContractTestsStep )) } - Given("a linear reconnection policy with 3 retries") { _, _ in + Given("^a linear reconnection policy with 3 retries$") { _, _ in self.replacePubNubConfiguration(with: PubNubConfiguration( publishKey: self.configuration.publishKey, subscribeKey: self.configuration.subscribeKey, diff --git a/Tests/PubNubContractTest/Steps/EventEngine/PubNubSubscribeEngineContractTestsSteps.swift b/Tests/PubNubContractTest/Steps/EventEngine/PubNubSubscribeEngineContractTestsSteps.swift index 5b65bb57..a37be908 100644 --- a/Tests/PubNubContractTest/Steps/EventEngine/PubNubSubscribeEngineContractTestsSteps.swift +++ b/Tests/PubNubContractTest/Steps/EventEngine/PubNubSubscribeEngineContractTestsSteps.swift @@ -122,7 +122,7 @@ class PubNubSubscribeEngineContractTestsSteps: PubNubEventEngineContractTestsSte override public func setup() { startCucumberHookEventsListening() - Given("a linear reconnection policy with 3 retries") { _, _ in + Given("^a linear reconnection policy with 3 retries$") { _, _ in self.replacePubNubConfiguration(with: PubNubConfiguration( publishKey: self.configuration.publishKey, subscribeKey: self.configuration.subscribeKey, @@ -136,7 +136,7 @@ class PubNubSubscribeEngineContractTestsSteps: PubNubEventEngineContractTestsSte )) } - Given("the demo keyset with event engine enabled") { _, _ in + Given("^the demo keyset with event engine enabled$") { _, _ in self.replacePubNubConfiguration(with: PubNubConfiguration( publishKey: self.configuration.publishKey, subscribeKey: self.configuration.subscribeKey, @@ -149,24 +149,24 @@ class PubNubSubscribeEngineContractTestsSteps: PubNubEventEngineContractTestsSte )) } - When("I subscribe") { _, _ in + When("^I subscribe$") { _, _ in self.subscribeSynchronously(self.client, to: ["test"]) } - When("I subscribe with timetoken 12345678901234567") { _, _ in + When("^I subscribe with timetoken 12345678901234567$") { _, _ in self.subscribeSynchronously(self.client, to: ["test"], timetoken: 12345678901234567) } - Then("I receive an error in my subscribe response") { _, _ in + Then("^I receive an error in my subscribe response$") { _, _ in XCTAssertNotNil(self.receivedErrorStatuses.first) } - Then("I receive the message in my subscribe response") { _, _ 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:") { _, 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/PubNubContractTest/Steps/Files/PubNubFilesContractTestSteps.swift b/Tests/PubNubContractTest/Steps/Files/PubNubFilesContractTestSteps.swift index 8de81dfe..94205249 100644 --- a/Tests/PubNubContractTest/Steps/Files/PubNubFilesContractTestSteps.swift +++ b/Tests/PubNubContractTest/Steps/Files/PubNubFilesContractTestSteps.swift @@ -104,5 +104,33 @@ public class PubNubFilesContractTestSteps: PubNubContractTestCase { self.wait(for: [sendFileExpect], timeout: 60.0) } + + When("^I send a file with '(.+)' customMessageType$") { args, _ in + let type = args?.first ?? String() + let sendFileExpect = self.expectation(description: "Send file Response") + + guard let data = "test file data".data(using: .utf8) else { + XCTAssert(false, "Unable prepare file data") + return + } + + let publishFileRequest = PubNub.PublishFileRequest(customMessageType: type) + + self.client.send( + .data(data, contentType: nil), + channel: "test", remoteFilename: "name.txt", + publishRequest: publishFileRequest + ) { result in + switch result { + case let .success(sendResults): + self.handleResult(result: sendResults) + case let .failure(error): + self.handleResult(result: error) + } + sendFileExpect.fulfill() + } + + self.wait(for: [sendFileExpect], timeout: 60.0) + } } } diff --git a/Tests/PubNubContractTest/Steps/History/PubNubHistoryContractTestSteps.swift b/Tests/PubNubContractTest/Steps/History/PubNubHistoryContractTestSteps.swift index e0721ee4..f23c987b 100644 --- a/Tests/PubNubContractTest/Steps/History/PubNubHistoryContractTestSteps.swift +++ b/Tests/PubNubContractTest/Steps/History/PubNubHistoryContractTestSteps.swift @@ -15,8 +15,69 @@ import PubNubSDK public class PubNubHistoryContractTestSteps: PubNubContractTestCase { override public func setup() { startCucumberHookEventsListening() + + When("^I fetch message history for '(.*)' channel$") { args, _ in + guard let channel = args?.first else { + XCTAssertNotNil(args?.first, "Step match failed") + return + } + + let historyExpect = self.expectation(description: "Fetch history Response") - When("^I fetch message history for (.*) channel(s)?$") { args, _ in + self.client.fetchMessageHistory(for: [channel]) { result in + switch result { + case let .success((messagesByChannel, next)): + self.handleResult(result: (messagesByChannel, next)) + case let .failure(error): + self.handleResult(result: error) + } + historyExpect.fulfill() + } + + self.wait(for: [historyExpect], timeout: 60.0) + } + + Match(["And"], "^history response contains messages (with|without) (?:customMessageType|'([^']*)' and '([^']*)' (?:message )?types?)$") { args, _ in + guard let matches = args, let inclusionFlag = matches.first else { + XCTAssertNotNil(args?.first, "Step match failed") + return + } + guard let lastResult = self.lastResult() else { + XCTFail("Fetch history didn't returned response"); return + } + guard + let result = lastResult as? (messagesByChannel: [String: [PubNubMessageBase]], next: PubNubBoundedPage?), + let channel = result.messagesByChannel.first?.key, let messages = result.messagesByChannel[channel] + else { + XCTFail("Fetch history returned unexpected response"); return + } + + XCTAssertGreaterThan(messages.count, 0) + + var messagesWithTypes: [String] = [] + + switch self.currentScenario?.name { + case "Client can fetch history without customMessageType enabled by default": + messagesWithTypes = messages.compactMap { $0.customMessageType } + case "Client can fetch history with customMessageType": + messagesWithTypes = messages.compactMap { $0.customMessageType } + case "Client can fetch history with message types": + messagesWithTypes = messages.compactMap { String(describing: $0.messageType.rawValue) } + default: + XCTFail("Unexpected condition") + } + + XCTAssertFalse(inclusionFlag == "with" && messagesWithTypes.count == 0) + XCTAssertFalse(inclusionFlag == "without" && messagesWithTypes.count > 0) + + if matches.count > 1 { + XCTAssertTrue(messagesWithTypes.map { String(describing: $0.rawValue) }.allSatisfy { Array(matches[1...]).contains($0) }) + } else { + XCTAssertEqual(inclusionFlag == "with" ? messages.count : 0, messagesWithTypes.count) + } + } + + When("^I fetch message history for (single|multiple) channel(s)?$") { args, _ in guard let type = args?.first else { XCTAssertNotNil(args?.first, "Step match failed") return @@ -37,6 +98,33 @@ public class PubNubHistoryContractTestSteps: PubNubContractTestCase { self.wait(for: [historyExpect], timeout: 60.0) } + + When("^I fetch message history with (messageType|customMessageType) for '(.*)' channel$") { args, _ in + guard let type = args?.first, let channel = args?.last else { + XCTAssertNotNil(args?.first, "Step match failed") + return + } + + let historyExpect = self.expectation(description: "Fetch history Response") + let includeMessageType = true + let includeCustomMessageType = type == "customMessageType" + + self.client.fetchMessageHistory( + for: [channel], + includeMessageType: includeMessageType, + includeCustomMessageType: includeCustomMessageType + ) { result in + switch result { + case let .success((messagesByChannel, next)): + self.handleResult(result: (messagesByChannel, next)) + case let .failure(error): + self.handleResult(result: error) + } + historyExpect.fulfill() + } + + self.wait(for: [historyExpect], timeout: 60.0) + } Then("the response contains pagination info") { _, _ in guard let lastResult = self.lastResult() else { @@ -66,5 +154,36 @@ public class PubNubHistoryContractTestSteps: PubNubContractTestCase { self.wait(for: [historyExpect], timeout: 60.0) } + + When("^I fetch message history with '(.*)' set to '(.*)' for '(.*)' channel$") { args, _ in + guard args?.count == 3, let channel = args?[2] else { + XCTAssertNotNil(args?.first, "Step match failed") + return + } + + /// Message types enabled by default and user can only opt-out them. + var includeType = true + + if args?.first == "include_custom_message_type" { + includeType = args?[1] == "true" + } + + let historyExpect = self.expectation(description: "Fetch history Response") + + self.client.fetchMessageHistory( + for: [channel], + includeCustomMessageType: includeType + ) { result in + switch result { + case let .success((messagesByChannel, next)): + self.handleResult(result: (messagesByChannel, next)) + case let .failure(error): + self.handleResult(result: error) + } + historyExpect.fulfill() + } + + self.wait(for: [historyExpect], timeout: 60.0) + } } } diff --git a/Tests/PubNubContractTest/Steps/Publish/PubNubPublishContractTestSteps.swift b/Tests/PubNubContractTest/Steps/Publish/PubNubPublishContractTestSteps.swift index 633171a9..f2752b53 100644 --- a/Tests/PubNubContractTest/Steps/Publish/PubNubPublishContractTestSteps.swift +++ b/Tests/PubNubContractTest/Steps/Publish/PubNubPublishContractTestSteps.swift @@ -16,7 +16,7 @@ public class PubNubPublishContractTestSteps: PubNubContractTestCase { override public func setup() { startCucumberHookEventsListening() - When("I publish a message") { _, _ in + When("^I publish a message$") { _, _ in let publishMessageExpect = self.expectation(description: "Publish message Response") self.client.publish(channel: "test", message: "hello") { result in @@ -54,7 +54,32 @@ public class PubNubPublishContractTestSteps: PubNubContractTestCase { self.wait(for: [publishMessageExpect], timeout: 60.0) } - When("I send a signal") { _, _ in + When("^I publish message with '(.*)' customMessageType$") { args, _ in + guard let type = args?.first as? String? else { + XCTAssertNotNil(args?.first, "Step match failed") + return + } + + let publishMessageExpect = self.expectation(description: "Publish message with space and type Response") + + self.client.publish( + channel: "test", + message: "hello", + customMessageType: type + ) { result in + switch result { + case let .success(timetoken): + self.handleResult(result: timetoken) + case let .failure(error): + self.handleResult(result: error) + } + publishMessageExpect.fulfill() + } + + self.wait(for: [publishMessageExpect], timeout: 60.0) + } + + When("^I send a signal$") { _, _ in let sendSignalExpect = self.expectation(description: "Send signal Response") self.client.signal(channel: "test", message: "hello") { result in @@ -69,5 +94,31 @@ public class PubNubPublishContractTestSteps: PubNubContractTestCase { self.wait(for: [sendSignalExpect], timeout: 60.0) } + + + When("^I send a signal with '(.*)' customMessageType$") { args, _ in + guard let type = args?.last else { + XCTAssertNotNil(args?.first, "Step match failed") + return + } + + let publishMessageExpect = self.expectation(description: "Publish message with space and type Response") + + self.client.signal( + channel: "test", + message: "hello", + customMessageType: type + ) { result in + switch result { + case let .success(timetoken): + self.handleResult(result: timetoken) + case let .failure(error): + self.handleResult(result: error) + } + publishMessageExpect.fulfill() + } + + self.wait(for: [publishMessageExpect], timeout: 60.0) + } } } diff --git a/Tests/PubNubContractTest/Steps/Subscribe/PubNubSubscribeContractTestSteps.swift b/Tests/PubNubContractTest/Steps/Subscribe/PubNubSubscribeContractTestSteps.swift index 0713ebe9..f82126ae 100644 --- a/Tests/PubNubContractTest/Steps/Subscribe/PubNubSubscribeContractTestSteps.swift +++ b/Tests/PubNubContractTest/Steps/Subscribe/PubNubSubscribeContractTestSteps.swift @@ -46,22 +46,38 @@ public class PubNubSubscribeContractTestSteps: PubNubContractTestCase { override public func setup() { startCucumberHookEventsListening() - Given("the crypto keyset") { _, _ in + Given("^the crypto keyset$") { _, _ in self.cryptoModule = CryptoModule.legacyCryptoModule(with: "enigma") } - Given("the invalid-crypto keyset") { _, _ in + Given("^the invalid-crypto keyset$") { _, _ in self.cryptoModule = CryptoModule.legacyCryptoModule(with: "secret") } - - When("I subscribe") { _, _ in + + When("^I subscribe$") { _, _ in self.subscribeSynchronously(self.client, to: ["test"]) // Give some time to rotate received timetokens. self.waitFor(delay: 0.25) } + + When("^I subscribe to '(.*)' channel$") { args, _ in + guard let matches = args, let channel = matches.first else { + XCTAssertNotNil(args?.first, "Step match failed") + return + } + + self.subscribeSynchronously(self.client, to: [channel]) + // Give some time to rotate received timetokens. + self.waitFor(delay: 0.25) + } - Then("I receive the message in my subscribe response") { _, userInfo in - let messages = self.waitForMessages(self.client, count: 1) + Then("^I receive (the|[0-9]+) message(s)? in my subscribe response$") { args, userInfo in + guard let match = args?.first else { + XCTAssertNotNil(args?.first, "Step match failed") + return + } + + let messages = self.waitForMessages(self.client, count: Int(match) ?? 1) XCTAssertNotNil(messages) if self.checkTestingFeature(feature: "MessageEncryption", userInfo: userInfo!) { @@ -99,5 +115,18 @@ public class PubNubSubscribeContractTestSteps: PubNubContractTestCase { /// Give some more time for SDK to check that it won't retry after 0.1 seconds. self.waitFor(delay: 0.3) } + + Match(["And"], "^response contains messages with '(.*)' and '(.*)' types$") { args, _ in + guard let matches = args else { + XCTAssertNotNil(args?.first, "Step match failed") + return + } + + let messages = self.waitForMessages(self.client, count: 2)! + XCTAssertNotNil(messages) + + let messagesWithTypes = messages.compactMap { $0.customMessageType } + XCTAssertTrue(messagesWithTypes.map { $0.description }.allSatisfy { matches.contains($0) }) + } } } diff --git a/Tests/PubNubTests/Events/Old/SubscriptionStreamTests.swift b/Tests/PubNubTests/Events/Old/SubscriptionStreamTests.swift index 4ad1ba0e..395bab5a 100644 --- a/Tests/PubNubTests/Events/Old/SubscriptionStreamTests.swift +++ b/Tests/PubNubTests/Events/Old/SubscriptionStreamTests.swift @@ -19,7 +19,8 @@ class SubscriptionListenerTests: XCTestCase { channel: "Channel", subscription: "Channel", published: 0, - metadata: "Message" + metadata: "Message", + messageType: .message ) let connectionEvent: ConnectionStatus = .connected let statusEvent: SubscriptionListener.StatusEvent = .success(.connected) diff --git a/Tests/PubNubTests/Networking/Routers/HistoryRouterTests.swift b/Tests/PubNubTests/Networking/Routers/HistoryRouterTests.swift index 56d3895b..4f2abd65 100644 --- a/Tests/PubNubTests/Networking/Routers/HistoryRouterTests.swift +++ b/Tests/PubNubTests/Networking/Routers/HistoryRouterTests.swift @@ -33,7 +33,8 @@ extension HistoryRouterTests { let router = HistoryRouter( .fetch( channels: testMultiChannels, max: nil, start: nil, end: nil, - includeMeta: false, includeMessageType: false, includeUUID: false + includeMeta: false, includeMessageType: false, + includeCustomMessageType: false, includeUUID: false ), configuration: config ) @@ -54,7 +55,8 @@ extension HistoryRouterTests { let router = HistoryRouter( .fetch( channels: [], max: nil, start: nil, end: nil, - includeMeta: false, includeMessageType: false, includeUUID: false + includeMeta: false, includeMessageType: false, + includeCustomMessageType: false, includeUUID: false ), configuration: config ) @@ -67,7 +69,8 @@ extension HistoryRouterTests { let router = HistoryRouter( .fetch( channels: testMultiChannels, max: nil, start: nil, end: nil, - includeMeta: false, includeMessageType: false, includeUUID: false + includeMeta: false, includeMessageType: false, + includeCustomMessageType: false, includeUUID: false ), configuration: config ) @@ -264,7 +267,8 @@ extension HistoryRouterTests { let router = HistoryRouter( .fetchWithActions( channel: testChannel, max: nil, start: nil, end: nil, - includeMeta: false, includeMessageType: false, includeUUID: false + includeMeta: false, includeMessageType: false, + includeCustomMessageType: false, includeUUID: false ), configuration: config ) @@ -278,7 +282,8 @@ extension HistoryRouterTests { let router = HistoryRouter( .fetchWithActions( channel: "", max: nil, start: nil, end: nil, - includeMeta: false, includeMessageType: false, includeUUID: false + includeMeta: false, includeMessageType: false, + includeCustomMessageType: false, includeUUID: false ), configuration: config ) @@ -291,7 +296,8 @@ extension HistoryRouterTests { let router = HistoryRouter( .fetchWithActions( channel: testChannel, max: nil, start: nil, end: nil, - includeMeta: false, includeMessageType: false, includeUUID: false + includeMeta: false, includeMessageType: false, + includeCustomMessageType: false, includeUUID: false ), configuration: config ) diff --git a/Tests/PubNubTests/Networking/Routers/PublishRouterTests.swift b/Tests/PubNubTests/Networking/Routers/PublishRouterTests.swift index 3f220621..aae12350 100644 --- a/Tests/PubNubTests/Networking/Routers/PublishRouterTests.swift +++ b/Tests/PubNubTests/Networking/Routers/PublishRouterTests.swift @@ -31,7 +31,7 @@ final class PublishRouterTests: XCTestCase { extension PublishRouterTests { func testPublish_Router() { let router = PublishRouter( - .publish(message: testMessage, channel: testChannel, shouldStore: nil, ttl: nil, meta: nil), + .publish(message: testMessage, channel: testChannel, customMessageType: nil, shouldStore: nil, ttl: nil, meta: nil), configuration: config ) @@ -47,7 +47,7 @@ extension PublishRouterTests { func testPublish_Router_ValidationError() { let router = PublishRouter( - .publish(message: [], channel: testChannel, shouldStore: nil, ttl: nil, meta: nil), + .publish(message: [], channel: testChannel, customMessageType: nil, shouldStore: nil, ttl: nil, meta: nil), configuration: config ) @@ -221,7 +221,7 @@ extension PublishRouterTests { extension PublishRouterTests { func testCompressedPublish_Router() { let router = PublishRouter( - .compressedPublish(message: testMessage, channel: testChannel, shouldStore: nil, ttl: nil, meta: nil), + .compressedPublish(message: testMessage, channel: testChannel, customMessageType: nil, shouldStore: nil, ttl: nil, meta: nil), configuration: config ) @@ -232,7 +232,7 @@ extension PublishRouterTests { func testCompressedPublish_Router_ValidationError() { let router = PublishRouter( - .compressedPublish(message: [], channel: testChannel, shouldStore: nil, ttl: nil, meta: nil), + .compressedPublish(message: [], channel: testChannel, customMessageType: nil, shouldStore: nil, ttl: nil, meta: nil), configuration: config ) @@ -268,7 +268,7 @@ extension PublishRouterTests { let router = PublishRouter( .file( message: FilePublishPayload(channel: testChannel, fileId: testFileId, filename: testFilename), - shouldStore: nil, ttl: nil, meta: nil + customMessageType: nil, shouldStore: nil, ttl: nil, meta: nil ), configuration: config ) @@ -277,6 +277,38 @@ extension PublishRouterTests { XCTAssertEqual(router.category, "Publish a File Message") XCTAssertEqual(router.service, .publish) } + + func testFile_Router_nilMessageTypeAndSpaceId() { + let router = PublishRouter( + .file( + message: FilePublishPayload(channel: testChannel, fileId: testFileId, filename: testFilename), + customMessageType: nil, shouldStore: nil, ttl: nil, meta: nil + ), + configuration: config + ) + + guard let queryItems = try? router.queryItems.get() else { + return XCTAssert(false, "'queryItems' not set") + } + + XCTAssertNil(queryItems.first(where: { $0.name == QueryKey.type.rawValue })) + } + + func testFile_Router_notNilMessageType() { + let router = PublishRouter( + .file( + message: FilePublishPayload(channel: testChannel, fileId: testFileId, filename: testFilename), + customMessageType: "type", shouldStore: nil, ttl: nil, meta: nil + ), + configuration: config + ) + + guard let queryItems = try? router.queryItems.get() else { + return XCTAssert(false, "'queryItems' not set") + } + + XCTAssertTrue(queryItems.contains(URLQueryItem(name: QueryKey.customMessageType.rawValue, value: "type"))) + } func testFile_Router_Validate_Message() { let file = FilePublishPayload( @@ -299,7 +331,7 @@ extension PublishRouterTests { let router = PublishRouter( .file( message: FilePublishPayload(channel: "", fileId: testFileId, filename: testFilename), - shouldStore: nil, ttl: nil, meta: nil + customMessageType: nil, shouldStore: nil, ttl: nil, meta: nil ), configuration: config ) @@ -310,7 +342,7 @@ extension PublishRouterTests { let router = PublishRouter( .file( message: FilePublishPayload(channel: testChannel, fileId: "", filename: testFilename), - shouldStore: nil, ttl: nil, meta: nil + customMessageType: nil, shouldStore: nil, ttl: nil, meta: nil ), configuration: config ) @@ -321,7 +353,7 @@ extension PublishRouterTests { let router = PublishRouter( .file( message: FilePublishPayload(channel: testChannel, fileId: testFileId, filename: ""), - shouldStore: nil, ttl: nil, meta: nil + customMessageType: nil, shouldStore: nil, ttl: nil, meta: nil ), configuration: config ) @@ -420,7 +452,7 @@ extension PublishRouterTests { extension PublishRouterTests { func testSignal_Router() { - let router = PublishRouter(.signal(message: testMessage, channel: testChannel), configuration: config) + let router = PublishRouter(.signal(message: testMessage, channel: testChannel, customMessageType: nil), configuration: config) XCTAssertEqual(router.endpoint.description, "Signal") XCTAssertEqual(router.category, "Signal") @@ -428,7 +460,7 @@ extension PublishRouterTests { } func testSignal_Router_ValidationError() { - let router = PublishRouter(.signal(message: "", channel: testChannel), configuration: config) + let router = PublishRouter(.signal(message: "", channel: testChannel, customMessageType: nil), configuration: config) XCTAssertNotEqual(router.validationError?.pubNubError, PubNubError(.invalidEndpointType, router: router)) }